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 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..2e860ddd --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Tracing/Internal/TracingAspect.cs @@ -0,0 +1,259 @@ +/* + * 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.Runtime.ExceptionServices; +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.Name)] string name, + [Argument(Source.Arguments)] object[] args, + [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) + { + 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 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(); + + // Capture and rethrow the original exception preserving the stack trace + ExceptionDispatchInfo.Capture(actualException).Throw(); + throw; + } + finally + { + if (_isAnnotationsCaptured) + _captureAnnotations = true; + } + } + + 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 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 + { + _xRayRecorder.AddMetadata( + @namespace, + $"{name} response", + Serializers.PowertoolsTracingSerializer.Serialize(result) + ); + return; + } +#endif + + _xRayRecorder.AddMetadata( + @namespace, + $"{name} response", + result + ); + } + + private void HandleException(Exception exception, string name, TracingCaptureMode captureMode, string @namespace) + { + 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"); + } + + _xRayRecorder.AddMetadata( + @namespace, + $"{name} error", + sb.ToString() + ); + } + + 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) + { + return captureMode switch + { + TracingCaptureMode.EnvironmentVariable => _powertoolsConfigurations.TracerCaptureResponse, + TracingCaptureMode.Response => true, + TracingCaptureMode.ResponseAndError => true, + _ => false + }; + } + + private bool CaptureError(TracingCaptureMode captureMode) + { + return captureMode switch + { + TracingCaptureMode.EnvironmentVariable => _powertoolsConfigurations.TracerCaptureError, + TracingCaptureMode.Error => true, + TracingCaptureMode.ResponseAndError => true, + _ => false + }; + } + + 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..5aee45b2 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Tracing/Serializers/TracingSerializerExtensions.cs @@ -0,0 +1,59 @@ +/* + * 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) }); + 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/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 80a419b1..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,12 +117,124 @@ 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() { 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/Serializers/PowertoolsTracingSerializerTests.cs b/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/Serializers/PowertoolsTracingSerializerTests.cs new file mode 100644 index 00000000..988cbab5 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/Serializers/PowertoolsTracingSerializerTests.cs @@ -0,0 +1,255 @@ +/* + * 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.True((bool)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)); + } + + [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 +{ + 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; } +} + +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 new file mode 100644 index 00000000..b1a8226d --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/Serializers/TestJsonContext.cs @@ -0,0 +1,51 @@ +/* + * 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.Collections.Generic; +using System.Text.Json.Serialization; + +namespace AWS.Lambda.Powertools.Tracing.Tests.Serializers; + +[JsonSourceGenerationOptions(WriteIndented = true)] +[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 +{ + 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 + +public class TestResponse +{ + public string Message { get; set; } +} \ 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..e638a35d --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/TracingAspectTests.cs @@ -0,0 +1,781 @@ +/* + * 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; +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; + +#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")] +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 + RuntimeFeatureWrapper.SetIsDynamicCodeSupported(true); + 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(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", + 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(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", + result); + _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() + { + // Arrange + var attribute = new TracingAttribute(); + var methodName = "TestVoidAsyncMethod"; + Func target = _ => Task.CompletedTask; + + // 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).EndSubsegment(); + } + + [Fact] + public async Task Around_AsyncMethodWithException_HandlesErrorCorrectly() + { + // Arrange + var attribute = new TracingAttribute(); + var methodName = "TestExceptionAsyncMethod"; + var expectedException = new Exception("Test exception"); + + // Create a completed task with exception before passing to Around + var exceptionTask = Task.FromException(expectedException); + Func target = _ => exceptionTask; + + // Act & Assert + await Assert.ThrowsAsync(async () => + { + 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(), + $"{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(methodName, Array.Empty(), target, new Attribute[] { attribute }); + + // Assert + _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_OnlyEndsSubsegment() + { + // Arrange + var tcs = new TaskCompletionSource(); + const string methodName = "TestMethod"; + const string nameSpace = "TestNamespace"; + + // Complete the task FIRST + tcs.SetResult(); + + // Act - now when Around calls GetResult(), the task is already complete + var wrappedTask = _handler.Around( + methodName, + new object[] { tcs.Task }, + args => args[0], + new Attribute[] + { new TracingAttribute { Namespace = nameSpace, CaptureMode = TracingCaptureMode.Response } } + ) as Task; + + // This should now complete quickly since the underlying task is already done + await wrappedTask!; + + // Assert + _mockXRayRecorder.DidNotReceive().AddMetadata( + Arg.Any(), + Arg.Any(), + Arg.Any() + ); + _mockXRayRecorder.Received(1).EndSubsegment(); + } + + [Fact] + public async Task WrapVoidTask_WithException_HandlesExceptionAndEndsSubsegment() + { + // Arrange + var tcs = new TaskCompletionSource(); + const string methodName = "TestMethod"; + const string nameSpace = "TestNamespace"; + var expectedException = new Exception("Test exception"); + + // Complete the task with exception 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!; + }); + + _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(); + + var mockXRayRecorder = Substitute.For(); + var mockConfigurations = Substitute.For(); + + 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); + + // 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 & 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(); + mockXRayRecorder.Received(1).BeginSubsegment(Arg.Any()); + mockXRayRecorder.Received(1).SetNamespace(nameSpace); + 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 + _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()); + } + } + + [Fact] + public async Task Around_AsyncMethodWithResult_HandlesTaskResultProperty() + { + // Arrange + var attribute = new TracingAttribute(); + var methodName = "TestAsyncMethod"; + var expectedResult = "test result"; + var taskWithResult = Task.FromResult(expectedResult); + Func target = _ => taskWithResult; + + // 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).AddMetadata( + "TestService", // This matches what's set in the test constructor + $"{methodName} response", + expectedResult); + } + + [Fact] + public async Task Around_AsyncMethodWithoutResult_HandlesNullTaskResultProperty() + { + // Arrange + var attribute = new TracingAttribute(); + var methodName = "TestAsyncMethod"; + var taskWithoutResult = new Task(() => { }); // Task without Result property + taskWithoutResult.Start(); + Func target = _ => taskWithoutResult; + + // 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.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( + 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 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 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; diff --git a/version.json b/version.json index 6ea697d7..f62b0aba 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.6.0" }, "Utilities": { "Parameters": "1.3.0",