From 4ec65b4b9706e94483913cb33e0a585d3ffc3119 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Thu, 31 Oct 2024 09:44:04 +0000 Subject: [PATCH 01/17] Remove dependency on IMethodAspectHandler. Add WithTracing to source generator. add dependency Amazon.Lambda.Serialization.SystemTextJson --- .../AWS.Lambda.Powertools.Tracing.csproj | 1 + .../Internal/TracingAspect.cs | 310 ++++++++++++++++++ .../Internal/TracingAspectFactory.cs | 33 ++ .../Internal/TracingAspectHandler.cs | 292 ----------------- .../PowertoolsTracingSerializer.cs | 108 ++++++ .../TracingSerializerExtensions.cs | 66 ++++ .../TracingAttribute.cs | 20 +- .../Handlers/HandlerTests.cs | 2 +- .../Handlers/Handlers.cs | 1 + .../PowertoolsTracingSerializerTests.cs | 198 +++++++++++ .../Serializers/TestJsonContext.cs | 28 ++ .../TracingSerializerExtensionsTests.cs | 44 +++ .../TracingAspectTests.cs | 178 ++++++++++ .../TracingAttributeTest.cs | 32 +- 14 files changed, 1000 insertions(+), 313 deletions(-) create mode 100644 libraries/src/AWS.Lambda.Powertools.Tracing/Internal/TracingAspect.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.Tracing/Internal/TracingAspectFactory.cs delete mode 100644 libraries/src/AWS.Lambda.Powertools.Tracing/Internal/TracingAspectHandler.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.Tracing/Serializers/PowertoolsTracingSerializer.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.Tracing/Serializers/TracingSerializerExtensions.cs create mode 100644 libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/Serializers/PowertoolsTracingSerializerTests.cs create mode 100644 libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/Serializers/TestJsonContext.cs create mode 100644 libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/Serializers/TracingSerializerExtensionsTests.cs create mode 100644 libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/TracingAspectTests.cs diff --git a/libraries/src/AWS.Lambda.Powertools.Tracing/AWS.Lambda.Powertools.Tracing.csproj b/libraries/src/AWS.Lambda.Powertools.Tracing/AWS.Lambda.Powertools.Tracing.csproj index 9eb67d6c..550a8e83 100644 --- a/libraries/src/AWS.Lambda.Powertools.Tracing/AWS.Lambda.Powertools.Tracing.csproj +++ b/libraries/src/AWS.Lambda.Powertools.Tracing/AWS.Lambda.Powertools.Tracing.csproj @@ -16,6 +16,7 @@ + diff --git a/libraries/src/AWS.Lambda.Powertools.Tracing/Internal/TracingAspect.cs b/libraries/src/AWS.Lambda.Powertools.Tracing/Internal/TracingAspect.cs new file mode 100644 index 00000000..ca0e6df8 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Tracing/Internal/TracingAspect.cs @@ -0,0 +1,310 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +using System; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using AspectInjector.Broker; +using AWS.Lambda.Powertools.Common; +using AWS.Lambda.Powertools.Common.Utils; + +namespace AWS.Lambda.Powertools.Tracing.Internal; + +/// +/// Tracing Aspect +/// Scope.Global is singleton +/// +[Aspect(Scope.Global, Factory = typeof(TracingAspectFactory))] +public class TracingAspect +{ + /// + /// The Powertools for AWS Lambda (.NET) configurations + /// + private readonly IPowertoolsConfigurations _powertoolsConfigurations; + + /// + /// X-Ray Recorder + /// + private readonly IXRayRecorder _xRayRecorder; + + /// + /// If true, then is cold start + /// + private static bool _isColdStart = true; + + /// + /// If true, capture annotations + /// + private static bool _captureAnnotations = true; + + /// + /// If true, annotations have been captured + /// + private bool _isAnnotationsCaptured; + + /// + /// Aspect constructor + /// + /// + /// + public TracingAspect(IPowertoolsConfigurations powertoolsConfigurations, IXRayRecorder xRayRecorder) + { + _powertoolsConfigurations = powertoolsConfigurations; + _xRayRecorder = xRayRecorder; + } + + /// + /// Surrounds the specific method with Tracing aspect + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + [Advice(Kind.Around)] + public object Around( + [Argument(Source.Instance)] object instance, + [Argument(Source.Name)] string name, + [Argument(Source.Arguments)] object[] args, + [Argument(Source.Type)] Type hostType, + [Argument(Source.Metadata)] MethodBase method, + [Argument(Source.ReturnType)] Type returnType, + [Argument(Source.Target)] Func target, + [Argument(Source.Triggers)] Attribute[] triggers) + { + var trigger = triggers.OfType().First(); + + if (TracingDisabled()) + return target(args); + + var @namespace = !string.IsNullOrWhiteSpace(trigger.Namespace) ? trigger.Namespace : _powertoolsConfigurations.Service; + + var (segmentName, metadataName) = string.IsNullOrWhiteSpace(trigger.SegmentName) + ? ($"## {name}", name) + : (trigger.SegmentName, trigger.SegmentName); + + BeginSegment(segmentName, @namespace); + + try + { + var result = target(args); + + if (result is Task task) + { + return WrapTask(task, metadataName, trigger.CaptureMode, @namespace); + } + + HandleResponse(metadataName, result, trigger.CaptureMode, @namespace); + _xRayRecorder.EndSubsegment(); // End segment here for sync methods + return result; + } + catch (Exception ex) + { + HandleException(ex, metadataName, trigger.CaptureMode, @namespace); + _xRayRecorder.EndSubsegment(); // End segment here for sync methods with exceptions + throw; + } + } + + private object WrapTask(Task task, string name, TracingCaptureMode captureMode, string @namespace) + { + if (task.GetType() == typeof(Task)) + { + return WrapVoidTask(task, name, captureMode, @namespace); + } + + // Create an async wrapper task that returns the original task type + async Task AsyncWrapper() + { + try + { + await task; + var result = task.GetType().GetProperty("Result")?.GetValue(task); + HandleResponse(name, result, captureMode, @namespace); + } + catch (Exception ex) + { + HandleException(ex, name, captureMode, @namespace); + throw; + } + finally + { + _xRayRecorder.EndSubsegment(); + } + } + + // Start the wrapper and return original task + _ = AsyncWrapper(); + return task; + } + + private async Task WrapVoidTask(Task task, string name, TracingCaptureMode captureMode, string @namespace) + { + try + { + await task; + HandleResponse(name, null, captureMode, @namespace); + } + catch (Exception ex) + { + HandleException(ex, name, captureMode, @namespace); + throw; + } + finally + { + _xRayRecorder.EndSubsegment(); + } + } + + private void BeginSegment(string segmentName, string @namespace) + { + _xRayRecorder.BeginSubsegment(segmentName); + _xRayRecorder.SetNamespace(@namespace); + + if (_captureAnnotations) + { + _xRayRecorder.AddAnnotation("ColdStart", _isColdStart); + + _captureAnnotations = false; + _isAnnotationsCaptured = true; + + if (_powertoolsConfigurations.IsServiceDefined) + _xRayRecorder.AddAnnotation("Service", _powertoolsConfigurations.Service); + } + + _isColdStart = false; + } + + private void HandleResponse(string segmentName, object result, TracingCaptureMode captureMode, string @namespace) + { + if (!CaptureResponse(captureMode)) return; +#if NET8_0_OR_GREATER + if (!RuntimeFeatureWrapper.IsDynamicCodeSupported) // is AOT + { + _xRayRecorder.AddMetadata( + @namespace, + $"{segmentName} response", + Serializers.PowertoolsTracingSerializer.Serialize(result) + ); + return; + } +#endif + + _xRayRecorder.AddMetadata( + @namespace, + $"{segmentName} response", + result + ); + } + + /// + /// When Aspect Handler exits runs this code to end subsegment + /// + [Advice(Kind.After)] + public void OnExit() + { + if (TracingDisabled()) + return; + + if (_isAnnotationsCaptured) + _captureAnnotations = true; + } + + private bool TracingDisabled() + { + if (_powertoolsConfigurations.TracingDisabled) + { + Console.WriteLine("Tracing has been disabled via env var POWERTOOLS_TRACE_DISABLED"); + return true; + } + + if (!_powertoolsConfigurations.IsLambdaEnvironment) + { + Console.WriteLine("Running outside Lambda environment; disabling Tracing"); + return true; + } + + return false; + } + + private bool CaptureResponse(TracingCaptureMode captureMode) + { + if (TracingDisabled()) + return false; + + return captureMode switch + { + TracingCaptureMode.EnvironmentVariable => _powertoolsConfigurations.TracerCaptureResponse, + TracingCaptureMode.Response => true, + TracingCaptureMode.ResponseAndError => true, + _ => false + }; + } + + private bool CaptureError(TracingCaptureMode captureMode) + { + if (TracingDisabled()) + return false; + + return captureMode switch + { + TracingCaptureMode.EnvironmentVariable => _powertoolsConfigurations.TracerCaptureError, + TracingCaptureMode.Error => true, + TracingCaptureMode.ResponseAndError => true, + _ => false + }; + } + + private void HandleException(Exception exception, string name, TracingCaptureMode captureMode, string @namespace) + { + if (!CaptureError(captureMode)) return; + + var nameSpace = @namespace; + var sb = new StringBuilder(); + sb.AppendLine($"Exception type: {exception.GetType()}"); + sb.AppendLine($"Exception message: {exception.Message}"); + sb.AppendLine($"Stack trace: {exception.StackTrace}"); + + if (exception.InnerException != null) + { + sb.AppendLine("---BEGIN InnerException--- "); + sb.AppendLine($"Exception type {exception.InnerException.GetType()}"); + sb.AppendLine($"Exception message: {exception.InnerException.Message}"); + sb.AppendLine($"Stack trace: {exception.InnerException.StackTrace}"); + sb.AppendLine("---END Inner Exception"); + } + + _xRayRecorder.AddMetadata( + nameSpace, + $"{name} error", + sb.ToString() + ); + } + + /// + /// Resets static variables for test. + /// + internal static void ResetForTest() + { + _isColdStart = true; + _captureAnnotations = true; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Tracing/Internal/TracingAspectFactory.cs b/libraries/src/AWS.Lambda.Powertools.Tracing/Internal/TracingAspectFactory.cs new file mode 100644 index 00000000..b013fde7 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Tracing/Internal/TracingAspectFactory.cs @@ -0,0 +1,33 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + + +using System; +using AWS.Lambda.Powertools.Common; + +namespace AWS.Lambda.Powertools.Tracing.Internal; + +internal static class TracingAspectFactory +{ + /// + /// Get an instance of the TracingAspect class. + /// + /// The type of the class to be logged. + /// An instance of the TracingAspect class. + public static object GetInstance(Type type) + { + return new TracingAspect(PowertoolsConfigurations.Instance, XRayRecorder.Instance); + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Tracing/Internal/TracingAspectHandler.cs b/libraries/src/AWS.Lambda.Powertools.Tracing/Internal/TracingAspectHandler.cs deleted file mode 100644 index c2863fce..00000000 --- a/libraries/src/AWS.Lambda.Powertools.Tracing/Internal/TracingAspectHandler.cs +++ /dev/null @@ -1,292 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -using System; -using System.Runtime.ExceptionServices; -using System.Text; -using AWS.Lambda.Powertools.Common; - -namespace AWS.Lambda.Powertools.Tracing.Internal; - -/// -/// Class TracingAspectHandler. -/// Implements the -/// -/// -internal class TracingAspectHandler : IMethodAspectHandler -{ - /// - /// The Powertools for AWS Lambda (.NET) configurations - /// - private readonly IPowertoolsConfigurations _powertoolsConfigurations; - - /// - /// X-Ray Recorder - /// - private readonly IXRayRecorder _xRayRecorder; - - /// - /// If true, then is cold start - /// - private static bool _isColdStart = true; - - /// - /// If true, capture annotations - /// - private static bool _captureAnnotations = true; - - /// - /// If true, annotations have been captured - /// - private bool _isAnnotationsCaptured; - - /// - /// Tracing namespace - /// - private readonly string _namespace; - - /// - /// The capture mode - /// - private readonly TracingCaptureMode _captureMode; - - /// - /// The segment name - /// - private readonly string _segmentName; - - /// - /// Initializes a new instance of the class. - /// - /// Name of the segment. - /// The namespace. - /// The capture mode. - /// The Powertools for AWS Lambda (.NET) configurations. - /// The X-Ray recorder. - internal TracingAspectHandler - ( - string segmentName, - string nameSpace, - TracingCaptureMode captureMode, - IPowertoolsConfigurations powertoolsConfigurations, - IXRayRecorder xRayRecorder - ) - { - _segmentName = segmentName; - _namespace = nameSpace; - _captureMode = captureMode; - _powertoolsConfigurations = powertoolsConfigurations; - _xRayRecorder = xRayRecorder; - } - - /// - /// Handles the event. - /// - /// - /// The instance containing the - /// event data. - /// - public void OnEntry(AspectEventArgs eventArgs) - { - if(TracingDisabled()) - return; - - var segmentName = !string.IsNullOrWhiteSpace(_segmentName) ? _segmentName : $"## {eventArgs.Name}"; - var nameSpace = GetNamespace(); - - _xRayRecorder.BeginSubsegment(segmentName); - _xRayRecorder.SetNamespace(nameSpace); - - if (_captureAnnotations) - { - _xRayRecorder.AddAnnotation("ColdStart", _isColdStart); - - _captureAnnotations = false; - _isAnnotationsCaptured = true; - - if (_powertoolsConfigurations.IsServiceDefined) - _xRayRecorder.AddAnnotation("Service", _powertoolsConfigurations.Service); - } - - _isColdStart = false; - } - - /// - /// Called when [success]. - /// - /// - /// The instance containing the - /// event data. - /// - /// The result. - public void OnSuccess(AspectEventArgs eventArgs, object result) - { - if (CaptureResponse()) - { - var nameSpace = GetNamespace(); - - _xRayRecorder.AddMetadata - ( - nameSpace, - $"{eventArgs.Name} response", - result - ); - } - } - - /// - /// Called when [exception]. - /// - /// - /// The instance containing the - /// event data. - /// - /// The exception. - public void OnException(AspectEventArgs eventArgs, Exception exception) - { - if (CaptureError()) - { - var nameSpace = GetNamespace(); - - var sb = new StringBuilder(); - sb.AppendLine($"Exception type: {exception.GetType()}"); - sb.AppendLine($"Exception message: {exception.Message}"); - sb.AppendLine($"Stack trace: {exception.StackTrace}"); - - if (exception.InnerException != null) - { - sb.AppendLine("---BEGIN InnerException--- "); - sb.AppendLine($"Exception type {exception.InnerException.GetType()}"); - sb.AppendLine($"Exception message: {exception.InnerException.Message}"); - sb.AppendLine($"Stack trace: {exception.InnerException.StackTrace}"); - sb.AppendLine("---END Inner Exception"); - } - - _xRayRecorder.AddMetadata - ( - nameSpace, - $"{eventArgs.Name} error", - sb.ToString() - ); - } - - // The purpose of ExceptionDispatchInfo.Capture is to capture a potentially mutating exception's StackTrace at a point in time: - // https://learn.microsoft.com/en-us/dotnet/standard/exceptions/best-practices-for-exceptions#capture-exceptions-to-rethrow-later - ExceptionDispatchInfo.Capture(exception).Throw(); - } - - /// - /// Handles the event. - /// - /// - /// The instance containing the - /// event data. - /// - public void OnExit(AspectEventArgs eventArgs) - { - if(TracingDisabled()) - return; - - if (_isAnnotationsCaptured) - _captureAnnotations = true; - - _xRayRecorder.EndSubsegment(); - } - - /// - /// Gets the namespace. - /// - /// System.String. - private string GetNamespace() - { - return !string.IsNullOrWhiteSpace(_namespace) ? _namespace : _powertoolsConfigurations.Service; - } - - /// - /// Captures the response. - /// - /// true if tracing should capture responses, false otherwise. - private bool CaptureResponse() - { - if(TracingDisabled()) - return false; - - switch (_captureMode) - { - case TracingCaptureMode.EnvironmentVariable: - return _powertoolsConfigurations.TracerCaptureResponse; - case TracingCaptureMode.Response: - case TracingCaptureMode.ResponseAndError: - return true; - case TracingCaptureMode.Error: - case TracingCaptureMode.Disabled: - default: - return false; - } - } - - /// - /// Captures the error. - /// - /// true if tracing should capture errors, false otherwise. - private bool CaptureError() - { - if(TracingDisabled()) - return false; - - switch (_captureMode) - { - case TracingCaptureMode.EnvironmentVariable: - return _powertoolsConfigurations.TracerCaptureError; - case TracingCaptureMode.Error: - case TracingCaptureMode.ResponseAndError: - return true; - case TracingCaptureMode.Response: - case TracingCaptureMode.Disabled: - default: - return false; - } - } - - /// - /// Tracing disabled. - /// - /// true if tracing is disabled, false otherwise. - private bool TracingDisabled() - { - if (_powertoolsConfigurations.TracingDisabled) - { - Console.WriteLine("Tracing has been disabled via env var POWERTOOLS_TRACE_DISABLED"); - return true; - } - - if (!_powertoolsConfigurations.IsLambdaEnvironment) - { - Console.WriteLine("Running outside Lambda environment; disabling Tracing"); - return true; - } - - return false; - } - - /// - /// Resets static variables for test. - /// - internal static void ResetForTest() - { - _isColdStart = true; - _captureAnnotations = true; - } -} diff --git a/libraries/src/AWS.Lambda.Powertools.Tracing/Serializers/PowertoolsTracingSerializer.cs b/libraries/src/AWS.Lambda.Powertools.Tracing/Serializers/PowertoolsTracingSerializer.cs new file mode 100644 index 00000000..52e77410 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Tracing/Serializers/PowertoolsTracingSerializer.cs @@ -0,0 +1,108 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + + +#if NET8_0_OR_GREATER + +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace AWS.Lambda.Powertools.Tracing.Serializers; + +/// +/// Powertools Tracing Serializer +/// Serializes with client Context +/// +public static class PowertoolsTracingSerializer +{ + private static JsonSerializerContext _context; + + /// + /// Adds a JsonSerializerContext for tracing serialization + /// + internal static void AddSerializerContext(JsonSerializerContext context) + { + _context = context; + } + + /// + /// Serializes an object using the configured context + /// + public static string Serialize(object value) + { + if (_context == null) + { + throw new InvalidOperationException("Serializer context not initialized. Ensure WithTracing() is called on the Lambda serializer."); + } + + // Serialize using the context + return JsonSerializer.Serialize(value, value.GetType(), _context); + } + + /// + /// Serializes an object using the configured context and returns a Dictionary + /// + public static IDictionary GetMetadataValue(object value) + { + if (_context == null) + { + throw new InvalidOperationException("Serializer context not initialized. Ensure WithTracing() is called on the Lambda serializer."); + } + + // Serialize using the context + var jsonString = Serialize(value); + + // From here bellow it converts the string to a dictionary + // this is required because xray will double serialize if we just pass the string + // this approach allows to output an object + using var document = JsonDocument.Parse(jsonString); + var result = new Dictionary(); + + foreach (var prop in document.RootElement.EnumerateObject()) + { + result[prop.Name] = ConvertValue(prop.Value); + } + + return result; + } + + private static object ConvertValue(JsonElement element) + { + switch (element.ValueKind) + { + case JsonValueKind.Object: + var dict = new Dictionary(); + foreach (var prop in element.EnumerateObject()) + { + dict[prop.Name] = ConvertValue(prop.Value); + } + return dict; + case JsonValueKind.String: + return element.GetString(); + case JsonValueKind.Number: + return element.GetDouble(); + case JsonValueKind.True: + return true; + case JsonValueKind.False: + return false; + default: + return null; + } + } +} + +#endif \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Tracing/Serializers/TracingSerializerExtensions.cs b/libraries/src/AWS.Lambda.Powertools.Tracing/Serializers/TracingSerializerExtensions.cs new file mode 100644 index 00000000..c1035292 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Tracing/Serializers/TracingSerializerExtensions.cs @@ -0,0 +1,66 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +#if NET8_0_OR_GREATER + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using System.Text.Json.Serialization; +using Amazon.Lambda.Serialization.SystemTextJson; + +namespace AWS.Lambda.Powertools.Tracing.Serializers; + +/// +/// Extensions for SourceGeneratorLambdaJsonSerializer to add tracing support +/// +public static class TracingSerializerExtensions +{ + // Internal helper to access protected methods + private sealed class DefaultOptionsHelper<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] T> + : SourceGeneratorLambdaJsonSerializer + where T : JsonSerializerContext + { + internal static JsonSerializerOptions GetDefaultOptions() + { + var helper = new DefaultOptionsHelper(); + return helper.CreateDefaultJsonSerializationOptions(); + } + } + + /// + /// Adds tracing serialization support to the Lambda serializer + /// + public static SourceGeneratorLambdaJsonSerializer WithTracing<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] T>( + this SourceGeneratorLambdaJsonSerializer serializer) + where T : JsonSerializerContext + { + var options = DefaultOptionsHelper.GetDefaultOptions(); + + var constructor = typeof(T).GetConstructor(new Type[] { typeof(JsonSerializerOptions) }); + if (constructor == null) + { + throw new JsonSerializerException( + $"The serializer {typeof(T).FullName} is missing a constructor that takes in JsonSerializerOptions object"); + } + + var jsonSerializerContext = constructor.Invoke(new object[] { options }) as T; + PowertoolsTracingSerializer.AddSerializerContext(jsonSerializerContext); + + return serializer; + } +} + +#endif \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Tracing/TracingAttribute.cs b/libraries/src/AWS.Lambda.Powertools.Tracing/TracingAttribute.cs index c3a3a5c6..1946fb9c 100644 --- a/libraries/src/AWS.Lambda.Powertools.Tracing/TracingAttribute.cs +++ b/libraries/src/AWS.Lambda.Powertools.Tracing/TracingAttribute.cs @@ -14,6 +14,7 @@ */ using System; +using AspectInjector.Broker; using AWS.Lambda.Powertools.Common; using AWS.Lambda.Powertools.Tracing.Internal; @@ -106,8 +107,9 @@ namespace AWS.Lambda.Powertools.Tracing; /// } /// /// +[Injection(typeof(TracingAspect))] [AttributeUsage(AttributeTargets.Method)] -public class TracingAttribute : MethodAspectAttribute +public class TracingAttribute : Attribute { /// /// Set custom segment name for the operation. @@ -130,20 +132,4 @@ public class TracingAttribute : MethodAspectAttribute /// /// The capture mode. public TracingCaptureMode CaptureMode { get; set; } = TracingCaptureMode.EnvironmentVariable; - - /// - /// Creates the handler. - /// - /// IMethodAspectHandler. - protected override IMethodAspectHandler CreateHandler() - { - return new TracingAspectHandler - ( - SegmentName, - Namespace, - CaptureMode, - PowertoolsConfigurations.Instance, - XRayRecorder.Instance - ); - } } \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/Handlers/HandlerTests.cs b/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/Handlers/HandlerTests.cs index 80a419b1..40bb3bbd 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/Handlers/HandlerTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/Handlers/HandlerTests.cs @@ -107,6 +107,6 @@ public void Dispose() Environment.SetEnvironmentVariable("LAMBDA_TASK_ROOT", ""); Environment.SetEnvironmentVariable("POWERTOOLS_SERVICE_NAME", ""); Environment.SetEnvironmentVariable("POWERTOOLS_TRACE_DISABLED", ""); - TracingAspectHandler.ResetForTest(); + TracingAspect.ResetForTest(); } } \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/Handlers/Handlers.cs b/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/Handlers/Handlers.cs index 385ce96c..9965680d 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/Handlers/Handlers.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/Handlers/Handlers.cs @@ -14,6 +14,7 @@ */ using System; +using System.Threading.Tasks; namespace AWS.Lambda.Powertools.Tracing.Tests; diff --git a/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/Serializers/PowertoolsTracingSerializerTests.cs b/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/Serializers/PowertoolsTracingSerializerTests.cs new file mode 100644 index 00000000..da9bad05 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/Serializers/PowertoolsTracingSerializerTests.cs @@ -0,0 +1,198 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +#if NET8_0_OR_GREATER +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; +using AWS.Lambda.Powertools.Tracing.Serializers; +using Xunit; + +namespace AWS.Lambda.Powertools.Tracing.Tests.Serializers; + +public class PowertoolsTracingSerializerTests +{ + private const string UninitializedContextMessage = "Serializer context not initialized. Ensure WithTracing() is called on the Lambda serializer."; + + public PowertoolsTracingSerializerTests() + { + var context = new TestJsonContext(new JsonSerializerOptions()); + PowertoolsTracingSerializer.AddSerializerContext(context); + } + + [Fact] + public void Serialize_WithSimpleObject_SerializesCorrectly() + { + // Arrange + var testPerson = new TestPerson { Name = "John", Age = 30 }; + + // Act + var result = PowertoolsTracingSerializer.Serialize(testPerson); + + // Assert + Assert.Contains("\"Name\":\"John\"", result); + Assert.Contains("\"Age\":30", result); + } + + [Fact] + public void GetMetadataValue_WithSimpleObject_ReturnsCorrectDictionary() + { + // Arrange + var testPerson = new TestPerson { Name = "John", Age = 30 }; + + // Act + var result = PowertoolsTracingSerializer.GetMetadataValue(testPerson); + + // Assert + Assert.IsType>(result); + Assert.Equal("John", result["Name"]); + Assert.Equal(30.0, result["Age"]); + } + + [Fact] + public void GetMetadataValue_WithComplexObject_ReturnsCorrectNestedDictionary() + { + // Arrange + var testObject = new TestComplexObject + { + StringValue = "test", + NumberValue = 42, + BoolValue = true, + NestedObject = new Dictionary + { + { "nested", "value" }, + { "number", 123 } + } + }; + + // Act + var result = PowertoolsTracingSerializer.GetMetadataValue(testObject); + + // Assert + Assert.IsType>(result); + Assert.Equal("test", result["StringValue"]); + Assert.Equal(42.0, result["NumberValue"]); + Assert.Equal(true, result["BoolValue"]); + + var nestedObject = Assert.IsType>(result["NestedObject"]); + Assert.Equal("value", nestedObject["nested"]); + Assert.Equal(123.0, nestedObject["number"]); + } + + [Fact] + public void Serialize_WithoutInitializedContext_ThrowsInvalidOperationException() + { + // Arrange + PowertoolsTracingSerializer.AddSerializerContext(null); + var testObject = new TestPerson { Name = "John", Age = 30 }; + + // Act + var exception = Assert.Throws( + () => PowertoolsTracingSerializer.Serialize(testObject)); + + // Assert + Assert.Equal(UninitializedContextMessage, exception.Message); + } + + [Fact] + public void Serialize_WithCircularReference_ThrowsJsonException() + { + // Arrange + var circular = new TestCircularReference { Name = "Test" }; + circular.Reference = circular; // Create circular reference + + // Act & Assert + Assert.Throws(() => PowertoolsTracingSerializer.Serialize(circular)); + } + + [Fact] + public void Serialize_WithUnregisteredType_ThrowsInvalidOperationException() + { + // Arrange + var unregisteredType = new UnregisteredClass { Value = "test" }; + + // Act & Assert + Assert.Throws(() => PowertoolsTracingSerializer.Serialize(unregisteredType)); + } + + [Fact] + public void GetMetadataValue_WithoutInitializedContext_ThrowsInvalidOperationException() + { + // Arrange + PowertoolsTracingSerializer.AddSerializerContext(null); + var testObject = new TestPerson { Name = "John", Age = 30 }; + + // Act + var exception = Assert.Throws( + () => PowertoolsTracingSerializer.GetMetadataValue(testObject)); + + // Assert + Assert.Equal(UninitializedContextMessage, exception.Message); + } + + [Fact] + public void GetMetadataValue_WithCircularReference_ThrowsJsonException() + { + // Arrange + var circular = new TestCircularReference { Name = "Test" }; + circular.Reference = circular; // Create circular reference + + // Act & Assert + Assert.Throws(() => PowertoolsTracingSerializer.GetMetadataValue(circular)); + } + + [Fact] + public void GetMetadataValue_WithUnregisteredType_ThrowsInvalidOperationException() + { + // Arrange + var unregisteredType = new UnregisteredClass { Value = "test" }; + + // Act & Assert + Assert.Throws(() => PowertoolsTracingSerializer.GetMetadataValue(unregisteredType)); + } + + [Fact] + public void GetMetadataValue_WithInvalidJson_ThrowsJsonException() + { + // This test requires modifying the internal state of the serializer + // which might not be possible in the actual implementation + // Including it to show the concept of testing invalid JSON handling + // Arrange + var invalidJsonObject = new JsonTestObject { Value = Double.NaN }; // NaN is not valid in JSON + + // Act & Assert + Assert.Throws(() => PowertoolsTracingSerializer.GetMetadataValue(invalidJsonObject)); + } +} + +public class TestCircularReference +{ + public string Name { get; set; } + public TestCircularReference Reference { get; set; } +} + +// Helper classes for testing +public class UnregisteredClass +{ + public string Value { get; set; } +} + +[JsonSerializable(typeof(JsonTestObject))] +public class JsonTestObject +{ + public double Value { get; set; } +} +#endif \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/Serializers/TestJsonContext.cs b/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/Serializers/TestJsonContext.cs new file mode 100644 index 00000000..11af5331 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/Serializers/TestJsonContext.cs @@ -0,0 +1,28 @@ + +#if NET8_0_OR_GREATER + +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace AWS.Lambda.Powertools.Tracing.Tests.Serializers; + +[JsonSourceGenerationOptions(WriteIndented = true)] +[JsonSerializable(typeof(TestPerson))] +[JsonSerializable(typeof(TestComplexObject))] +public partial class TestJsonContext : JsonSerializerContext { } + +public class TestPerson +{ + public string Name { get; set; } + public int Age { get; set; } +} + +public class TestComplexObject +{ + public string StringValue { get; set; } + public int NumberValue { get; set; } + public bool BoolValue { get; set; } + public Dictionary NestedObject { get; set; } +} + +#endif \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/Serializers/TracingSerializerExtensionsTests.cs b/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/Serializers/TracingSerializerExtensionsTests.cs new file mode 100644 index 00000000..72fe92b4 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/Serializers/TracingSerializerExtensionsTests.cs @@ -0,0 +1,44 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + + +#if NET8_0_OR_GREATER +using Amazon.Lambda.Serialization.SystemTextJson; +using AWS.Lambda.Powertools.Tracing.Serializers; +using Xunit; + +namespace AWS.Lambda.Powertools.Tracing.Tests.Serializers; + +public class TracingSerializerExtensionsTests +{ + [Fact] + public void WithTracing_InitializesSerializer_Successfully() + { + // Arrange + var serializer = new SourceGeneratorLambdaJsonSerializer(); + + // Act + var result = serializer.WithTracing(); + + // Assert + Assert.NotNull(result); + + // Verify the context was initialized by attempting to serialize + var testObject = new TestPerson { Name = "Test", Age = 25 }; + var serialized = PowertoolsTracingSerializer.Serialize(testObject); + Assert.Contains("\"Name\":\"Test\"", serialized); + } +} +#endif \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/TracingAspectTests.cs b/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/TracingAspectTests.cs new file mode 100644 index 00000000..a81aaed8 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/TracingAspectTests.cs @@ -0,0 +1,178 @@ +using System; +using System.Threading.Tasks; +using AWS.Lambda.Powertools.Common; +using AWS.Lambda.Powertools.Tracing.Internal; +using NSubstitute; +using Xunit; + +namespace AWS.Lambda.Powertools.Tracing.Tests; + +[Collection("Sequential")] +public class TracingAspectTests +{ + private readonly IXRayRecorder _mockXRayRecorder; + private readonly IPowertoolsConfigurations _mockConfigurations; + private readonly TracingAspect _handler; + + public TracingAspectTests() + { + // Setup mocks + _mockXRayRecorder = Substitute.For(); + _mockConfigurations = Substitute.For(); + + // Configure default behavior + _mockConfigurations.IsLambdaEnvironment.Returns(true); + _mockConfigurations.TracingDisabled.Returns(false); + _mockConfigurations.Service.Returns("TestService"); + _mockConfigurations.IsServiceDefined.Returns(true); + _mockConfigurations.TracerCaptureResponse.Returns(true); + _mockConfigurations.TracerCaptureError.Returns(true); + + // Setup test handler with mocks + _handler = new TracingAspect(_mockConfigurations, _mockXRayRecorder); + + // Reset static state + TracingAspect.ResetForTest(); + } + + [Fact] + public void Around_SyncMethod_HandlesResponseAndSegmentCorrectly() + { + // Arrange + var attribute = new TracingAttribute(); + var methodName = "TestMethod"; + var result = "test result"; + Func target = _ => result; + + // Act + _handler.Around(null, methodName, Array.Empty(), + GetType(), GetType().GetMethod(nameof(Around_SyncMethod_HandlesResponseAndSegmentCorrectly)), + typeof(string), target, new Attribute[] { attribute }); + + // Assert + _mockXRayRecorder.Received(1).BeginSubsegment($"## {methodName}"); + _mockXRayRecorder.Received(1).AddAnnotation("ColdStart", true); + _mockXRayRecorder.Received(1).AddAnnotation("Service", "TestService"); + _mockXRayRecorder.Received(1).AddMetadata( + Arg.Any(), + $"{methodName} response", + result); + _mockXRayRecorder.Received(1).EndSubsegment(); + } + + [Fact] + public async Task Around_AsyncMethod_HandlesResponseAndSegmentCorrectly() + { + // Arrange + var attribute = new TracingAttribute(); + var methodName = "TestAsyncMethod"; + var result = new TestResponse { Message = "test async result" }; + Func target = _ => Task.FromResult(result); + + // Act + var taskResult = _handler.Around(null, methodName, Array.Empty(), + GetType(), GetType().GetMethod(nameof(Around_AsyncMethod_HandlesResponseAndSegmentCorrectly)), + typeof(Task), target, new Attribute[] { attribute }); + + // Wait for the async operation to complete + if (taskResult is Task task) + { + await task; + } + + // Assert with wait + await Task.Delay(100); // Give time for the continuation to complete + + _mockXRayRecorder.Received(1).BeginSubsegment($"## {methodName}"); + _mockXRayRecorder.Received(1).AddAnnotation("ColdStart", true); + _mockXRayRecorder.Received(1).AddAnnotation("Service", "TestService"); + _mockXRayRecorder.Received(1).AddMetadata( + Arg.Any(), + $"{methodName} response", + result); + _mockXRayRecorder.Received(1).EndSubsegment(); + } + + [Fact] + public async Task Around_VoidAsyncMethod_HandlesSegmentCorrectly() + { + // Arrange + var attribute = new TracingAttribute(); + var methodName = "TestVoidAsyncMethod"; + Func target = _ => Task.CompletedTask; + + // Act + var taskResult = _handler.Around(null, methodName, Array.Empty(), + GetType(), GetType().GetMethod(nameof(Around_VoidAsyncMethod_HandlesSegmentCorrectly)), + typeof(Task), target, new Attribute[] { attribute }); + + // Wait for the async operation to complete + if (taskResult is Task task) + { + await task; + } + + // Assert with wait + await Task.Delay(100); // Give time for the continuation to complete + + _mockXRayRecorder.Received(1).BeginSubsegment($"## {methodName}"); + _mockXRayRecorder.Received(1).AddAnnotation("ColdStart", true); + _mockXRayRecorder.Received(1).AddAnnotation("Service", "TestService"); + _mockXRayRecorder.Received(1).EndSubsegment(); + } + + [Fact] + public async Task Around_AsyncMethodWithException_HandlesErrorCorrectly() + { + // Arrange + var attribute = new TracingAttribute(); + var methodName = "TestExceptionAsyncMethod"; + var expectedException = new Exception("Test exception"); + Func target = _ => Task.FromException(expectedException); + + // Act & Assert + var taskResult = _handler.Around(null, methodName, Array.Empty(), + GetType(), GetType().GetMethod(nameof(Around_AsyncMethodWithException_HandlesErrorCorrectly)), + typeof(Task), target, new Attribute[] { attribute }); + + // Wait for the async operation to complete + if (taskResult is Task task) + { + await Assert.ThrowsAsync(() => task); + } + + // Assert with wait + await Task.Delay(100); // Give time for the continuation to complete + + _mockXRayRecorder.Received(1).BeginSubsegment($"## {methodName}"); + _mockXRayRecorder.Received(1).AddMetadata( + Arg.Any(), + $"{methodName} error", + Arg.Is(s => s.Contains(expectedException.Message))); + _mockXRayRecorder.Received(1).EndSubsegment(); + } + + [Fact] + public void Around_TracingDisabled_DoesNotCreateSegment() + { + // Arrange + _mockConfigurations.TracingDisabled.Returns(true); + var attribute = new TracingAttribute(); + var methodName = "TestMethod"; + Func target = _ => "result"; + + // Act + _handler.Around(null, methodName, Array.Empty(), + GetType(), GetType().GetMethod(nameof(Around_TracingDisabled_DoesNotCreateSegment)), + typeof(string), target, new Attribute[] { attribute }); + + // Assert + _mockXRayRecorder.DidNotReceive().BeginSubsegment(Arg.Any()); + _mockXRayRecorder.DidNotReceive().EndSubsegment(); + } + + private class TestResponse + { + public string Message { get; set; } + } +} \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/TracingAttributeTest.cs b/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/TracingAttributeTest.cs index d9ff9a54..58252d67 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/TracingAttributeTest.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/TracingAttributeTest.cs @@ -115,7 +115,7 @@ public void Dispose() Environment.SetEnvironmentVariable("POWERTOOLS_TRACER_CAPTURE_RESPONSE", ""); Environment.SetEnvironmentVariable("POWERTOOLS_TRACER_CAPTURE_ERROR", ""); Environment.SetEnvironmentVariable("POWERTOOLS_TRACE_DISABLED", ""); - TracingAspectHandler.ResetForTest(); + TracingAspect.ResetForTest(); } } @@ -171,7 +171,7 @@ private static void ClearEnvironment() Environment.SetEnvironmentVariable("POWERTOOLS_TRACER_CAPTURE_RESPONSE", ""); Environment.SetEnvironmentVariable("POWERTOOLS_TRACER_CAPTURE_ERROR", ""); Environment.SetEnvironmentVariable("POWERTOOLS_TRACE_DISABLED", ""); - TracingAspectHandler.ResetForTest(); + TracingAspect.ResetForTest(); } } @@ -298,6 +298,32 @@ public void OnEntry_WhenNamespaceHasValue_SetNamespaceWithValue() #region OnSuccess Tests + + [Fact] + public void OnSuccess_When_NotSet_Defaults_CapturesResponse() + { + // Arrange + Environment.SetEnvironmentVariable("LAMBDA_TASK_ROOT", "AWS"); + Environment.SetEnvironmentVariable("POWERTOOLS_SERVICE_NAME", "POWERTOOLS"); + + // Act + var segment = AWSXRayRecorder.Instance.TraceContext.GetEntity(); + _handler.Handle(); + var subSegment = segment.Subsegments[0]; + + // Assert + Assert.True(segment.IsSubsegmentsAdded); + Assert.Single(segment.Subsegments); + Assert.True(subSegment.IsMetadataAdded); + Assert.True(subSegment.Metadata.ContainsKey("POWERTOOLS")); + + var metadata = subSegment.Metadata["POWERTOOLS"]; + Assert.Equal("Handle response", metadata.Keys.Cast().First()); + var handlerResponse = metadata.Values.Cast().First(); + Assert.Equal("A", handlerResponse[0]); + Assert.Equal("B", handlerResponse[1]); + } + [Fact] public void OnSuccess_WhenTracerCaptureResponseEnvironmentVariableIsTrue_CapturesResponse() { @@ -757,7 +783,7 @@ public void Dispose() Environment.SetEnvironmentVariable("POWERTOOLS_TRACER_CAPTURE_RESPONSE", ""); Environment.SetEnvironmentVariable("POWERTOOLS_TRACER_CAPTURE_ERROR", ""); Environment.SetEnvironmentVariable("POWERTOOLS_TRACE_DISABLED", ""); - TracingAspectHandler.ResetForTest(); + TracingAspect.ResetForTest(); } } } \ No newline at end of file From 129f14ea7fa7ebad350d6f1d932e568d8ec29798 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Thu, 31 Oct 2024 09:45:12 +0000 Subject: [PATCH 02/17] remove unused --- .../AWS.Lambda.Powertools.Tracing.Tests/Handlers/Handlers.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/Handlers/Handlers.cs b/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/Handlers/Handlers.cs index 9965680d..385ce96c 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/Handlers/Handlers.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/Handlers/Handlers.cs @@ -14,7 +14,6 @@ */ using System; -using System.Threading.Tasks; namespace AWS.Lambda.Powertools.Tracing.Tests; From 071c2243ab132bd61e346611aafc9af693d64e42 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Thu, 31 Oct 2024 13:07:11 +0000 Subject: [PATCH 03/17] sonar fixes --- .../Internal/TracingAspect.cs | 10 +--------- .../Serializers/PowertoolsTracingSerializerTests.cs | 2 +- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/libraries/src/AWS.Lambda.Powertools.Tracing/Internal/TracingAspect.cs b/libraries/src/AWS.Lambda.Powertools.Tracing/Internal/TracingAspect.cs index ca0e6df8..d146dd46 100644 --- a/libraries/src/AWS.Lambda.Powertools.Tracing/Internal/TracingAspect.cs +++ b/libraries/src/AWS.Lambda.Powertools.Tracing/Internal/TracingAspect.cs @@ -66,27 +66,19 @@ public TracingAspect(IPowertoolsConfigurations powertoolsConfigurations, IXRayRe _powertoolsConfigurations = powertoolsConfigurations; _xRayRecorder = xRayRecorder; } - + /// /// Surrounds the specific method with Tracing aspect /// - /// /// /// - /// - /// - /// /// /// /// [Advice(Kind.Around)] public object Around( - [Argument(Source.Instance)] object instance, [Argument(Source.Name)] string name, [Argument(Source.Arguments)] object[] args, - [Argument(Source.Type)] Type hostType, - [Argument(Source.Metadata)] MethodBase method, - [Argument(Source.ReturnType)] Type returnType, [Argument(Source.Target)] Func target, [Argument(Source.Triggers)] Attribute[] triggers) { diff --git a/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/Serializers/PowertoolsTracingSerializerTests.cs b/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/Serializers/PowertoolsTracingSerializerTests.cs index da9bad05..98122dc1 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/Serializers/PowertoolsTracingSerializerTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/Serializers/PowertoolsTracingSerializerTests.cs @@ -85,7 +85,7 @@ public void GetMetadataValue_WithComplexObject_ReturnsCorrectNestedDictionary() Assert.IsType>(result); Assert.Equal("test", result["StringValue"]); Assert.Equal(42.0, result["NumberValue"]); - Assert.Equal(true, result["BoolValue"]); + Assert.True((bool)result["BoolValue"]); var nestedObject = Assert.IsType>(result["NestedObject"]); Assert.Equal("value", nestedObject["nested"]); From 823dede16e00bbebb519ee0bc2aca44845de5a8a Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Thu, 31 Oct 2024 14:34:59 +0000 Subject: [PATCH 04/17] fix tests --- .../TracingAspectTests.cs | 21 ++++++------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/TracingAspectTests.cs b/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/TracingAspectTests.cs index a81aaed8..e3760226 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/TracingAspectTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/TracingAspectTests.cs @@ -1,6 +1,7 @@ using System; using System.Threading.Tasks; using AWS.Lambda.Powertools.Common; +using AWS.Lambda.Powertools.Common.Utils; using AWS.Lambda.Powertools.Tracing.Internal; using NSubstitute; using Xunit; @@ -45,9 +46,7 @@ public void Around_SyncMethod_HandlesResponseAndSegmentCorrectly() Func target = _ => result; // Act - _handler.Around(null, methodName, Array.Empty(), - GetType(), GetType().GetMethod(nameof(Around_SyncMethod_HandlesResponseAndSegmentCorrectly)), - typeof(string), target, new Attribute[] { attribute }); + _handler.Around(methodName, Array.Empty(), target, new Attribute[] { attribute }); // Assert _mockXRayRecorder.Received(1).BeginSubsegment($"## {methodName}"); @@ -70,9 +69,7 @@ public async Task Around_AsyncMethod_HandlesResponseAndSegmentCorrectly() Func target = _ => Task.FromResult(result); // Act - var taskResult = _handler.Around(null, methodName, Array.Empty(), - GetType(), GetType().GetMethod(nameof(Around_AsyncMethod_HandlesResponseAndSegmentCorrectly)), - typeof(Task), target, new Attribute[] { attribute }); + var taskResult = _handler.Around(methodName, Array.Empty(), target, new Attribute[] { attribute }); // Wait for the async operation to complete if (taskResult is Task task) @@ -102,9 +99,7 @@ public async Task Around_VoidAsyncMethod_HandlesSegmentCorrectly() Func target = _ => Task.CompletedTask; // Act - var taskResult = _handler.Around(null, methodName, Array.Empty(), - GetType(), GetType().GetMethod(nameof(Around_VoidAsyncMethod_HandlesSegmentCorrectly)), - typeof(Task), target, new Attribute[] { attribute }); + var taskResult = _handler.Around(methodName, Array.Empty(), target, new Attribute[] { attribute }); // Wait for the async operation to complete if (taskResult is Task task) @@ -131,9 +126,7 @@ public async Task Around_AsyncMethodWithException_HandlesErrorCorrectly() Func target = _ => Task.FromException(expectedException); // Act & Assert - var taskResult = _handler.Around(null, methodName, Array.Empty(), - GetType(), GetType().GetMethod(nameof(Around_AsyncMethodWithException_HandlesErrorCorrectly)), - typeof(Task), target, new Attribute[] { attribute }); + var taskResult = _handler.Around(methodName, Array.Empty(), target, new Attribute[] { attribute }); // Wait for the async operation to complete if (taskResult is Task task) @@ -162,9 +155,7 @@ public void Around_TracingDisabled_DoesNotCreateSegment() Func target = _ => "result"; // Act - _handler.Around(null, methodName, Array.Empty(), - GetType(), GetType().GetMethod(nameof(Around_TracingDisabled_DoesNotCreateSegment)), - typeof(string), target, new Attribute[] { attribute }); + _handler.Around(methodName, Array.Empty(), target, new Attribute[] { attribute }); // Assert _mockXRayRecorder.DidNotReceive().BeginSubsegment(Arg.Any()); From 2cfe474cea8f91fc8708b0d39b054cd4c90d9620 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Thu, 31 Oct 2024 14:51:29 +0000 Subject: [PATCH 05/17] more test paths --- .../Serializers/TestJsonContext.cs | 8 +- .../TracingAspectTests.cs | 85 +++++++++++++++++-- 2 files changed, 84 insertions(+), 9 deletions(-) diff --git a/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/Serializers/TestJsonContext.cs b/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/Serializers/TestJsonContext.cs index 11af5331..333c13f7 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/Serializers/TestJsonContext.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/Serializers/TestJsonContext.cs @@ -9,6 +9,7 @@ namespace AWS.Lambda.Powertools.Tracing.Tests.Serializers; [JsonSourceGenerationOptions(WriteIndented = true)] [JsonSerializable(typeof(TestPerson))] [JsonSerializable(typeof(TestComplexObject))] +[JsonSerializable(typeof(TestResponse))] public partial class TestJsonContext : JsonSerializerContext { } public class TestPerson @@ -25,4 +26,9 @@ public class TestComplexObject public Dictionary NestedObject { get; set; } } -#endif \ No newline at end of file +#endif + +public class TestResponse +{ + public string Message { get; set; } +} \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/TracingAspectTests.cs b/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/TracingAspectTests.cs index e3760226..535eb771 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/TracingAspectTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/TracingAspectTests.cs @@ -1,4 +1,5 @@ using System; +using System.Text.Json; using System.Threading.Tasks; using AWS.Lambda.Powertools.Common; using AWS.Lambda.Powertools.Common.Utils; @@ -6,6 +7,11 @@ using NSubstitute; using Xunit; +#if NET8_0_OR_GREATER +using AWS.Lambda.Powertools.Tracing.Serializers; +using AWS.Lambda.Powertools.Tracing.Tests.Serializers; +#endif + namespace AWS.Lambda.Powertools.Tracing.Tests; [Collection("Sequential")] @@ -20,7 +26,7 @@ public TracingAspectTests() // Setup mocks _mockXRayRecorder = Substitute.For(); _mockConfigurations = Substitute.For(); - + // Configure default behavior _mockConfigurations.IsLambdaEnvironment.Returns(true); _mockConfigurations.TracingDisabled.Returns(false); @@ -31,8 +37,9 @@ public TracingAspectTests() // Setup test handler with mocks _handler = new TracingAspect(_mockConfigurations, _mockXRayRecorder); - + // Reset static state + RuntimeFeatureWrapper.SetIsDynamicCodeSupported(true); TracingAspect.ResetForTest(); } @@ -79,7 +86,7 @@ public async Task Around_AsyncMethod_HandlesResponseAndSegmentCorrectly() // Assert with wait await Task.Delay(100); // Give time for the continuation to complete - + _mockXRayRecorder.Received(1).BeginSubsegment($"## {methodName}"); _mockXRayRecorder.Received(1).AddAnnotation("ColdStart", true); _mockXRayRecorder.Received(1).AddAnnotation("Service", "TestService"); @@ -90,6 +97,73 @@ public async Task Around_AsyncMethod_HandlesResponseAndSegmentCorrectly() _mockXRayRecorder.Received(1).EndSubsegment(); } +#if NET8_0_OR_GREATER + + [Fact] + public void Around_SyncMethod_HandlesResponseAndSegmentCorrectly_AOT() + { + // Arrange + RuntimeFeatureWrapper.SetIsDynamicCodeSupported(false); + + var context = new TestJsonContext(new JsonSerializerOptions()); + PowertoolsTracingSerializer.AddSerializerContext(context); + + var attribute = new TracingAttribute(); + var methodName = "TestMethod"; + var result = "test result"; + Func target = _ => result; + + // Act + _handler.Around(methodName, Array.Empty(), target, new Attribute[] { attribute }); + + // Assert + _mockXRayRecorder.Received(1).BeginSubsegment($"## {methodName}"); + _mockXRayRecorder.Received(1).AddAnnotation("ColdStart", true); + _mockXRayRecorder.Received(1).AddAnnotation("Service", "TestService"); + _mockXRayRecorder.Received(1).AddMetadata( + Arg.Any(), + $"{methodName} response", + PowertoolsTracingSerializer.Serialize(result)); + _mockXRayRecorder.Received(1).EndSubsegment(); + } + + [Fact] + public async Task Around_AsyncMethod_HandlesResponseAndSegmentCorrectly_AOT() + { + // Arrange + RuntimeFeatureWrapper.SetIsDynamicCodeSupported(false); + + var context = new TestJsonContext(new JsonSerializerOptions()); + PowertoolsTracingSerializer.AddSerializerContext(context); + + var attribute = new TracingAttribute(); + var methodName = "TestAsyncMethod"; + var result = new TestResponse { Message = "test async result" }; + Func target = _ => Task.FromResult(result); + + // Act + var taskResult = _handler.Around(methodName, Array.Empty(), target, new Attribute[] { attribute }); + + // Wait for the async operation to complete + if (taskResult is Task task) + { + await task; + } + + // Assert with wait + await Task.Delay(100); // Give time for the continuation to complete + + _mockXRayRecorder.Received(1).BeginSubsegment($"## {methodName}"); + _mockXRayRecorder.Received(1).AddAnnotation("ColdStart", true); + _mockXRayRecorder.Received(1).AddAnnotation("Service", "TestService"); + _mockXRayRecorder.Received(1).AddMetadata( + Arg.Any(), + $"{methodName} response", + PowertoolsTracingSerializer.Serialize(result)); + _mockXRayRecorder.Received(1).EndSubsegment(); + } +#endif + [Fact] public async Task Around_VoidAsyncMethod_HandlesSegmentCorrectly() { @@ -161,9 +235,4 @@ public void Around_TracingDisabled_DoesNotCreateSegment() _mockXRayRecorder.DidNotReceive().BeginSubsegment(Arg.Any()); _mockXRayRecorder.DidNotReceive().EndSubsegment(); } - - private class TestResponse - { - public string Message { get; set; } - } } \ No newline at end of file From 90dfaeea24dbdee1368e37a2cb93e7dbe2ced7be Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Thu, 31 Oct 2024 15:20:30 +0000 Subject: [PATCH 06/17] remove unreachable path --- .../Serializers/TracingSerializerExtensions.cs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/libraries/src/AWS.Lambda.Powertools.Tracing/Serializers/TracingSerializerExtensions.cs b/libraries/src/AWS.Lambda.Powertools.Tracing/Serializers/TracingSerializerExtensions.cs index c1035292..5aee45b2 100644 --- a/libraries/src/AWS.Lambda.Powertools.Tracing/Serializers/TracingSerializerExtensions.cs +++ b/libraries/src/AWS.Lambda.Powertools.Tracing/Serializers/TracingSerializerExtensions.cs @@ -48,15 +48,8 @@ internal static JsonSerializerOptions GetDefaultOptions() where T : JsonSerializerContext { var options = DefaultOptionsHelper.GetDefaultOptions(); - var constructor = typeof(T).GetConstructor(new Type[] { typeof(JsonSerializerOptions) }); - if (constructor == null) - { - throw new JsonSerializerException( - $"The serializer {typeof(T).FullName} is missing a constructor that takes in JsonSerializerOptions object"); - } - - var jsonSerializerContext = constructor.Invoke(new object[] { options }) as T; + var jsonSerializerContext = constructor!.Invoke(new object[] { options }) as T; PowertoolsTracingSerializer.AddSerializerContext(jsonSerializerContext); return serializer; From b650e584fba8245e099d7292c6e551c3089e2e94 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Thu, 31 Oct 2024 16:00:05 +0000 Subject: [PATCH 07/17] more test coverage --- .../Internal/TracingAspect.cs | 1 - .../PowertoolsTracingSerializerTests.cs | 59 +++++- .../Serializers/TestJsonContext.cs | 3 + .../TracingAspectTests.cs | 168 +++++++++++++++++- 4 files changed, 226 insertions(+), 5 deletions(-) diff --git a/libraries/src/AWS.Lambda.Powertools.Tracing/Internal/TracingAspect.cs b/libraries/src/AWS.Lambda.Powertools.Tracing/Internal/TracingAspect.cs index d146dd46..4ed76b24 100644 --- a/libraries/src/AWS.Lambda.Powertools.Tracing/Internal/TracingAspect.cs +++ b/libraries/src/AWS.Lambda.Powertools.Tracing/Internal/TracingAspect.cs @@ -15,7 +15,6 @@ using System; using System.Linq; -using System.Reflection; using System.Text; using System.Threading.Tasks; using AspectInjector.Broker; diff --git a/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/Serializers/PowertoolsTracingSerializerTests.cs b/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/Serializers/PowertoolsTracingSerializerTests.cs index 98122dc1..988cbab5 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/Serializers/PowertoolsTracingSerializerTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/Serializers/PowertoolsTracingSerializerTests.cs @@ -176,6 +176,48 @@ public void GetMetadataValue_WithInvalidJson_ThrowsJsonException() // Act & Assert Assert.Throws(() => PowertoolsTracingSerializer.GetMetadataValue(invalidJsonObject)); } + + [Fact] + public void GetMetadataValue_WithFalseValue_ReturnsFalse() + { + // Arrange + var testObject = new TestBooleanObject { Value = false }; + + // Act + var result = PowertoolsTracingSerializer.GetMetadataValue(testObject); + + // Assert + Assert.IsType>(result); + Assert.False((bool)result["Value"]); + } + + [Fact] + public void GetMetadataValue_WithNullValue_ReturnsNull() + { + // Arrange + var testObject = new TestNullableObject { Value = null }; + + // Act + var result = PowertoolsTracingSerializer.GetMetadataValue(testObject); + + // Assert + Assert.IsType>(result); + Assert.Null(result["Value"]); + } + + [Fact] + public void GetMetadataValue_WithArrayValue_ReturnsNull() + { + // Arrange + var testObject = new TestArrayObject { Values = new[] { 1, 2, 3 } }; + + // Act + var result = PowertoolsTracingSerializer.GetMetadataValue(testObject); + + // Assert + Assert.IsType>(result); + Assert.Null(result["Values"]); // Arrays fall into the default case + } } public class TestCircularReference @@ -190,9 +232,24 @@ public class UnregisteredClass public string Value { get; set; } } -[JsonSerializable(typeof(JsonTestObject))] +[JsonSerializable(typeof(JsonTestObject))] public class JsonTestObject { public double Value { get; set; } } + +public class TestBooleanObject +{ + public bool Value { get; set; } +} + +public class TestNullableObject +{ + public string? Value { get; set; } +} + +public class TestArrayObject +{ + public int[] Values { get; set; } +} #endif \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/Serializers/TestJsonContext.cs b/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/Serializers/TestJsonContext.cs index 333c13f7..8b5d4b06 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/Serializers/TestJsonContext.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/Serializers/TestJsonContext.cs @@ -10,6 +10,9 @@ namespace AWS.Lambda.Powertools.Tracing.Tests.Serializers; [JsonSerializable(typeof(TestPerson))] [JsonSerializable(typeof(TestComplexObject))] [JsonSerializable(typeof(TestResponse))] +[JsonSerializable(typeof(TestBooleanObject))] +[JsonSerializable(typeof(TestNullableObject))] +[JsonSerializable(typeof(TestArrayObject))] public partial class TestJsonContext : JsonSerializerContext { } public class TestPerson diff --git a/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/TracingAspectTests.cs b/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/TracingAspectTests.cs index 535eb771..48d19cd4 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/TracingAspectTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/TracingAspectTests.cs @@ -1,5 +1,6 @@ using System; using System.Text.Json; +using System.Threading; using System.Threading.Tasks; using AWS.Lambda.Powertools.Common; using AWS.Lambda.Powertools.Common.Utils; @@ -98,7 +99,6 @@ public async Task Around_AsyncMethod_HandlesResponseAndSegmentCorrectly() } #if NET8_0_OR_GREATER - [Fact] public void Around_SyncMethod_HandlesResponseAndSegmentCorrectly_AOT() { @@ -107,7 +107,7 @@ public void Around_SyncMethod_HandlesResponseAndSegmentCorrectly_AOT() var context = new TestJsonContext(new JsonSerializerOptions()); PowertoolsTracingSerializer.AddSerializerContext(context); - + var attribute = new TracingAttribute(); var methodName = "TestMethod"; var result = "test result"; @@ -126,7 +126,7 @@ public void Around_SyncMethod_HandlesResponseAndSegmentCorrectly_AOT() PowertoolsTracingSerializer.Serialize(result)); _mockXRayRecorder.Received(1).EndSubsegment(); } - + [Fact] public async Task Around_AsyncMethod_HandlesResponseAndSegmentCorrectly_AOT() { @@ -235,4 +235,166 @@ public void Around_TracingDisabled_DoesNotCreateSegment() _mockXRayRecorder.DidNotReceive().BeginSubsegment(Arg.Any()); _mockXRayRecorder.DidNotReceive().EndSubsegment(); } + + [Fact] + public void TracingDisabled_WhenTracingDisabledTrue_ReturnsTrue() + { + // Arrange + _mockConfigurations.TracingDisabled.Returns(true); + + // Act + var result = _handler.Around( + "TestMethod", + Array.Empty(), + _ => null, + new Attribute[] { new TracingAttribute() } + ); + + // Assert + Assert.Null(result); + _mockXRayRecorder.DidNotReceive().BeginSubsegment(Arg.Any()); + } + + [Fact] + public void TracingDisabled_WhenNotInLambdaEnvironment_ReturnsTrue() + { + // Arrange + _mockConfigurations.IsLambdaEnvironment.Returns(false); + + // Act + var result = _handler.Around( + "TestMethod", + Array.Empty(), + _ => null, + new Attribute[] { new TracingAttribute() } + ); + + // Assert + Assert.Null(result); + _mockXRayRecorder.DidNotReceive().BeginSubsegment(Arg.Any()); + } + + [Fact] + public async Task WrapVoidTask_SuccessfulExecution_HandlesResponseAndEndsSubsegment() + { + // Arrange + var tcs = new TaskCompletionSource(); + var task = tcs.Task; + const string methodName = "TestMethod"; + const string nameSpace = "TestNamespace"; + + // Act + var wrappedTask = _handler.Around( + methodName, + new object[] { task }, + args => args[0], + new Attribute[] + { new TracingAttribute { Namespace = nameSpace, CaptureMode = TracingCaptureMode.Response } } + ) as Task; + + tcs.SetResult(); // Complete the task + await wrappedTask!; + + // Assert + _mockXRayRecorder.Received(1).AddMetadata( + Arg.Is(nameSpace), + Arg.Is($"{methodName} response"), + Arg.Any() + ); + _mockXRayRecorder.Received(1).EndSubsegment(); + } + + [Fact] + public async Task WrapVoidTask_WithException_HandlesExceptionAndEndsSubsegment() + { + // Arrange + var tcs = new TaskCompletionSource(); + var task = tcs.Task; + const string methodName = "TestMethod"; + const string nameSpace = "TestNamespace"; + var expectedException = new Exception("Test exception"); + + // Act + var wrappedTask = _handler.Around( + methodName, + new object[] { task }, + args => args[0], + new Attribute[] + { new TracingAttribute { Namespace = nameSpace, CaptureMode = TracingCaptureMode.ResponseAndError } } + ) as Task; + + tcs.SetException(expectedException); // Fail the task + + // Assert + await Assert.ThrowsAsync(() => wrappedTask!); + + _mockXRayRecorder.Received(1).AddMetadata( + Arg.Is(nameSpace), + Arg.Is($"{methodName} error"), + Arg.Is(s => s.Contains("Test exception")) + ); + _mockXRayRecorder.Received(1).EndSubsegment(); + } + + [Fact] + public async Task WrapVoidTask_WithCancellation_EndsSubsegment() + { + // Arrange + TracingAspect.ResetForTest(); // Ensure static state is reset + + // Reinitialize mocks for this specific test + var mockXRayRecorder = Substitute.For(); + var mockConfigurations = Substitute.For(); + + // Configure all required behavior + mockConfigurations.IsLambdaEnvironment.Returns(true); + mockConfigurations.TracingDisabled.Returns(false); + mockConfigurations.Service.Returns("TestService"); + mockConfigurations.IsServiceDefined.Returns(true); + mockConfigurations.TracerCaptureResponse.Returns(true); + mockConfigurations.TracerCaptureError.Returns(true); + + var handler = new TracingAspect(mockConfigurations, mockXRayRecorder); + + var cts = new CancellationTokenSource(); + var tcs = new TaskCompletionSource(); + var task = Task.Run(async () => + { + await tcs.Task; + await Task.Delay(Timeout.Infinite, cts.Token); + }); + + const string methodName = "TestMethod"; + const string nameSpace = "TestNamespace"; + + // Act + var wrappedTask = handler.Around( + methodName, + new object[] { task }, + args => args[0], + new Attribute[] + { new TracingAttribute { Namespace = nameSpace, CaptureMode = TracingCaptureMode.ResponseAndError } } + ) as Task; + + // Ensure the task is running before we cancel + tcs.SetResult(); + await Task.Delay(100); // Give time for the task to start running + + // Cancel the task + cts.Cancel(); + + // Assert + await Assert.ThrowsAsync(() => wrappedTask!); + + // Add small delay before verification to ensure all async operations complete + await Task.Delay(50); + + mockXRayRecorder.Received(1).EndSubsegment(); + + // Verify other expected calls + mockXRayRecorder.Received(1).BeginSubsegment(Arg.Any()); + mockXRayRecorder.Received(1).SetNamespace(nameSpace); + mockXRayRecorder.Received(1).AddAnnotation("ColdStart", true); + mockXRayRecorder.Received(1).AddAnnotation("Service", "TestService"); + } } \ No newline at end of file From 5f101a322abf73326bf951b0893ccc1186526b65 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Thu, 31 Oct 2024 18:26:27 +0000 Subject: [PATCH 08/17] update documentation --- docs/core/tracing.md | 101 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 87 insertions(+), 14 deletions(-) diff --git a/docs/core/tracing.md b/docs/core/tracing.md index 7f27fcb8..6e16afde 100644 --- a/docs/core/tracing.md +++ b/docs/core/tracing.md @@ -244,7 +244,7 @@ under a subsegment, or you are doing multithreaded programming. Refer examples b } ``` -## Instrumenting SDK clients and HTTP calls +## Instrumenting SDK clients You should make sure to instrument the SDK clients explicitly based on the function dependency. You can instrument all of your AWS SDK for .NET clients by calling RegisterForAllServices before you create them. @@ -277,25 +277,98 @@ To instrument clients for some services and not others, call Register instead of Tracing.Register() ``` -This functionality is a thin wrapper for AWS X-Ray .NET SDK. Refer details on [how to instrument SDK client with Xray](https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-dotnet-sdkclients.html) and [outgoing http calls](https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-dotnet-httpclients.html). +This functionality is a thin wrapper for AWS X-Ray .NET SDK. Refer details on [how to instrument SDK client with Xray](https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-dotnet-sdkclients.html) + +## Instrumenting outgoing HTTP calls + +=== "Function.cs" + + ```c# hl_lines="7" + using Amazon.XRay.Recorder.Handlers.System.Net; + + public class Function + { + public Function() + { + var httpClient = new HttpClient(new HttpClientXRayTracingHandler(new HttpClientHandler())); + var myIp = await httpClient.GetStringAsync("https://checkip.amazonaws.com/"); + } + } + ``` + +More information about instrumenting [outgoing http calls](https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-dotnet-httpclients.html). ## AOT Support Native AOT trims your application code as part of the compilation to ensure that the binary is as small as possible. .NET 8 for Lambda provides improved trimming support compared to previous versions of .NET. -These improvements offer the potential to eliminate build-time trimming warnings, but .NET will never be completely trim safe. This means that parts of libraries that your function relies on may be trimmed out as part of the compilation step. You can manage this by defining TrimmerRootAssemblies as part of your `.csproj` file as shown in the following example. -For the Tracing utility to work correctly and without trim warnings please add the following to your `.csproj` file +### WithTracing() -```xml - - - - - - -``` +To use Tracing utility with AOT support you first need to add `WithTracing()` to the source generator you are using either the default `SourceGeneratorLambdaJsonSerializer` +or the Powertools Logging utility [source generator](logging.md#aot-support){:target="_blank"} `PowertoolsSourceGeneratorSerializer`. + +Examples: + +=== "Without Powertools Logging" + + ```c# hl_lines="8" + using AWS.Lambda.Powertools.Tracing; + using AWS.Lambda.Powertools.Tracing.Serializers; + + private static async Task Main() + { + Func handler = FunctionHandler; + await LambdaBootstrapBuilder.Create(handler, new SourceGeneratorLambdaJsonSerializer() + .WithTracing()) + .Build() + .RunAsync(); + } + ``` + +=== "With Powertools Logging" + + ```c# hl_lines="10 11" + using AWS.Lambda.Powertools.Logging; + using AWS.Lambda.Powertools.Logging.Serializers; + using AWS.Lambda.Powertools.Tracing; + using AWS.Lambda.Powertools.Tracing.Serializers; + + private static async Task Main() + { + Func handler = FunctionHandler; + await LambdaBootstrapBuilder.Create(handler, + new PowertoolsSourceGeneratorSerializer() + .WithTracing()) + .Build() + .RunAsync(); + } + ``` + +### Publishing + +!!! warning "Publishing" + Make sure you are publishing your code with `--self-contained true` and that you have `partial` in your `.csproj` file + +### Trimming + +!!! warning "Trim warnings" + ```xml + + + + + + + + + ``` + + Note that when you receive a trim warning, adding the class that generates the warning to TrimmerRootAssembly might not resolve the issue. A trim warning indicates that the class is trying to access some other class that can't be determined until runtime. To avoid runtime errors, add this second class to TrimmerRootAssembly. + + To learn more about managing trim warnings, see [Introduction to trim warnings](https://learn.microsoft.com/en-us/dotnet/core/deploying/trimming/fixing-warnings) in the Microsoft .NET documentation. -Note that when you receive a trim warning, adding the class that generates the warning to TrimmerRootAssembly might not resolve the issue. A trim warning indicates that the class is trying to access some other class that can't be determined until runtime. To avoid runtime errors, add this second class to TrimmerRootAssembly. +### Not supported -To learn more about managing trim warnings, see [Introduction to trim warnings](https://learn.microsoft.com/en-us/dotnet/core/deploying/trimming/fixing-warnings) in the Microsoft .NET documentation. +!!! warning "Not supported" + Currently instrumenting SDK clients with `Tracing.RegisterForAllServices()` is not supported on AOT mode. \ No newline at end of file From 5317fc234f14bab32c1f87480dcf9ec9a7fffe98 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Mon, 4 Nov 2024 09:55:36 +0000 Subject: [PATCH 09/17] more test coverage --- .../Serializers/TestJsonContext.cs | 14 ++ .../TracingAspectTests.cs | 126 ++++++++++++++++++ .../XRayRecorderTests.cs | 15 +++ 3 files changed, 155 insertions(+) diff --git a/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/Serializers/TestJsonContext.cs b/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/Serializers/TestJsonContext.cs index 8b5d4b06..b1a8226d 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/Serializers/TestJsonContext.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/Serializers/TestJsonContext.cs @@ -1,3 +1,17 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ #if NET8_0_OR_GREATER diff --git a/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/TracingAspectTests.cs b/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/TracingAspectTests.cs index 48d19cd4..6d52ae9f 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/TracingAspectTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/TracingAspectTests.cs @@ -1,3 +1,18 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + using System; using System.Text.Json; using System.Threading; @@ -397,4 +412,115 @@ public async Task WrapVoidTask_WithCancellation_EndsSubsegment() mockXRayRecorder.Received(1).AddAnnotation("ColdStart", true); mockXRayRecorder.Received(1).AddAnnotation("Service", "TestService"); } + + [Fact] + public void CaptureResponse_WhenTracingDisabled_ReturnsFalse() + { + // Arrange + _mockConfigurations.TracingDisabled.Returns(true); + var attribute = new TracingAttribute { CaptureMode = TracingCaptureMode.Response }; + var methodName = "TestMethod"; + Func target = _ => "result"; + + // Act + var result = _handler.Around(methodName, Array.Empty(), target, new Attribute[] { attribute }); + + // Assert + _mockXRayRecorder.DidNotReceive().AddMetadata( + Arg.Any(), + Arg.Any(), + Arg.Any()); + } + + [Theory] + [InlineData(TracingCaptureMode.EnvironmentVariable, true)] + [InlineData(TracingCaptureMode.Response, true)] + [InlineData(TracingCaptureMode.ResponseAndError, true)] + [InlineData(TracingCaptureMode.Error, false)] + [InlineData(TracingCaptureMode.Disabled, false)] + public void CaptureResponse_WithDifferentModes_ReturnsExpectedResult(TracingCaptureMode mode, bool expectedCapture) + { + // Arrange + _mockConfigurations.TracerCaptureResponse.Returns(true); + var attribute = new TracingAttribute { CaptureMode = mode }; + var methodName = "TestMethod"; + var result = "test result"; + Func target = _ => result; + + // Act + _handler.Around(methodName, Array.Empty(), target, new Attribute[] { attribute }); + + // Assert + if (expectedCapture) + { + _mockXRayRecorder.Received(1).AddMetadata( + Arg.Any(), + $"{methodName} response", + result); + } + else + { + _mockXRayRecorder.DidNotReceive().AddMetadata( + Arg.Any(), + $"{methodName} response", + Arg.Any()); + } + } + + [Fact] + public void CaptureError_WhenTracingDisabled_ReturnsFalse() + { + // Arrange + _mockConfigurations.TracingDisabled.Returns(true); + var attribute = new TracingAttribute { CaptureMode = TracingCaptureMode.Error }; + var methodName = "TestMethod"; + var expectedException = new Exception("Test exception"); + Func target = _ => throw expectedException; + + // Act & Assert + Assert.Throws(() => + _handler.Around(methodName, Array.Empty(), target, new Attribute[] { attribute })); + + // Verify no error metadata was added + _mockXRayRecorder.DidNotReceive().AddMetadata( + Arg.Any(), + Arg.Any(), + Arg.Any()); + } + + [Theory] + [InlineData(TracingCaptureMode.EnvironmentVariable, true)] + [InlineData(TracingCaptureMode.Error, true)] + [InlineData(TracingCaptureMode.ResponseAndError, true)] + [InlineData(TracingCaptureMode.Response, false)] + [InlineData(TracingCaptureMode.Disabled, false)] + public void CaptureError_WithDifferentModes_ReturnsExpectedResult(TracingCaptureMode mode, bool expectedCapture) + { + // Arrange + _mockConfigurations.TracerCaptureError.Returns(true); + var attribute = new TracingAttribute { CaptureMode = mode }; + var methodName = "TestMethod"; + var expectedException = new Exception("Test exception"); + Func target = _ => throw expectedException; + + // Act & Assert + Assert.Throws(() => + _handler.Around(methodName, Array.Empty(), target, new Attribute[] { attribute })); + + // Verify error metadata was added or not based on expected capture + if (expectedCapture) + { + _mockXRayRecorder.Received(1).AddMetadata( + Arg.Any(), + $"{methodName} error", + Arg.Is(s => s.Contains(expectedException.Message))); + } + else + { + _mockXRayRecorder.DidNotReceive().AddMetadata( + Arg.Any(), + $"{methodName} error", + Arg.Any()); + } + } } \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/XRayRecorderTests.cs b/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/XRayRecorderTests.cs index 038703d4..6a024334 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/XRayRecorderTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/XRayRecorderTests.cs @@ -1,3 +1,18 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + using System; using Amazon.XRay.Recorder.Core; using Amazon.XRay.Recorder.Core.Internal.Entities; From 233198d89209e868a967e623b33ac305453d3f66 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Mon, 4 Nov 2024 10:07:21 +0000 Subject: [PATCH 10/17] more coverage for disabled path --- .../TracingAspectTests.cs | 44 ++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/TracingAspectTests.cs b/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/TracingAspectTests.cs index 6d52ae9f..a93111cc 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/TracingAspectTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/TracingAspectTests.cs @@ -423,7 +423,7 @@ public void CaptureResponse_WhenTracingDisabled_ReturnsFalse() Func target = _ => "result"; // Act - var result = _handler.Around(methodName, Array.Empty(), target, new Attribute[] { attribute }); + _handler.Around(methodName, Array.Empty(), target, new Attribute[] { attribute }); // Assert _mockXRayRecorder.DidNotReceive().AddMetadata( @@ -523,4 +523,46 @@ public void CaptureError_WithDifferentModes_ReturnsExpectedResult(TracingCapture Arg.Any()); } } + + [Fact] + public void CaptureResponse_WhenTracingIsDisabled_ReturnsFalseDirectly() + { + // Arrange + _mockConfigurations.TracingDisabled.Returns(true); + var methodName = "TestMethod"; + var result = "test result"; + Func target = _ => result; + + // Act + _handler.Around(methodName, Array.Empty(), target, new Attribute[] { new TracingAttribute() }); + + // Assert - No metadata should be added since we return false immediately + _mockXRayRecorder.DidNotReceive().AddMetadata( + Arg.Any(), + Arg.Any(), + Arg.Any()); + } + + [Fact] + public void CaptureError_WhenTracingIsDisabled_ReturnsFalseDirectly() + { + // Arrange + _mockConfigurations.TracingDisabled.Returns(true); + var methodName = "TestMethod"; + var expectedException = new Exception("Test exception"); + Func target = _ => throw expectedException; + + // Act & Assert + Assert.Throws(() => _handler.Around( + methodName, + Array.Empty(), + target, + new Attribute[] { new TracingAttribute() })); + + // Verify no error metadata was added since we return false immediately + _mockXRayRecorder.DidNotReceive().AddMetadata( + Arg.Any(), + Arg.Any(), + Arg.Any()); + } } \ No newline at end of file From ccc3268a4e5e37426c2eb7ddb6c5dd4170aa5fca Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Mon, 4 Nov 2024 10:16:10 +0000 Subject: [PATCH 11/17] remove unreachable code --- .../AWS.Lambda.Powertools.Tracing/Internal/TracingAspect.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/libraries/src/AWS.Lambda.Powertools.Tracing/Internal/TracingAspect.cs b/libraries/src/AWS.Lambda.Powertools.Tracing/Internal/TracingAspect.cs index 4ed76b24..58b4ba6e 100644 --- a/libraries/src/AWS.Lambda.Powertools.Tracing/Internal/TracingAspect.cs +++ b/libraries/src/AWS.Lambda.Powertools.Tracing/Internal/TracingAspect.cs @@ -238,9 +238,6 @@ private bool TracingDisabled() private bool CaptureResponse(TracingCaptureMode captureMode) { - if (TracingDisabled()) - return false; - return captureMode switch { TracingCaptureMode.EnvironmentVariable => _powertoolsConfigurations.TracerCaptureResponse, @@ -252,9 +249,6 @@ private bool CaptureResponse(TracingCaptureMode captureMode) private bool CaptureError(TracingCaptureMode captureMode) { - if (TracingDisabled()) - return false; - return captureMode switch { TracingCaptureMode.EnvironmentVariable => _powertoolsConfigurations.TracerCaptureError, From 11ff616df2dd74ea0c44272c8c7a4371a572bb5d Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Mon, 4 Nov 2024 10:25:56 +0000 Subject: [PATCH 12/17] more coverage to cover the Result property retrieval from different Task types. --- .../TracingAspectTests.cs | 68 +++++++++++-------- 1 file changed, 41 insertions(+), 27 deletions(-) diff --git a/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/TracingAspectTests.cs b/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/TracingAspectTests.cs index a93111cc..b9b98821 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/TracingAspectTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/TracingAspectTests.cs @@ -525,44 +525,58 @@ public void CaptureError_WithDifferentModes_ReturnsExpectedResult(TracingCapture } [Fact] - public void CaptureResponse_WhenTracingIsDisabled_ReturnsFalseDirectly() + public async Task Around_AsyncMethodWithResult_HandlesTaskResultProperty() { // Arrange - _mockConfigurations.TracingDisabled.Returns(true); - var methodName = "TestMethod"; - var result = "test result"; - Func target = _ => result; + var attribute = new TracingAttribute(); + var methodName = "TestAsyncMethod"; + var expectedResult = "test result"; + var taskWithResult = Task.FromResult(expectedResult); + Func target = _ => taskWithResult; // Act - _handler.Around(methodName, Array.Empty(), target, new Attribute[] { new TracingAttribute() }); + var taskResult = _handler.Around(methodName, Array.Empty(), target, new Attribute[] { attribute }); - // Assert - No metadata should be added since we return false immediately - _mockXRayRecorder.DidNotReceive().AddMetadata( - Arg.Any(), - Arg.Any(), - Arg.Any()); + // Wait for the async operation to complete + if (taskResult is Task task) + { + await task; + } + + // Assert with wait + await Task.Delay(100); // Give time for the continuation to complete + + _mockXRayRecorder.Received(1).AddMetadata( + "TestService", // This matches what's set in the test constructor + $"{methodName} response", + expectedResult); } [Fact] - public void CaptureError_WhenTracingIsDisabled_ReturnsFalseDirectly() + public async Task Around_AsyncMethodWithoutResult_HandlesNullTaskResultProperty() { // Arrange - _mockConfigurations.TracingDisabled.Returns(true); - var methodName = "TestMethod"; - var expectedException = new Exception("Test exception"); - Func target = _ => throw expectedException; + var attribute = new TracingAttribute(); + var methodName = "TestAsyncMethod"; + var taskWithoutResult = new Task(() => { }); // Task without Result property + taskWithoutResult.Start(); + Func target = _ => taskWithoutResult; - // Act & Assert - Assert.Throws(() => _handler.Around( - methodName, - Array.Empty(), - target, - new Attribute[] { new TracingAttribute() })); + // Act + var taskResult = _handler.Around(methodName, Array.Empty(), target, new Attribute[] { attribute }); - // Verify no error metadata was added since we return false immediately - _mockXRayRecorder.DidNotReceive().AddMetadata( - Arg.Any(), - Arg.Any(), - Arg.Any()); + // Wait for the async operation to complete + if (taskResult is Task task) + { + await task; + } + + // Assert with wait + await Task.Delay(100); // Give time for the continuation to complete + + _mockXRayRecorder.Received(1).AddMetadata( + "TestService", + $"{methodName} response", + null); } } \ No newline at end of file From e19143e211dc398b74601c9df8c3e492ec902046 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Mon, 4 Nov 2024 10:34:30 +0000 Subject: [PATCH 13/17] update version for release --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index 6ea697d7..87474c84 100644 --- a/version.json +++ b/version.json @@ -2,7 +2,7 @@ "Core": { "Logging": "1.6.2", "Metrics": "1.8.0", - "Tracing": "1.5.2" + "Tracing": "1.5.3" }, "Utilities": { "Parameters": "1.3.0", From 764727ebc3055891445e5d918fa6ed8658d110a3 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Mon, 4 Nov 2024 10:36:04 +0000 Subject: [PATCH 14/17] bump version --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index 87474c84..f62b0aba 100644 --- a/version.json +++ b/version.json @@ -2,7 +2,7 @@ "Core": { "Logging": "1.6.2", "Metrics": "1.8.0", - "Tracing": "1.5.3" + "Tracing": "1.6.0" }, "Utilities": { "Parameters": "1.3.0", From 27bba28d78c38de16088a507433df169728312c2 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Tue, 5 Nov 2024 19:21:24 +0000 Subject: [PATCH 15/17] fix async segment not output correctly, add tests --- .../Internal/TracingAspect.cs | 139 ++++++------------ .../Handlers/FullExampleHandler.cs | 98 +++++++++++- .../Handlers/HandlerTests.cs | 128 ++++++++++++++++ .../TracingAspectTests.cs | 122 ++++++++------- 4 files changed, 327 insertions(+), 160 deletions(-) diff --git a/libraries/src/AWS.Lambda.Powertools.Tracing/Internal/TracingAspect.cs b/libraries/src/AWS.Lambda.Powertools.Tracing/Internal/TracingAspect.cs index 58b4ba6e..11be6cc2 100644 --- a/libraries/src/AWS.Lambda.Powertools.Tracing/Internal/TracingAspect.cs +++ b/libraries/src/AWS.Lambda.Powertools.Tracing/Internal/TracingAspect.cs @@ -86,12 +86,14 @@ public object Around( if (TracingDisabled()) return target(args); - var @namespace = !string.IsNullOrWhiteSpace(trigger.Namespace) ? trigger.Namespace : _powertoolsConfigurations.Service; - - var (segmentName, metadataName) = string.IsNullOrWhiteSpace(trigger.SegmentName) - ? ($"## {name}", name) + var @namespace = !string.IsNullOrWhiteSpace(trigger.Namespace) + ? trigger.Namespace + : _powertoolsConfigurations.Service; + + var (segmentName, metadataName) = string.IsNullOrWhiteSpace(trigger.SegmentName) + ? ($"## {name}", name) : (trigger.SegmentName, trigger.SegmentName); - + BeginSegment(segmentName, @namespace); try @@ -100,68 +102,29 @@ public object Around( if (result is Task task) { - return WrapTask(task, metadataName, trigger.CaptureMode, @namespace); - } - - HandleResponse(metadataName, result, trigger.CaptureMode, @namespace); - _xRayRecorder.EndSubsegment(); // End segment here for sync methods - return result; - } - catch (Exception ex) - { - HandleException(ex, metadataName, trigger.CaptureMode, @namespace); - _xRayRecorder.EndSubsegment(); // End segment here for sync methods with exceptions - throw; - } - } - - private object WrapTask(Task task, string name, TracingCaptureMode captureMode, string @namespace) - { - if (task.GetType() == typeof(Task)) - { - return WrapVoidTask(task, name, captureMode, @namespace); - } + task.GetAwaiter().GetResult(); + var taskResult = task.GetType().GetProperty("Result")?.GetValue(task); + HandleResponse(metadataName, taskResult, trigger.CaptureMode, @namespace); - // Create an async wrapper task that returns the original task type - async Task AsyncWrapper() - { - try - { - await task; - var result = task.GetType().GetProperty("Result")?.GetValue(task); - HandleResponse(name, result, captureMode, @namespace); - } - catch (Exception ex) - { - HandleException(ex, name, captureMode, @namespace); - throw; - } - finally - { _xRayRecorder.EndSubsegment(); + return task; } - } - // Start the wrapper and return original task - _ = AsyncWrapper(); - return task; - } + HandleResponse(metadataName, result, trigger.CaptureMode, @namespace); - private async Task WrapVoidTask(Task task, string name, TracingCaptureMode captureMode, string @namespace) - { - try - { - await task; - HandleResponse(name, null, captureMode, @namespace); + _xRayRecorder.EndSubsegment(); + return result; } catch (Exception ex) { - HandleException(ex, name, captureMode, @namespace); + HandleException(ex, metadataName, trigger.CaptureMode, @namespace); + _xRayRecorder.EndSubsegment(); throw; } finally { - _xRayRecorder.EndSubsegment(); + if (_isAnnotationsCaptured) + _captureAnnotations = true; } } @@ -184,15 +147,16 @@ private void BeginSegment(string segmentName, string @namespace) _isColdStart = false; } - private void HandleResponse(string segmentName, object result, TracingCaptureMode captureMode, string @namespace) + private void HandleResponse(string name, object result, TracingCaptureMode captureMode, string @namespace) { if (!CaptureResponse(captureMode)) return; + #if NET8_0_OR_GREATER if (!RuntimeFeatureWrapper.IsDynamicCodeSupported) // is AOT { _xRayRecorder.AddMetadata( @namespace, - $"{segmentName} response", + $"{name} response", Serializers.PowertoolsTracingSerializer.Serialize(result) ); return; @@ -201,22 +165,34 @@ private void HandleResponse(string segmentName, object result, TracingCaptureMod _xRayRecorder.AddMetadata( @namespace, - $"{segmentName} response", + $"{name} response", result ); } - /// - /// When Aspect Handler exits runs this code to end subsegment - /// - [Advice(Kind.After)] - public void OnExit() + private void HandleException(Exception exception, string name, TracingCaptureMode captureMode, string @namespace) { - if (TracingDisabled()) - return; + if (!CaptureError(captureMode)) return; + + var sb = new StringBuilder(); + sb.AppendLine($"Exception type: {exception.GetType()}"); + sb.AppendLine($"Exception message: {exception.Message}"); + sb.AppendLine($"Stack trace: {exception.StackTrace}"); + + if (exception.InnerException != null) + { + sb.AppendLine("---BEGIN InnerException--- "); + sb.AppendLine($"Exception type {exception.InnerException.GetType()}"); + sb.AppendLine($"Exception message: {exception.InnerException.Message}"); + sb.AppendLine($"Stack trace: {exception.InnerException.StackTrace}"); + sb.AppendLine("---END Inner Exception"); + } - if (_isAnnotationsCaptured) - _captureAnnotations = true; + _xRayRecorder.AddMetadata( + @namespace, + $"{name} error", + sb.ToString() + ); } private bool TracingDisabled() @@ -258,35 +234,6 @@ private bool CaptureError(TracingCaptureMode captureMode) }; } - private void HandleException(Exception exception, string name, TracingCaptureMode captureMode, string @namespace) - { - if (!CaptureError(captureMode)) return; - - var nameSpace = @namespace; - var sb = new StringBuilder(); - sb.AppendLine($"Exception type: {exception.GetType()}"); - sb.AppendLine($"Exception message: {exception.Message}"); - sb.AppendLine($"Stack trace: {exception.StackTrace}"); - - if (exception.InnerException != null) - { - sb.AppendLine("---BEGIN InnerException--- "); - sb.AppendLine($"Exception type {exception.InnerException.GetType()}"); - sb.AppendLine($"Exception message: {exception.InnerException.Message}"); - sb.AppendLine($"Stack trace: {exception.InnerException.StackTrace}"); - sb.AppendLine("---END Inner Exception"); - } - - _xRayRecorder.AddMetadata( - nameSpace, - $"{name} error", - sb.ToString() - ); - } - - /// - /// Resets static variables for test. - /// internal static void ResetForTest() { _isColdStart = true; diff --git a/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/Handlers/FullExampleHandler.cs b/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/Handlers/FullExampleHandler.cs index 68808be3..4f84cc97 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/Handlers/FullExampleHandler.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/Handlers/FullExampleHandler.cs @@ -13,6 +13,8 @@ * permissions and limitations under the License. */ +using System.Collections.Generic; +using System.Net.Http; using System.Threading.Tasks; using Amazon.Lambda.Core; @@ -25,10 +27,24 @@ public async Task Handle(string text, ILambdaContext context) { Tracing.AddAnnotation("annotation", "value"); await BusinessLogic1(); + + var response = DoAction(text); - return await Task.FromResult(text.ToUpper()); + return await Task.FromResult(response); + } + + [Tracing(SegmentName = "Do Action")] + private string DoAction(string text) + { + return ToUpper(text); + } + + [Tracing(SegmentName = "To Upper")] + private string ToUpper(string text) + { + return text.ToUpper(); } - + [Tracing(SegmentName = "First Call")] private async Task BusinessLogic1() { @@ -51,4 +67,82 @@ private void GetSomething() { Tracing.AddAnnotation("getsomething", "value"); } +} + +public class FullExampleHandler2 +{ + [Tracing] + public static async Task FunctionHandler(string request, ILambdaContext context) + { + string myIp = await GetIp(); + + var response = CallDynamo(myIp); + + return "hello"; + } + + [Tracing(SegmentName = "Call DynamoDB")] + private static List CallDynamo(string myIp) + { + var newList = ToUpper(myIp, new List() { "Hello", "World" }); + return newList; + } + + [Tracing(SegmentName = "To Upper")] + private static List ToUpper(string myIp, List response) + { + var newList = new List(); + foreach (var item in response) + { + newList.Add(item.ToUpper()); + } + + newList.Add(myIp); + return newList; + } + + [Tracing(SegmentName = "Get Ip Address")] + private static async Task GetIp() + { + return await Task.FromResult("127.0.0.1"); + } +} + +public class FullExampleHandler3 +{ + [Tracing] + public static async Task FunctionHandler(string request, ILambdaContext context) + { + string myIp = GetIp(); + + var response = CallDynamo(myIp); + + return "hello"; + } + + [Tracing(SegmentName = "Call DynamoDB")] + private static List CallDynamo(string myIp) + { + var newList = ToUpper(myIp, new List() { "Hello", "World" }); + return newList; + } + + [Tracing(SegmentName = "To Upper")] + private static List ToUpper(string myIp, List response) + { + var newList = new List(); + foreach (var item in response) + { + newList.Add(item.ToUpper()); + } + + newList.Add(myIp); + return newList; + } + + [Tracing(SegmentName = "Get Ip Address")] + private static string GetIp() + { + return "127.0.0.1"; + } } \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/Handlers/HandlerTests.cs b/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/Handlers/HandlerTests.cs index 40bb3bbd..17e3af3a 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/Handlers/HandlerTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/Handlers/HandlerTests.cs @@ -1,4 +1,20 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Amazon.Lambda.TestUtilities; @@ -101,6 +117,118 @@ public async Task Full_Example() Assert.False(getSomethingSubsegment.IsInProgress); Assert.Equal("value", getSomethingSubsegment.Annotations["getsomething"]); } + + [Fact] + public async Task Full_Example_Async() + { + // Arrange + Environment.SetEnvironmentVariable("LAMBDA_TASK_ROOT", "AWS"); + Environment.SetEnvironmentVariable("POWERTOOLS_SERVICE_NAME", "POWERTOOLS"); + + var context = new TestLambdaContext + { + FunctionName = "FullExampleLambda", + FunctionVersion = "1", + MemoryLimitInMB = 215, + AwsRequestId = Guid.NewGuid().ToString("D") + }; + + // Act + var facadeSegment = AWSXRayRecorder.Instance.TraceContext.GetEntity(); + await FullExampleHandler2.FunctionHandler("Hello World", context); + var handleSegment = facadeSegment.Subsegments[0]; + + // Assert + Assert.True(handleSegment.IsAnnotationsAdded); + Assert.True(handleSegment.IsSubsegmentsAdded); + + Assert.Equal("POWERTOOLS", handleSegment.Annotations["Service"]); + Assert.True((bool)handleSegment.Annotations["ColdStart"]); + Assert.Equal("## FunctionHandler", handleSegment.Name); + Assert.Equal(2, handleSegment.Subsegments.Count); + + var firstCallSubsegment = handleSegment.Subsegments[0]; + + Assert.Equal("Get Ip Address", firstCallSubsegment.Name); + Assert.False(firstCallSubsegment.IsInProgress); + var metadata1 = firstCallSubsegment.Metadata["POWERTOOLS"]; + Assert.Contains("Get Ip Address response", metadata1.Keys.Cast()); + Assert.Contains("127.0.0.1", metadata1.Values.Cast()); + + var businessLogicSubsegment = handleSegment.Subsegments[1]; + + Assert.Equal("Call DynamoDB", businessLogicSubsegment.Name); + + Assert.False(businessLogicSubsegment.IsInProgress); + Assert.Single(businessLogicSubsegment.Metadata); + var metadata = businessLogicSubsegment.Metadata["POWERTOOLS"]; + Assert.Contains("Call DynamoDB response", metadata.Keys.Cast()); + Assert.Contains(["HELLO", "WORLD", "127.0.0.1"], metadata.Values.Cast>()); + Assert.True(businessLogicSubsegment.IsSubsegmentsAdded); + + var getSomethingSubsegment = businessLogicSubsegment.Subsegments[0]; + + Assert.Equal("To Upper", getSomethingSubsegment.Name); + + Assert.False(getSomethingSubsegment.IsSubsegmentsAdded); + Assert.False(getSomethingSubsegment.IsInProgress); + } + + [Fact] + public async Task Full_Example_Sync() + { + // Arrange + Environment.SetEnvironmentVariable("LAMBDA_TASK_ROOT", "AWS"); + Environment.SetEnvironmentVariable("POWERTOOLS_SERVICE_NAME", "POWERTOOLS"); + + var context = new TestLambdaContext + { + FunctionName = "FullExampleLambda", + FunctionVersion = "1", + MemoryLimitInMB = 215, + AwsRequestId = Guid.NewGuid().ToString("D") + }; + + // Act + var facadeSegment = AWSXRayRecorder.Instance.TraceContext.GetEntity(); + await FullExampleHandler3.FunctionHandler("Hello World", context); + var handleSegment = facadeSegment.Subsegments[0]; + + // Assert + Assert.True(handleSegment.IsAnnotationsAdded); + Assert.True(handleSegment.IsSubsegmentsAdded); + + Assert.Equal("POWERTOOLS", handleSegment.Annotations["Service"]); + Assert.True((bool)handleSegment.Annotations["ColdStart"]); + Assert.Equal("## FunctionHandler", handleSegment.Name); + Assert.Equal(2, handleSegment.Subsegments.Count); + + var firstCallSubsegment = handleSegment.Subsegments[0]; + + Assert.Equal("Get Ip Address", firstCallSubsegment.Name); + Assert.False(firstCallSubsegment.IsInProgress); + var metadata1 = firstCallSubsegment.Metadata["POWERTOOLS"]; + Assert.Contains("Get Ip Address response", metadata1.Keys.Cast()); + Assert.Contains("127.0.0.1", metadata1.Values.Cast()); + + var businessLogicSubsegment = handleSegment.Subsegments[1]; + + Assert.Equal("Call DynamoDB", businessLogicSubsegment.Name); + + Assert.False(businessLogicSubsegment.IsInProgress); + Assert.Single(businessLogicSubsegment.Metadata); + var metadata = businessLogicSubsegment.Metadata["POWERTOOLS"]; + Assert.Contains("Call DynamoDB response", metadata.Keys.Cast()); + Assert.Contains(["HELLO", "WORLD", "127.0.0.1"], metadata.Values.Cast>()); + Assert.True(businessLogicSubsegment.IsSubsegmentsAdded); + + var getSomethingSubsegment = businessLogicSubsegment.Subsegments[0]; + + Assert.Equal("To Upper", getSomethingSubsegment.Name); + + Assert.False(getSomethingSubsegment.IsSubsegmentsAdded); + Assert.False(getSomethingSubsegment.IsInProgress); + } public void Dispose() { diff --git a/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/TracingAspectTests.cs b/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/TracingAspectTests.cs index b9b98821..c2f04b77 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/TracingAspectTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/TracingAspectTests.cs @@ -212,20 +212,25 @@ public async Task Around_AsyncMethodWithException_HandlesErrorCorrectly() var attribute = new TracingAttribute(); var methodName = "TestExceptionAsyncMethod"; var expectedException = new Exception("Test exception"); - Func target = _ => Task.FromException(expectedException); + + // Create a completed task with exception before passing to Around + var exceptionTask = Task.FromException(expectedException); + Func target = _ => exceptionTask; // Act & Assert - var taskResult = _handler.Around(methodName, Array.Empty(), target, new Attribute[] { attribute }); - - // Wait for the async operation to complete - if (taskResult is Task task) + await Assert.ThrowsAsync(async () => { - await Assert.ThrowsAsync(() => task); - } - - // Assert with wait - await Task.Delay(100); // Give time for the continuation to complete + var wrappedTask = _handler.Around( + methodName, + Array.Empty(), + target, + new Attribute[] { attribute } + ) as Task; + + await wrappedTask!; + }); + // Assert _mockXRayRecorder.Received(1).BeginSubsegment($"## {methodName}"); _mockXRayRecorder.Received(1).AddMetadata( Arg.Any(), @@ -294,20 +299,22 @@ public async Task WrapVoidTask_SuccessfulExecution_HandlesResponseAndEndsSubsegm { // Arrange var tcs = new TaskCompletionSource(); - var task = tcs.Task; const string methodName = "TestMethod"; const string nameSpace = "TestNamespace"; - // Act + // Complete the task FIRST + tcs.SetResult(); + + // Act - now when Around calls GetResult(), the task is already complete var wrappedTask = _handler.Around( methodName, - new object[] { task }, + new object[] { tcs.Task }, args => args[0], new Attribute[] { new TracingAttribute { Namespace = nameSpace, CaptureMode = TracingCaptureMode.Response } } ) as Task; - tcs.SetResult(); // Complete the task + // This should now complete quickly since the underlying task is already done await wrappedTask!; // Assert @@ -324,24 +331,28 @@ public async Task WrapVoidTask_WithException_HandlesExceptionAndEndsSubsegment() { // Arrange var tcs = new TaskCompletionSource(); - var task = tcs.Task; const string methodName = "TestMethod"; const string nameSpace = "TestNamespace"; var expectedException = new Exception("Test exception"); - // Act - var wrappedTask = _handler.Around( - methodName, - new object[] { task }, - args => args[0], - new Attribute[] - { new TracingAttribute { Namespace = nameSpace, CaptureMode = TracingCaptureMode.ResponseAndError } } - ) as Task; - - tcs.SetException(expectedException); // Fail the task + // Complete the task with exception BEFORE passing to Around + tcs.SetException(expectedException); - // Assert - await Assert.ThrowsAsync(() => wrappedTask!); + // Act & Assert + await Assert.ThrowsAsync(async () => + { + var wrappedTask = _handler.Around( + methodName, + new object[] { tcs.Task }, + args => args[0], + new Attribute[] + { + new TracingAttribute { Namespace = nameSpace, CaptureMode = TracingCaptureMode.ResponseAndError } + } + ) as Task; + + await wrappedTask!; + }); _mockXRayRecorder.Received(1).AddMetadata( Arg.Is(nameSpace), @@ -355,13 +366,11 @@ public async Task WrapVoidTask_WithException_HandlesExceptionAndEndsSubsegment() public async Task WrapVoidTask_WithCancellation_EndsSubsegment() { // Arrange - TracingAspect.ResetForTest(); // Ensure static state is reset + TracingAspect.ResetForTest(); - // Reinitialize mocks for this specific test var mockXRayRecorder = Substitute.For(); var mockConfigurations = Substitute.For(); - // Configure all required behavior mockConfigurations.IsLambdaEnvironment.Returns(true); mockConfigurations.TracingDisabled.Returns(false); mockConfigurations.Service.Returns("TestService"); @@ -371,42 +380,31 @@ public async Task WrapVoidTask_WithCancellation_EndsSubsegment() var handler = new TracingAspect(mockConfigurations, mockXRayRecorder); - var cts = new CancellationTokenSource(); - var tcs = new TaskCompletionSource(); - var task = Task.Run(async () => - { - await tcs.Task; - await Task.Delay(Timeout.Infinite, cts.Token); - }); + // Create a cancellation token source and cancel it + using var cts = new CancellationTokenSource(); + cts.Cancel(); // Cancel it first + + // Now create the cancelled task + var task = Task.FromCanceled(cts.Token); const string methodName = "TestMethod"; const string nameSpace = "TestNamespace"; - // Act - var wrappedTask = handler.Around( - methodName, - new object[] { task }, - args => args[0], - new Attribute[] - { new TracingAttribute { Namespace = nameSpace, CaptureMode = TracingCaptureMode.ResponseAndError } } - ) as Task; - - // Ensure the task is running before we cancel - tcs.SetResult(); - await Task.Delay(100); // Give time for the task to start running - - // Cancel the task - cts.Cancel(); - - // Assert - await Assert.ThrowsAsync(() => wrappedTask!); - - // Add small delay before verification to ensure all async operations complete - await Task.Delay(50); + // Act & Assert + await Assert.ThrowsAsync(async () => + { + var wrappedTask = handler.Around( + methodName, + new object[] { task }, + args => args[0], + new Attribute[] + { new TracingAttribute { Namespace = nameSpace, CaptureMode = TracingCaptureMode.ResponseAndError } } + ) as Task; + + await wrappedTask!; + }); mockXRayRecorder.Received(1).EndSubsegment(); - - // Verify other expected calls mockXRayRecorder.Received(1).BeginSubsegment(Arg.Any()); mockXRayRecorder.Received(1).SetNamespace(nameSpace); mockXRayRecorder.Received(1).AddAnnotation("ColdStart", true); @@ -523,7 +521,7 @@ public void CaptureError_WithDifferentModes_ReturnsExpectedResult(TracingCapture Arg.Any()); } } - + [Fact] public async Task Around_AsyncMethodWithResult_HandlesTaskResultProperty() { @@ -547,7 +545,7 @@ public async Task Around_AsyncMethodWithResult_HandlesTaskResultProperty() await Task.Delay(100); // Give time for the continuation to complete _mockXRayRecorder.Received(1).AddMetadata( - "TestService", // This matches what's set in the test constructor + "TestService", // This matches what's set in the test constructor $"{methodName} response", expectedResult); } From 2752711b2f2c8ccbe72643a576734280131dad6b Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Wed, 6 Nov 2024 19:18:51 +0000 Subject: [PATCH 16/17] tackle issue with Task void result --- .../Internal/TracingAspect.cs | 35 ++- .../TracingAspectTests.cs | 215 +++++++++++++++++- 2 files changed, 238 insertions(+), 12 deletions(-) diff --git a/libraries/src/AWS.Lambda.Powertools.Tracing/Internal/TracingAspect.cs b/libraries/src/AWS.Lambda.Powertools.Tracing/Internal/TracingAspect.cs index 11be6cc2..f8635257 100644 --- a/libraries/src/AWS.Lambda.Powertools.Tracing/Internal/TracingAspect.cs +++ b/libraries/src/AWS.Lambda.Powertools.Tracing/Internal/TracingAspect.cs @@ -15,6 +15,7 @@ using System; using System.Linq; +using System.Runtime.ExceptionServices; using System.Text; using System.Threading.Tasks; using AspectInjector.Broker; @@ -102,22 +103,42 @@ public object Around( if (result is Task task) { - task.GetAwaiter().GetResult(); - var taskResult = task.GetType().GetProperty("Result")?.GetValue(task); - HandleResponse(metadataName, taskResult, trigger.CaptureMode, @namespace); + if (task.IsFaulted && task.Exception != null) + { + var actualException = task.Exception.InnerExceptions.Count == 1 + ? task.Exception.InnerExceptions[0] + : task.Exception; + + // Capture and rethrow the original exception preserving the stack trace + ExceptionDispatchInfo.Capture(actualException).Throw(); + } + + // Only handle response if it's not a void Task + if (task.GetType().IsGenericType) + { + var taskType = task.GetType(); + var resultProperty = taskType.GetProperty("Result"); + + // Handle the response only if task completed successfully + if (task.Status == TaskStatus.RanToCompletion) + { + var taskResult = resultProperty?.GetValue(task); + HandleResponse(metadataName, taskResult, trigger.CaptureMode, @namespace); + } + } _xRayRecorder.EndSubsegment(); return task; } HandleResponse(metadataName, result, trigger.CaptureMode, @namespace); - _xRayRecorder.EndSubsegment(); return result; } catch (Exception ex) { - HandleException(ex, metadataName, trigger.CaptureMode, @namespace); + var actualException = ex is AggregateException ae ? ae.InnerException! : ex; + HandleException(actualException, metadataName, trigger.CaptureMode, @namespace); _xRayRecorder.EndSubsegment(); throw; } @@ -150,6 +171,10 @@ private void BeginSegment(string segmentName, string @namespace) private void HandleResponse(string name, object result, TracingCaptureMode captureMode, string @namespace) { if (!CaptureResponse(captureMode)) return; + if (result == null) return; // Don't try to serialize null results + + // Skip if the result is VoidTaskResult + if (result.GetType().Name == "VoidTaskResult") return; #if NET8_0_OR_GREATER if (!RuntimeFeatureWrapper.IsDynamicCodeSupported) // is AOT diff --git a/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/TracingAspectTests.cs b/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/TracingAspectTests.cs index c2f04b77..e638a35d 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/TracingAspectTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/TracingAspectTests.cs @@ -295,7 +295,7 @@ public void TracingDisabled_WhenNotInLambdaEnvironment_ReturnsTrue() } [Fact] - public async Task WrapVoidTask_SuccessfulExecution_HandlesResponseAndEndsSubsegment() + public async Task WrapVoidTask_SuccessfulExecution_OnlyEndsSubsegment() { // Arrange var tcs = new TaskCompletionSource(); @@ -318,9 +318,9 @@ public async Task WrapVoidTask_SuccessfulExecution_HandlesResponseAndEndsSubsegm await wrappedTask!; // Assert - _mockXRayRecorder.Received(1).AddMetadata( - Arg.Is(nameSpace), - Arg.Is($"{methodName} response"), + _mockXRayRecorder.DidNotReceive().AddMetadata( + Arg.Any(), + Arg.Any(), Arg.Any() ); _mockXRayRecorder.Received(1).EndSubsegment(); @@ -572,9 +572,210 @@ public async Task Around_AsyncMethodWithoutResult_HandlesNullTaskResultProperty( // Assert with wait await Task.Delay(100); // Give time for the continuation to complete + _mockXRayRecorder.DidNotReceive().AddMetadata( + Arg.Any(), + Arg.Any(), + Arg.Any() + ); + _mockXRayRecorder.Received(1).EndSubsegment(); + } + + [Fact] + public async Task Around_VoidTask_DoesNotAddResponseMetadata() + { + // Arrange + var tcs = new TaskCompletionSource(); + const string methodName = "VoidTaskMethod"; + const string nameSpace = "TestNamespace"; + + // Complete the task before passing to Around + tcs.SetResult(); + + // Act + var wrappedTask = _handler.Around( + methodName, + new object[] { tcs.Task }, + args => args[0], + new Attribute[] + { new TracingAttribute { Namespace = nameSpace, CaptureMode = TracingCaptureMode.Response } } + ) as Task; + + await wrappedTask!; + + // Assert + _mockXRayRecorder.Received(1).BeginSubsegment($"## {methodName}"); + _mockXRayRecorder.Received(1).EndSubsegment(); + // Verify that AddMetadata was NOT called with response + _mockXRayRecorder.DidNotReceive().AddMetadata( + Arg.Any(), + Arg.Is(s => s.EndsWith("response")), + Arg.Any() + ); + } + + [Fact] + public async Task Around_VoidTask_HandlesExceptionCorrectly() + { + // Arrange + var tcs = new TaskCompletionSource(); + const string methodName = "VoidTaskMethod"; + const string nameSpace = "TestNamespace"; + var expectedException = new Exception("Test exception"); + + // Fail the task before passing to Around + tcs.SetException(expectedException); + + // Act & Assert + await Assert.ThrowsAsync(async () => + { + var wrappedTask = _handler.Around( + methodName, + new object[] { tcs.Task }, + args => args[0], + new Attribute[] + { new TracingAttribute { Namespace = nameSpace, CaptureMode = TracingCaptureMode.ResponseAndError } } + ) as Task; + + await wrappedTask!; + }); + + // Assert + _mockXRayRecorder.Received(1).BeginSubsegment($"## {methodName}"); _mockXRayRecorder.Received(1).AddMetadata( - "TestService", - $"{methodName} response", - null); + Arg.Is(nameSpace), + Arg.Is($"{methodName} error"), + Arg.Is(s => s.Contains(expectedException.Message)) + ); + _mockXRayRecorder.Received(1).EndSubsegment(); + } + + [Fact] + public async Task Around_VoidTask_WithCancellation_EndsSegmentCorrectly() + { + // Arrange + using var cts = new CancellationTokenSource(); + var tcs = new TaskCompletionSource(); + const string methodName = "VoidTaskMethod"; + const string nameSpace = "TestNamespace"; + + // Cancel before passing to Around + cts.Cancel(); + tcs.SetCanceled(cts.Token); + + // Act & Assert + await Assert.ThrowsAsync(async () => + { + var wrappedTask = _handler.Around( + methodName, + new object[] { tcs.Task }, + args => args[0], + new Attribute[] + { new TracingAttribute { Namespace = nameSpace, CaptureMode = TracingCaptureMode.Response } } + ) as Task; + + await wrappedTask!; + }); + + // Assert + _mockXRayRecorder.Received(1).BeginSubsegment($"## {methodName}"); + _mockXRayRecorder.Received(1).EndSubsegment(); + } + + [Fact] + public async Task Around_TaskWithResult_AddsResponseMetadata() + { + // Arrange + var tcs = new TaskCompletionSource(); + const string methodName = "TaskWithResultMethod"; + const string nameSpace = "TestNamespace"; + const string result = "test result"; + + // Complete the task before passing to Around + tcs.SetResult(result); + + // Act + var wrappedTask = _handler.Around( + methodName, + new object[] { tcs.Task }, + args => args[0], + new Attribute[] + { new TracingAttribute { Namespace = nameSpace, CaptureMode = TracingCaptureMode.Response } } + ) as Task; + + await wrappedTask!; + + // Assert + _mockXRayRecorder.Received(1).BeginSubsegment($"## {methodName}"); + _mockXRayRecorder.Received(1).AddMetadata( + Arg.Is(nameSpace), + Arg.Is($"{methodName} response"), + Arg.Is(s => s == result) + ); + _mockXRayRecorder.Received(1).EndSubsegment(); + } + + [Fact] + public async Task Around_NullResult_DoesNotAddResponseMetadata() + { + // Arrange + var tcs = new TaskCompletionSource(); + const string methodName = "NullResultMethod"; + const string nameSpace = "TestNamespace"; + + // Complete the task with null before passing to Around + tcs.SetResult(null!); + + // Act + var wrappedTask = _handler.Around( + methodName, + new object[] { tcs.Task }, + args => args[0], + new Attribute[] + { new TracingAttribute { Namespace = nameSpace, CaptureMode = TracingCaptureMode.Response } } + ) as Task; + + await wrappedTask!; + + // Assert + _mockXRayRecorder.Received(1).BeginSubsegment($"## {methodName}"); + _mockXRayRecorder.Received(1).EndSubsegment(); + // Verify that AddMetadata was NOT called with response + _mockXRayRecorder.DidNotReceive().AddMetadata( + Arg.Any(), + Arg.Is(s => s.EndsWith("response")), + Arg.Any() + ); + } + + [Fact] + public async Task Around_TracingDisabled_DoesNotAddSegments() + { + // Arrange + _mockConfigurations.TracingDisabled.Returns(true); + var tcs = new TaskCompletionSource(); + const string methodName = "DisabledTracingMethod"; + + // Complete the task before passing to Around + tcs.SetResult(); + + // Act + var wrappedTask = _handler.Around( + methodName, + new object[] { tcs.Task }, + args => args[0], + new Attribute[] + { new TracingAttribute() } + ) as Task; + + await wrappedTask!; + + // Assert + _mockXRayRecorder.DidNotReceive().BeginSubsegment(Arg.Any()); + _mockXRayRecorder.DidNotReceive().EndSubsegment(); + _mockXRayRecorder.DidNotReceive().AddMetadata( + Arg.Any(), + Arg.Any(), + Arg.Any() + ); } } \ No newline at end of file From e304c485994bcffc65a695f5dcd70d42ab26bebd Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Wed, 6 Nov 2024 19:44:33 +0000 Subject: [PATCH 17/17] update flow --- .../Internal/TracingAspect.cs | 40 ++++++++----------- 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/libraries/src/AWS.Lambda.Powertools.Tracing/Internal/TracingAspect.cs b/libraries/src/AWS.Lambda.Powertools.Tracing/Internal/TracingAspect.cs index f8635257..2e860ddd 100644 --- a/libraries/src/AWS.Lambda.Powertools.Tracing/Internal/TracingAspect.cs +++ b/libraries/src/AWS.Lambda.Powertools.Tracing/Internal/TracingAspect.cs @@ -103,43 +103,35 @@ public object Around( if (result is Task task) { - if (task.IsFaulted && task.Exception != null) - { - var actualException = task.Exception.InnerExceptions.Count == 1 - ? task.Exception.InnerExceptions[0] - : task.Exception; - - // Capture and rethrow the original exception preserving the stack trace - ExceptionDispatchInfo.Capture(actualException).Throw(); - } - + if (task.IsFaulted) + { + // Force the exception to be thrown + task.Exception?.Handle(ex => false); + } + // Only handle response if it's not a void Task if (task.GetType().IsGenericType) { - var taskType = task.GetType(); - var resultProperty = taskType.GetProperty("Result"); - - // Handle the response only if task completed successfully - if (task.Status == TaskStatus.RanToCompletion) - { - var taskResult = resultProperty?.GetValue(task); - HandleResponse(metadataName, taskResult, trigger.CaptureMode, @namespace); - } + var taskResult = task.GetType().GetProperty("Result")?.GetValue(task); + HandleResponse(metadataName, taskResult, trigger.CaptureMode, @namespace); } - _xRayRecorder.EndSubsegment(); return task; } HandleResponse(metadataName, result, trigger.CaptureMode, @namespace); + _xRayRecorder.EndSubsegment(); return result; } catch (Exception ex) { - var actualException = ex is AggregateException ae ? ae.InnerException! : ex; - HandleException(actualException, metadataName, trigger.CaptureMode, @namespace); - _xRayRecorder.EndSubsegment(); + var actualException = ex is AggregateException ae ? ae.InnerException! : ex; + HandleException(actualException, metadataName, trigger.CaptureMode, @namespace); + _xRayRecorder.EndSubsegment(); + + // Capture and rethrow the original exception preserving the stack trace + ExceptionDispatchInfo.Capture(actualException).Throw(); throw; } finally @@ -171,7 +163,7 @@ private void BeginSegment(string segmentName, string @namespace) private void HandleResponse(string name, object result, TracingCaptureMode captureMode, string @namespace) { if (!CaptureResponse(captureMode)) return; - if (result == null) return; // Don't try to serialize null results + if (result == null) return; // Don't try to serialize null results // Skip if the result is VoidTaskResult if (result.GetType().Name == "VoidTaskResult") return;