diff --git a/docs/core/tracing.md b/docs/core/tracing.md index 9815b49a..aee48ffb 100644 --- a/docs/core/tracing.md +++ b/docs/core/tracing.md @@ -16,6 +16,7 @@ a provides functionality to reduce the overhead of performing common tracing tas * Better experience when developing with multiple threads. * Auto-patch supported modules by AWS X-Ray * Auto-disable when not running in AWS Lambda environment +* Ahead-of-Time compilation to native code support [AOT](https://docs.aws.amazon.com/lambda/latest/dg/dotnet-native-aot.html) from version 1.5.0 ## Installation @@ -278,3 +279,23 @@ 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). +## 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 + +```xaml + + + + + + +``` + +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. \ 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/TracingAspect.cs similarity index 54% rename from libraries/src/AWS.Lambda.Powertools.Tracing/Internal/TracingAspectHandler.cs rename to libraries/src/AWS.Lambda.Powertools.Tracing/Internal/TracingAspect.cs index e445ce8f..655e2229 100644 --- a/libraries/src/AWS.Lambda.Powertools.Tracing/Internal/TracingAspectHandler.cs +++ b/libraries/src/AWS.Lambda.Powertools.Tracing/Internal/TracingAspect.cs @@ -1,12 +1,12 @@ /* * 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 @@ -14,152 +14,161 @@ */ using System; +using System.Linq; +using System.Reflection; using System.Runtime.ExceptionServices; using System.Text; +using AspectInjector.Broker; using AWS.Lambda.Powertools.Common; namespace AWS.Lambda.Powertools.Tracing.Internal; /// -/// Class TracingAspectHandler. -/// Implements the +/// This aspect will automatically trace all function handlers. +/// Scope.Global is singleton /// -/// -internal class TracingAspectHandler : IMethodAspectHandler +[Aspect(Scope.Global)] +public class TracingAspect { /// - /// If true, then is cold start + /// The Powertools for AWS Lambda (.NET) configurations /// - private static bool _isColdStart = true; + private readonly IPowertoolsConfigurations _powertoolsConfigurations; /// - /// If true, capture annotations + /// X-Ray Recorder /// - private static bool _captureAnnotations = true; + private readonly IXRayRecorder _xRayRecorder; /// - /// If true, tracing is disabled - /// - private static bool? _isTracingDisabled; - - /// - /// The capture mode - /// - private readonly TracingCaptureMode _captureMode; - - /// - /// Tracing namespace + /// If true, then is cold start /// - private readonly string _namespace; + private static bool _isColdStart = true; /// - /// The Powertools for AWS Lambda (.NET) configurations + /// If true, capture annotations /// - private readonly IPowertoolsConfigurations _powertoolsConfigurations; - + private static bool _captureAnnotations = true; + /// - /// The segment name + /// If true, annotations have been captured /// - private readonly string _segmentName; - + private bool _isAnnotationsCaptured; + /// - /// X-Ray Recorder + /// Tracing namespace /// - private readonly IXRayRecorder _xRayRecorder; - + private string _namespace; + /// - /// If true, annotations have been captured + /// The capture mode /// - private bool _isAnnotationsCaptured; + private TracingCaptureMode _captureMode; /// - /// Initializes a new instance of the class. + /// Initializes a new instance /// - /// 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 - ) + public TracingAspect() { - _segmentName = segmentName; - _namespace = nameSpace; - _captureMode = captureMode; - _powertoolsConfigurations = powertoolsConfigurations; - _xRayRecorder = xRayRecorder; + _xRayRecorder = XRayRecorder.Instance; + _powertoolsConfigurations = PowertoolsConfigurations.Instance; } - + /// - /// Handles the event. + /// the code is executed instead of the target method. + /// The call to original method is wrapped around the following code + /// the original code is called with var result = target(args); /// - /// - /// The instance containing the - /// event data. - /// - public void OnEntry(AspectEventArgs eventArgs) + /// + /// + /// + /// + /// + /// + /// + /// + /// + [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) { - if(TracingDisabled()) - return; + // Before running Function + + try + { + var trigger = triggers.OfType().First(); + + if(TracingDisabled()) + return target(args); - var segmentName = !string.IsNullOrWhiteSpace(_segmentName) ? _segmentName : $"## {eventArgs.Name}"; - var nameSpace = GetNamespace(); + _namespace = trigger.Namespace; + _captureMode = trigger.CaptureMode; + + var segmentName = !string.IsNullOrWhiteSpace(trigger.SegmentName) ? trigger.SegmentName : $"## {name}"; + var nameSpace = GetNamespace(); - _xRayRecorder.BeginSubsegment(segmentName); - _xRayRecorder.SetNamespace(nameSpace); + _xRayRecorder.BeginSubsegment(segmentName); + _xRayRecorder.SetNamespace(nameSpace); - if (_captureAnnotations) - { - _xRayRecorder.AddAnnotation("ColdStart", _isColdStart); + if (_captureAnnotations) + { + _xRayRecorder.AddAnnotation("ColdStart", _isColdStart); - _captureAnnotations = false; - _isAnnotationsCaptured = true; + _captureAnnotations = false; + _isAnnotationsCaptured = true; - if (_powertoolsConfigurations.IsServiceDefined) - _xRayRecorder.AddAnnotation("Service", _powertoolsConfigurations.Service); - } + if (_powertoolsConfigurations.IsServiceDefined) + _xRayRecorder.AddAnnotation("Service", _powertoolsConfigurations.Service); + } + + _isColdStart = false; + + // return of the handler + var result = target(args); - _isColdStart = false; + if (CaptureResponse()) + { + _xRayRecorder.AddMetadata + ( + nameSpace, + $"{name} response", + result + ); + } + + // after + return result; + } + catch (Exception e) + { + HandleException(e, name); + throw; + } } /// - /// Called when [success]. + /// the code is injected after the method ends. /// - /// - /// 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 - ); - } - } + [Advice(Kind.After)] + public void OnExit() { + if(TracingDisabled()) + return; + + if (_isAnnotationsCaptured) + _captureAnnotations = true; + _xRayRecorder.EndSubsegment(); + } + /// - /// Called when [exception]. + /// Code that handles when exceptions occur in the client method /// - /// - /// The instance containing the - /// event data. - /// - /// The exception. - public void OnException(AspectEventArgs eventArgs, Exception exception) + /// + /// + private void HandleException(Exception exception, string name) { if (CaptureError()) { @@ -182,34 +191,16 @@ public void OnException(AspectEventArgs eventArgs, Exception exception) _xRayRecorder.AddMetadata ( nameSpace, - $"{eventArgs.Name} error", + $"{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 + // // 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. /// @@ -218,16 +209,34 @@ private string GetNamespace() { return !string.IsNullOrWhiteSpace(_namespace) ? _namespace : _powertoolsConfigurations.Service; } + + /// + /// Method that checks if tracing is disabled + /// + /// + 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; + } + /// /// Captures the response. /// /// true if tracing should capture responses, false otherwise. private bool CaptureResponse() { - if(TracingDisabled()) - return false; - switch (_captureMode) { case TracingCaptureMode.EnvironmentVariable: @@ -241,7 +250,7 @@ private bool CaptureResponse() return false; } } - + /// /// Captures the error. /// @@ -265,32 +274,6 @@ private bool CaptureError() } } - /// - /// Tracing disabled. - /// - /// true if tracing is disabled, false otherwise. - private bool TracingDisabled() - { - if (_isTracingDisabled.HasValue) - return _isTracingDisabled.Value; - - if (_powertoolsConfigurations.TracingDisabled) - { - _isTracingDisabled = true; - Console.WriteLine("Tracing has been disabled via env var POWERTOOLS_TRACE_DISABLED"); - } - else if (!_powertoolsConfigurations.IsLambdaEnvironment) - { - _isTracingDisabled = true; - Console.WriteLine("Running outside Lambda environment; disabling Tracing"); - } - else - { - _isTracingDisabled = false; - } - return _isTracingDisabled.Value; - } - /// /// Resets static variables for test. /// @@ -298,6 +281,5 @@ internal static void ResetForTest() { _isColdStart = true; _captureAnnotations = true; - _isTracingDisabled = null; } -} +} \ 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 0d9d6727..4ac0db2d 100644 --- a/libraries/src/AWS.Lambda.Powertools.Tracing/TracingAttribute.cs +++ b/libraries/src/AWS.Lambda.Powertools.Tracing/TracingAttribute.cs @@ -13,6 +13,8 @@ * permissions and limitations under the License. */ +using System; +using AspectInjector.Broker; using AWS.Lambda.Powertools.Common; using AWS.Lambda.Powertools.Tracing.Internal; @@ -105,7 +107,8 @@ namespace AWS.Lambda.Powertools.Tracing; /// } /// /// -public class TracingAttribute : MethodAspectAttribute +[Injection(typeof(TracingAspect))] +public class TracingAttribute : Attribute { /// /// Set custom segment name for the operation. @@ -128,20 +131,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/src/Directory.Build.props b/libraries/src/Directory.Build.props index 3a1d85f2..8f412fa4 100644 --- a/libraries/src/Directory.Build.props +++ b/libraries/src/Directory.Build.props @@ -20,13 +20,13 @@ - - + + true true true - + diff --git a/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/AWS.Lambda.Powertools.Tracing.Tests.csproj b/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/AWS.Lambda.Powertools.Tracing.Tests.csproj index ce7a88ba..7c88448a 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/AWS.Lambda.Powertools.Tracing.Tests.csproj +++ b/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/AWS.Lambda.Powertools.Tracing.Tests.csproj @@ -13,6 +13,8 @@ + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/Handlers/FullExampleHandler.cs b/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/Handlers/FullExampleHandler.cs new file mode 100644 index 00000000..5d1e78e5 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/Handlers/FullExampleHandler.cs @@ -0,0 +1,54 @@ +/* + * 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.Threading.Tasks; +using Amazon.Lambda.Core; + +namespace AWS.Lambda.Powertools.Tracing.Tests.Handlers; + +public class FullExampleHandler +{ + [Tracing(Namespace = "ns", CaptureMode = TracingCaptureMode.ResponseAndError)] + public Task Handle(string text, ILambdaContext context) + { + Tracing.AddAnnotation("annotation", "value"); + BusinessLogic1().GetAwaiter().GetResult(); + + return Task.FromResult(text.ToUpper()); + } + + [Tracing(SegmentName = "First Call")] + private async Task BusinessLogic1() + { + await BusinessLogic2(); + } + + [Tracing(CaptureMode = TracingCaptureMode.Disabled)] + private async Task BusinessLogic2() + { + Tracing.AddMetadata("metadata", "value"); + + Tracing.WithSubsegment("localNamespace", "GetSomething", (subsegment) => { + GetSomething(); + }); + + await Task.FromResult(0); + } + + private void GetSomething() + { + Tracing.AddAnnotation("getsomething", "value"); + } +} \ 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 066d0743..29f444cb 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/Handlers/HandlerTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/Handlers/HandlerTests.cs @@ -1,11 +1,22 @@ using System; +using System.Linq; using System.Threading.Tasks; +using Amazon.Lambda.TestUtilities; +using Amazon.XRay.Recorder.Core; +using AWS.Lambda.Powertools.Tracing.Internal; +using AWS.Lambda.Powertools.Tracing.Tests.Handlers; using Xunit; -namespace AWS.Lambda.Powertools.Tracing.Tests.Handlers; +namespace AWS.Lambda.Powertools.Tracing.Tests; -public sealed class HandlerTests +[Collection("Sequential")] +public sealed class HandlerTests : IDisposable { + public HandlerTests() + { + Environment.SetEnvironmentVariable("LAMBDA_TASK_ROOT", "AWS"); + } + [Fact] public async Task Stack_Trace_Included_When_Decorator_Present() { @@ -18,7 +29,6 @@ public async Task Stack_Trace_Included_When_Decorator_Present() // Assert var tracedException = await Assert.ThrowsAsync(Handle); Assert.StartsWith("at AWS.Lambda.Powertools.Tracing.Tests.Handlers.ExceptionFunctionHandler.ThisThrows()", tracedException.StackTrace?.TrimStart()); - } [Fact] @@ -31,6 +41,71 @@ public async Task When_Decorator_Present_In_Generic_Method_Should_Not_Throw_When await handler.Handle("whatever"); // Assert + } + + [Fact] + public async Task Full_Example() + { + // Arrange + Environment.SetEnvironmentVariable("LAMBDA_TASK_ROOT", "AWS"); + Environment.SetEnvironmentVariable("POWERTOOLS_SERVICE_NAME", "POWERTOOLS"); + + var handler = new FullExampleHandler(); + var context = new TestLambdaContext + { + FunctionName = "FullExampleLambda", + FunctionVersion = "1", + MemoryLimitInMB = 215, + AwsRequestId = Guid.NewGuid().ToString("D") + }; + // Act + await handler.Handle("Hello World", context); + var handleSegment = AWSXRayRecorder.Instance.TraceContext.GetEntity(); + + // Assert + Assert.True(handleSegment.IsAnnotationsAdded); + Assert.True(handleSegment.IsSubsegmentsAdded); + + Assert.Equal("POWERTOOLS", handleSegment.Annotations["Service"]); + Assert.True((bool)handleSegment.Annotations["ColdStart"]); + Assert.Equal("value", handleSegment.Annotations["annotation"]); + Assert.Equal("## Handle", handleSegment.Name); + + var firstCallSubsegment = handleSegment.Subsegments[0]; + + Assert.Equal("First Call", firstCallSubsegment.Name); + Assert.False(firstCallSubsegment.IsInProgress); + Assert.False(firstCallSubsegment.IsAnnotationsAdded); + // Assert.True(firstCallSubsegment.IsMetadataAdded); + Assert.True(firstCallSubsegment.IsSubsegmentsAdded); + + var businessLogicSubsegment = firstCallSubsegment.Subsegments[0]; + + Assert.Equal("## BusinessLogic2", businessLogicSubsegment.Name); + Assert.True(businessLogicSubsegment.IsMetadataAdded); + Assert.False(businessLogicSubsegment.IsInProgress); + Assert.Single(businessLogicSubsegment.Metadata); + var metadata = businessLogicSubsegment.Metadata["POWERTOOLS"]; + Assert.Contains("metadata", metadata.Keys.Cast()); + Assert.Contains("value", metadata.Values.Cast()); + Assert.True(businessLogicSubsegment.IsSubsegmentsAdded); + + var getSomethingSubsegment = businessLogicSubsegment.Subsegments[0]; + + Assert.Equal("## GetSomething", getSomethingSubsegment.Name); + Assert.Equal("localNamespace", getSomethingSubsegment.Namespace); + Assert.True(getSomethingSubsegment.IsAnnotationsAdded); + Assert.False(getSomethingSubsegment.IsSubsegmentsAdded); + Assert.False(getSomethingSubsegment.IsInProgress); + Assert.Equal("value", getSomethingSubsegment.Annotations["getsomething"]); + } + + public void Dispose() + { + Environment.SetEnvironmentVariable("LAMBDA_TASK_ROOT", ""); + Environment.SetEnvironmentVariable("POWERTOOLS_SERVICE_NAME", ""); + Environment.SetEnvironmentVariable("POWERTOOLS_TRACE_DISABLED", ""); + 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 new file mode 100644 index 00000000..19ecc681 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/Handlers/Handlers.cs @@ -0,0 +1,86 @@ +/* + * 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; + +namespace AWS.Lambda.Powertools.Tracing.Tests; + +public class HandlerFunctions +{ + [Tracing()] + public string[] Handle() + { + return new[] { "A", "B" }; + } + + [Tracing(SegmentName = "SegmentName")] + public void HandleWithSegmentName() + { + + } + + [Tracing(Namespace = "Namespace Defined")] + public void HandleWithNamespace() + { + + } + + [Tracing()] + public void HandleThrowsException(string exception) + { + throw new Exception(exception); + } + + [Tracing(CaptureMode = TracingCaptureMode.Response)] + public string[] HandleWithCaptureModeResponse(bool exception = false) + { + if (exception) + throw new Exception("Failed"); + + return new[] { "A", "B" }; + } + + [Tracing(CaptureMode = TracingCaptureMode.ResponseAndError)] + public string[] HandleWithCaptureModeResponseAndError(bool exception = false) + { + if (exception) + throw new Exception("Failed"); + return new[] { "A", "B" }; + } + + [Tracing(CaptureMode = TracingCaptureMode.Error)] + public string[] HandleWithCaptureModeError(bool exception = false) + { + if (exception) + throw new Exception("Failed"); + return new[] { "A", "B" }; + } + + [Tracing(CaptureMode = TracingCaptureMode.Error)] + public string[] HandleWithCaptureModeErrorInner(bool exception = false) + { + if (exception) + throw new Exception("Failed", new Exception("Inner Exception!!")); + return new[] { "A", "B" }; + } + + [Tracing(CaptureMode = TracingCaptureMode.Disabled)] + public string[] HandleWithCaptureModeDisabled(bool exception = false) + { + if (exception) + throw new Exception("Failed"); + return new[] { "A", "B" }; + } +} \ 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 34031725..b99c41a7 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/TracingAttributeTest.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/TracingAttributeTest.cs @@ -1,12 +1,12 @@ /* * 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 @@ -16,9 +16,8 @@ using System; using System.Linq; using System.Text; -using AWS.Lambda.Powertools.Common; +using Amazon.XRay.Recorder.Core; using AWS.Lambda.Powertools.Tracing.Internal; -using NSubstitute; using Xunit; [assembly: CollectionBehavior(DisableTestParallelization = true)] @@ -26,350 +25,273 @@ namespace AWS.Lambda.Powertools.Tracing.Tests { [Collection("Sequential")] - public class TracingAttributeColdStartTest + public class TracingAttributeColdStartTest : IDisposable { + private readonly HandlerFunctions _handler; + + public TracingAttributeColdStartTest() + { + _handler = new HandlerFunctions(); + } + [Fact] public void OnEntry_WhenFirstCall_CapturesColdStart() { // Arrange - const bool isColdStart = true; - var methodName = Guid.NewGuid().ToString(); - var service = Guid.NewGuid().ToString(); - - var configurations1 = Substitute.For(); - configurations1.TracingDisabled.Returns(false); - configurations1.IsLambdaEnvironment.Returns(true); - configurations1.IsServiceDefined.Returns(true); - configurations1.Service.Returns(service); - - var configurations2 = Substitute.For(); - configurations2.TracingDisabled.Returns(false); - configurations2.IsLambdaEnvironment.Returns(true); - configurations2.IsServiceDefined.Returns(false); - - var recorder1 = Substitute.For(); - var recorder2 = Substitute.For(); - var recorder3 = Substitute.For(); - var recorder4 = Substitute.For(); - - TracingAspectHandler.ResetForTest(); - var handler1 = new TracingAspectHandler(null, null, TracingCaptureMode.EnvironmentVariable, - configurations1, recorder1); - var handler2 = new TracingAspectHandler(null, null, TracingCaptureMode.EnvironmentVariable, - configurations1, recorder2); - var handler3 = new TracingAspectHandler(null, null, TracingCaptureMode.EnvironmentVariable, - configurations2, recorder3); - var handler4 = new TracingAspectHandler(null, null, TracingCaptureMode.EnvironmentVariable, - configurations2, recorder4); - - var eventArgs = new AspectEventArgs { Name = methodName }; + Environment.SetEnvironmentVariable("LAMBDA_TASK_ROOT", "AWS"); + Environment.SetEnvironmentVariable("POWERTOOLS_SERVICE_NAME", "POWERTOOLS"); // Act // Cold Start Execution - handler1.OnEntry(eventArgs); - handler2.OnEntry(eventArgs); - handler2.OnExit(eventArgs); - handler1.OnExit(eventArgs); + // Start segment + var segmentCold = AWSXRayRecorder.Instance.TraceContext.GetEntity(); + _handler.Handle(); + + var subSegmentCold = segmentCold.Subsegments[0]; // Warm Start Execution - handler3.OnEntry(eventArgs); - handler4.OnEntry(eventArgs); - handler4.OnExit(eventArgs); - handler3.OnExit(eventArgs); + // Start segment + var segmentWarm = AWSXRayRecorder.Instance.TraceContext.GetEntity(); + _handler.Handle(); + var subSegmentWarm = segmentWarm.Subsegments[0]; // Assert - recorder1.Received(1).AddAnnotation( - Arg.Is(i => i == "ColdStart"), - Arg.Is(i => i == isColdStart) - ); - - recorder1.Received(1).AddAnnotation( - Arg.Is(i => i == "Service"), - Arg.Is(i => i == service) - ); - - recorder2.DidNotReceive().AddAnnotation( - Arg.Any(), - Arg.Any() - ); - - recorder2.DidNotReceive().AddAnnotation( - Arg.Any(), - Arg.Any() - ); - - recorder3.Received(1).AddAnnotation( - Arg.Is(i => i == "ColdStart"), - Arg.Is(i => i == !isColdStart) - ); - - recorder3.DidNotReceive().AddAnnotation( - Arg.Any(), - Arg.Any() - ); - - recorder4.DidNotReceive().AddAnnotation( - Arg.Any(), - Arg.Any() - ); - - recorder4.DidNotReceive().AddAnnotation( - Arg.Any(), - Arg.Any() - ); + // Cold + Assert.True(segmentCold.IsSubsegmentsAdded); + Assert.Single(segmentCold.Subsegments); + Assert.True(subSegmentCold.IsAnnotationsAdded); + Assert.Equal(2, subSegmentCold.Annotations.Count()); + Assert.True((bool)subSegmentCold.Annotations.Single(x => x.Key == "ColdStart").Value); + Assert.Equal("POWERTOOLS", subSegmentCold.Annotations.Single(x => x.Key == "Service").Value); + + // Warm + Assert.True(segmentWarm.IsSubsegmentsAdded); + Assert.Single(segmentWarm.Subsegments); + Assert.True(subSegmentWarm.IsAnnotationsAdded); + Assert.Equal(2, subSegmentWarm.Annotations.Count()); + Assert.False((bool)subSegmentWarm.Annotations.Single(x => x.Key == "ColdStart").Value); + Assert.Equal("POWERTOOLS", subSegmentWarm.Annotations.Single(x => x.Key == "Service").Value); } - } - [Collection("Sequential")] - public class TracingAttributeDisableTest - { [Fact] - public void Tracing_WhenTracerDisabled_DisablesTracing() + public void OnEntry_WhenFirstCall_And_Service_Not_Set_CapturesColdStart() { // Arrange - var methodName = Guid.NewGuid().ToString(); - var configurations1 = Substitute.For(); - configurations1.TracingDisabled.Returns(true); - configurations1.IsLambdaEnvironment.Returns(true); + Environment.SetEnvironmentVariable("LAMBDA_TASK_ROOT", "AWS"); - var configurations2 = Substitute.For(); - configurations2.TracingDisabled.Returns(true); - configurations2.IsLambdaEnvironment.Returns(true); + // Act + // Cold Start Execution + // Start segment + var segmentCold = AWSXRayRecorder.Instance.TraceContext.GetEntity(); + _handler.Handle(); + var subSegmentCold = segmentCold.Subsegments[0]; - var recorder1 = Substitute.For(); - var recorder2 = Substitute.For(); + // Warm Start Execution + // Start segment + var segmentWarm = AWSXRayRecorder.Instance.TraceContext.GetEntity(); + _handler.Handle(); + var subSegmentWarm = segmentWarm.Subsegments[0]; - TracingAspectHandler.ResetForTest(); - var handler1 = new TracingAspectHandler(null, null, TracingCaptureMode.EnvironmentVariable, - configurations1, recorder1); - var handler2 = new TracingAspectHandler(null, null, TracingCaptureMode.EnvironmentVariable, - configurations2, recorder2); + // Assert + // Cold + Assert.True(segmentCold.IsSubsegmentsAdded); + Assert.Single(segmentCold.Subsegments); + Assert.True(subSegmentCold.IsAnnotationsAdded); + Assert.Single(subSegmentCold.Annotations); + Assert.True((bool)subSegmentCold.Annotations.Single(x => x.Key == "ColdStart").Value); + + // Warm + Assert.True(segmentWarm.IsSubsegmentsAdded); + Assert.Single(segmentWarm.Subsegments); + Assert.True(subSegmentWarm.IsAnnotationsAdded); + Assert.Single(subSegmentWarm.Annotations); + Assert.False((bool)subSegmentWarm.Annotations.Single(x => x.Key == "ColdStart").Value); + } - var results = new[] { "A", "B" }; - var exception = new Exception("Test Exception"); - var eventArgs = new AspectEventArgs { Name = methodName }; + public void Dispose() + { + Environment.SetEnvironmentVariable("LAMBDA_TASK_ROOT", ""); + Environment.SetEnvironmentVariable("POWERTOOLS_SERVICE_NAME", ""); + Environment.SetEnvironmentVariable("POWERTOOLS_TRACER_CAPTURE_RESPONSE", ""); + Environment.SetEnvironmentVariable("POWERTOOLS_TRACER_CAPTURE_ERROR", ""); + Environment.SetEnvironmentVariable("POWERTOOLS_TRACE_DISABLED", ""); + TracingAspect.ResetForTest(); + } + } + + [Collection("Sequential")] + public class TracingAttributeDisableTest : IDisposable + { + private readonly HandlerFunctions _handler; + + public TracingAttributeDisableTest() + { + _handler = new HandlerFunctions(); + } + + [Fact] + public void Tracing_WhenTracerDisabled_DisablesTracing() + { + // Arrange + Environment.SetEnvironmentVariable("LAMBDA_TASK_ROOT", "AWS"); + Environment.SetEnvironmentVariable("POWERTOOLS_TRACE_DISABLED", "true"); // Act // Cold Start Execution - handler1.OnEntry(eventArgs); - handler1.OnSuccess(eventArgs, results); - void Act1() => handler1.OnException(eventArgs, exception); - handler1.OnExit(eventArgs); + // Start segment + var segmentCold = AWSXRayRecorder.Instance.TraceContext.GetEntity(); + _handler.Handle(); // Warm Start Execution - handler2.OnEntry(eventArgs); - handler2.OnSuccess(eventArgs, results); - void Act2() => handler2.OnException(eventArgs, exception); - handler2.OnExit(eventArgs); + // Start segment + var segmentWarm = AWSXRayRecorder.Instance.TraceContext.GetEntity(); + _handler.Handle(); // Assert - recorder1.DidNotReceive().BeginSubsegment(Arg.Any()); - recorder1.DidNotReceive().EndSubsegment(); - recorder1.DidNotReceive().AddMetadata( - Arg.Any(), - Arg.Any(), - Arg.Any() - ); - Assert.Throws(Act1); - recorder1.DidNotReceive().AddMetadata( - Arg.Any(), - Arg.Any(), - Arg.Any() - ); - - recorder2.DidNotReceive().BeginSubsegment(Arg.Any()); - recorder2.DidNotReceive().EndSubsegment(); - recorder2.DidNotReceive().AddMetadata( - Arg.Any(), - Arg.Any(), - Arg.Any() - ); - Assert.Throws(Act2); - recorder2.DidNotReceive().AddMetadata( - Arg.Any(), - Arg.Any(), - Arg.Any() - ); + Assert.False(segmentCold.IsAnnotationsAdded); + Assert.Empty(segmentCold.Annotations); + Assert.False(segmentCold.IsSubsegmentsAdded); + Assert.False(segmentCold.IsMetadataAdded); + + Assert.False(segmentWarm.IsAnnotationsAdded); + Assert.Empty(segmentWarm.Annotations); + Assert.False(segmentWarm.IsSubsegmentsAdded); + Assert.False(segmentWarm.IsMetadataAdded); + } + + public void Dispose() + { + ClearEnvironment(); + } + + private static void ClearEnvironment() + { + Environment.SetEnvironmentVariable("LAMBDA_TASK_ROOT", ""); + Environment.SetEnvironmentVariable("POWERTOOLS_SERVICE_NAME", ""); + Environment.SetEnvironmentVariable("POWERTOOLS_TRACER_CAPTURE_RESPONSE", ""); + Environment.SetEnvironmentVariable("POWERTOOLS_TRACER_CAPTURE_ERROR", ""); + Environment.SetEnvironmentVariable("POWERTOOLS_TRACE_DISABLED", ""); + TracingAspect.ResetForTest(); } } [Collection("Sequential")] public class TracingAttributeLambdaEnvironmentTest { + private readonly HandlerFunctions _handler; + + public TracingAttributeLambdaEnvironmentTest() + { + _handler = new HandlerFunctions(); + } + [Fact] public void Tracing_WhenOutsideOfLambdaEnv_DisablesTracing() { // Arrange - var methodName = Guid.NewGuid().ToString(); - var configurations1 = Substitute.For(); - configurations1.TracingDisabled.Returns(false); - configurations1.IsLambdaEnvironment.Returns(false); - - var configurations2 = Substitute.For(); - configurations2.TracingDisabled.Returns(true); - configurations2.IsLambdaEnvironment.Returns(true); - - var recorder1 = Substitute.For(); - var recorder2 = Substitute.For(); - - TracingAspectHandler.ResetForTest(); - var handler1 = new TracingAspectHandler(null, null, TracingCaptureMode.EnvironmentVariable, - configurations1, recorder1); - var handler2 = new TracingAspectHandler(null, null, TracingCaptureMode.EnvironmentVariable, - configurations1, recorder2); - - var results = new[] { "A", "B" }; - var exception = new Exception("Test Exception"); - var eventArgs = new AspectEventArgs { Name = methodName }; - + + // Need to manually create the initial segment + AWSXRayRecorder.Instance.BeginSegment("foo"); + // Act // Cold Start Execution - handler1.OnEntry(eventArgs); - handler1.OnSuccess(eventArgs, results); - void Act1() => handler1.OnException(eventArgs, exception); - handler1.OnExit(eventArgs); - - // Warm Start Execution - handler2.OnEntry(eventArgs); - handler2.OnSuccess(eventArgs, results); - void Act2() => handler2.OnException(eventArgs, exception); - handler2.OnExit(eventArgs); - + _handler.Handle(); + var segmentCold = AWSXRayRecorder.Instance.TraceContext.GetEntity(); + // Assert - recorder1.DidNotReceive().BeginSubsegment(Arg.Any()); - recorder1.DidNotReceive().EndSubsegment(); - recorder1.DidNotReceive().AddMetadata( - Arg.Any(), - Arg.Any(), - Arg.Any() - ); - Assert.Throws(Act1); - recorder1.DidNotReceive().AddMetadata( - Arg.Any(), - Arg.Any(), - Arg.Any() - ); - - recorder2.DidNotReceive().BeginSubsegment(Arg.Any()); - recorder2.DidNotReceive().EndSubsegment(); - recorder2.DidNotReceive().AddMetadata( - Arg.Any(), - Arg.Any(), - Arg.Any() - ); - Assert.Throws(Act2); - recorder2.DidNotReceive().AddMetadata( - Arg.Any(), - Arg.Any(), - Arg.Any() - ); + Assert.False(AWSXRayRecorder.IsLambda()); + Assert.False(segmentCold.IsAnnotationsAdded); + Assert.Empty(segmentCold.Annotations); + Assert.False(segmentCold.IsSubsegmentsAdded); + Assert.False(segmentCold.IsMetadataAdded); + + AWSXRayRecorder.Instance.EndSegment(); } } - + [Collection("Sequential")] - public class TracingAttributeTest + public class TracingAttributeTest : IDisposable { + private readonly HandlerFunctions _handler; + public TracingAttributeTest() { - TracingAspectHandler.ResetForTest(); + _handler = new HandlerFunctions(); } - + #region OnEntry Tests [Fact] public void OnEntry_WhenSegmentNameIsNull_BeginSubsegmentWithMethodName() { // Arrange - var methodName = Guid.NewGuid().ToString(); - var configurations = Substitute.For(); - configurations.IsLambdaEnvironment.Returns(true); - configurations.TracingDisabled.Returns(false); - var recorder = Substitute.For(); - - var handler = new TracingAspectHandler(null, null, TracingCaptureMode.EnvironmentVariable, - configurations, recorder); - var eventArgs = new AspectEventArgs { Name = methodName }; - + Environment.SetEnvironmentVariable("LAMBDA_TASK_ROOT", "AWS"); + Environment.SetEnvironmentVariable("POWERTOOLS_SERVICE_NAME", "POWERTOOLS"); + // Act - handler.OnEntry(eventArgs); + var segment = AWSXRayRecorder.Instance.TraceContext.GetEntity(); + _handler.Handle(); + var subSegment = segment.Subsegments[0]; // Assert - recorder.Received(1).BeginSubsegment( - Arg.Is(i => i == $"## {methodName}") - ); + Assert.True(segment.IsSubsegmentsAdded); + Assert.Single(segment.Subsegments); + Assert.Equal("## Handle", subSegment.Name); } [Fact] public void OnEntry_WhenSegmentNameHasValue_BeginSubsegmentWithValue() { // Arrange - var segmentName = Guid.NewGuid().ToString(); - var methodName = Guid.NewGuid().ToString(); - var configurations = Substitute.For(); - configurations.IsLambdaEnvironment.Returns(true); - configurations.TracingDisabled.Returns(false); - var recorder = Substitute.For(); - - var handler = new TracingAspectHandler(segmentName, null, TracingCaptureMode.EnvironmentVariable, - configurations, recorder); - var eventArgs = new AspectEventArgs { Name = methodName }; + Environment.SetEnvironmentVariable("LAMBDA_TASK_ROOT", "AWS"); + Environment.SetEnvironmentVariable("POWERTOOLS_SERVICE_NAME", "POWERTOOLS"); // Act - handler.OnEntry(eventArgs); + var segment = AWSXRayRecorder.Instance.TraceContext.GetEntity(); + _handler.HandleWithSegmentName(); + var subSegment = segment.Subsegments[0]; // Assert - recorder.Received(1).BeginSubsegment( - Arg.Is(i => i == segmentName) - ); + Assert.True(segment.IsSubsegmentsAdded); + Assert.Single(segment.Subsegments); + Assert.Equal("SegmentName", subSegment.Name); } [Fact] public void OnEntry_WhenNamespaceIsNull_SetNamespaceWithService() { // Arrange - var methodName = Guid.NewGuid().ToString(); - var service = Guid.NewGuid().ToString(); - var configurations = Substitute.For(); - configurations.IsLambdaEnvironment.Returns(true); - configurations.TracingDisabled.Returns(false); - configurations.Service.Returns(service); - var recorder = Substitute.For(); - - var handler = new TracingAspectHandler(null, null, TracingCaptureMode.EnvironmentVariable, - configurations, recorder); - var eventArgs = new AspectEventArgs { Name = methodName }; + var serviceName = "POWERTOOLS"; + Environment.SetEnvironmentVariable("LAMBDA_TASK_ROOT", "AWS"); + Environment.SetEnvironmentVariable("POWERTOOLS_SERVICE_NAME", serviceName); // Act - handler.OnEntry(eventArgs); + var segment = AWSXRayRecorder.Instance.TraceContext.GetEntity(); + _handler.Handle(); + var subSegment = segment.Subsegments[0]; // Assert - recorder.Received(1).SetNamespace( - Arg.Is(i => i == service) - ); + Assert.True(segment.IsSubsegmentsAdded); + Assert.Single(segment.Subsegments); + Assert.Equal(serviceName, subSegment.Namespace); } [Fact] public void OnEntry_WhenNamespaceHasValue_SetNamespaceWithValue() { // Arrange - var methodName = Guid.NewGuid().ToString(); - var nameSpace = Guid.NewGuid().ToString(); - var configurations = Substitute.For(); - configurations.IsLambdaEnvironment.Returns(true); - configurations.TracingDisabled.Returns(false); - var recorder = Substitute.For(); - - var handler = new TracingAspectHandler(null, nameSpace, TracingCaptureMode.EnvironmentVariable, - configurations, recorder); - var eventArgs = new AspectEventArgs { Name = methodName }; + Environment.SetEnvironmentVariable("LAMBDA_TASK_ROOT", "AWS"); + Environment.SetEnvironmentVariable("POWERTOOLS_SERVICE_NAME", "POWERTOOLS"); // Act - handler.OnEntry(eventArgs); + var segment = AWSXRayRecorder.Instance.TraceContext.GetEntity(); + _handler.HandleWithNamespace(); + var subSegment = segment.Subsegments[0]; // Assert - recorder.Received(1).SetNamespace( - Arg.Is(i => i == nameSpace) - ); + Assert.True(segment.IsSubsegmentsAdded); + Assert.Single(segment.Subsegments); + Assert.Equal("Namespace Defined", subSegment.Namespace); } #endregion @@ -380,173 +302,132 @@ public void OnEntry_WhenNamespaceHasValue_SetNamespaceWithValue() public void OnSuccess_WhenTracerCaptureResponseEnvironmentVariableIsTrue_CapturesResponse() { // Arrange - var methodName = Guid.NewGuid().ToString(); - var nameSpace = Guid.NewGuid().ToString(); - var configurations = Substitute.For(); - configurations.IsLambdaEnvironment.Returns(true); - configurations.TracingDisabled.Returns(false); - configurations.TracerCaptureResponse.Returns(true); - var recorder = Substitute.For(); - var results = new[] { "A", "B" }; - - var handler = new TracingAspectHandler(null, nameSpace, TracingCaptureMode.EnvironmentVariable, - configurations, recorder); - var eventArgs = new AspectEventArgs { Name = methodName }; - + Environment.SetEnvironmentVariable("LAMBDA_TASK_ROOT", "AWS"); + Environment.SetEnvironmentVariable("POWERTOOLS_SERVICE_NAME", "POWERTOOLS"); + Environment.SetEnvironmentVariable("POWERTOOLS_TRACER_CAPTURE_RESPONSE", "true"); + // Act - handler.OnSuccess(eventArgs, results); + var segment = AWSXRayRecorder.Instance.TraceContext.GetEntity(); + _handler.Handle(); + var subSegment = segment.Subsegments[0]; // Assert - recorder.Received(1).AddMetadata( - Arg.Is(i => i == nameSpace), - Arg.Is(i => i == $"{methodName} response"), - Arg.Is(i => - i.First() == results.First() && - i.Last() == results.Last() - ) - ); + 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_WhenTracerCaptureResponseEnvironmentVariableIsFalse_DoesNotCaptureResponse() { // Arrange - var methodName = Guid.NewGuid().ToString(); - var nameSpace = Guid.NewGuid().ToString(); - var configurations = Substitute.For(); - configurations.IsLambdaEnvironment.Returns(true); - configurations.TracingDisabled.Returns(false); - configurations.TracerCaptureResponse.Returns(false); - var recorder = Substitute.For(); - var results = new[] { "A", "B" }; - - var handler = new TracingAspectHandler(null, nameSpace, TracingCaptureMode.EnvironmentVariable, - configurations, recorder); - var eventArgs = new AspectEventArgs { Name = methodName }; - + Environment.SetEnvironmentVariable("LAMBDA_TASK_ROOT", "AWS"); + Environment.SetEnvironmentVariable("POWERTOOLS_SERVICE_NAME", "POWERTOOLS"); + Environment.SetEnvironmentVariable("POWERTOOLS_TRACER_CAPTURE_RESPONSE", "false"); + // Act - handler.OnSuccess(eventArgs, results); + var segment = AWSXRayRecorder.Instance.TraceContext.GetEntity(); + _handler.Handle(); + var subSegment = segment.Subsegments[0]; // Assert - recorder.DidNotReceive().AddMetadata( - Arg.Any(), - Arg.Any(), - Arg.Any() - ); + Assert.True(segment.IsSubsegmentsAdded); + Assert.Single(segment.Subsegments); + Assert.False(subSegment.IsMetadataAdded); + Assert.Empty(subSegment.Metadata); } [Fact] public void OnSuccess_WhenTracerCaptureModeIsResponse_CapturesResponse() { // Arrange - var methodName = Guid.NewGuid().ToString(); - var nameSpace = Guid.NewGuid().ToString(); - var configurations = Substitute.For(); - configurations.IsLambdaEnvironment.Returns(true); - configurations.TracingDisabled.Returns(false); - var recorder = Substitute.For(); - var results = new[] { "A", "B" }; - - var handler = new TracingAspectHandler(null, nameSpace, TracingCaptureMode.Response, - configurations, recorder); - var eventArgs = new AspectEventArgs { Name = methodName }; - + Environment.SetEnvironmentVariable("LAMBDA_TASK_ROOT", "AWS"); + Environment.SetEnvironmentVariable("POWERTOOLS_SERVICE_NAME", "POWERTOOLS"); + // Act - handler.OnSuccess(eventArgs, results); + var segment = AWSXRayRecorder.Instance.TraceContext.GetEntity(); + _handler.HandleWithCaptureModeResponse(); + var subSegment = segment.Subsegments[0]; // Assert - recorder.Received(1).AddMetadata( - Arg.Is(i => i == nameSpace), - Arg.Is(i => i == $"{methodName} response"), - Arg.Is(i => - i.First() == results.First() && - i.Last() == results.Last() - ) - ); + 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("HandleWithCaptureModeResponse 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_WhenTracerCaptureModeIsResponseAndError_CapturesResponse() { // Arrange - var methodName = Guid.NewGuid().ToString(); - var nameSpace = Guid.NewGuid().ToString(); - var configurations = Substitute.For(); - configurations.IsLambdaEnvironment.Returns(true); - configurations.TracingDisabled.Returns(false); - var recorder = Substitute.For(); - var results = new[] { "A", "B" }; - - var handler = new TracingAspectHandler(null, nameSpace, TracingCaptureMode.ResponseAndError, - configurations, recorder); - var eventArgs = new AspectEventArgs { Name = methodName }; - + Environment.SetEnvironmentVariable("LAMBDA_TASK_ROOT", "AWS"); + Environment.SetEnvironmentVariable("POWERTOOLS_SERVICE_NAME", "POWERTOOLS"); + // Act - handler.OnSuccess(eventArgs, results); + var segment = AWSXRayRecorder.Instance.TraceContext.GetEntity(); + _handler.HandleWithCaptureModeResponseAndError(); + var subSegment = segment.Subsegments[0]; // Assert - recorder.Received(1).AddMetadata( - Arg.Is(i => i == nameSpace), - Arg.Is(i => i == $"{methodName} response"), - Arg.Is(i => - i.First() == results.First() && - i.Last() == results.Last() - ) - ); + 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("HandleWithCaptureModeResponseAndError 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_WhenTracerCaptureModeIsError_DoesNotCaptureResponse() { // Arrange - var methodName = Guid.NewGuid().ToString(); - var nameSpace = Guid.NewGuid().ToString(); - var configurations = Substitute.For(); - configurations.IsLambdaEnvironment.Returns(true); - configurations.TracingDisabled.Returns(false); - var recorder = Substitute.For(); - var results = new[] { "A", "B" }; - - var handler = new TracingAspectHandler(null, nameSpace, TracingCaptureMode.Error, - configurations, recorder); - var eventArgs = new AspectEventArgs { Name = methodName }; - + Environment.SetEnvironmentVariable("LAMBDA_TASK_ROOT", "AWS"); + Environment.SetEnvironmentVariable("POWERTOOLS_SERVICE_NAME", "POWERTOOLS"); + // Act - handler.OnSuccess(eventArgs, results); + var segment = AWSXRayRecorder.Instance.TraceContext.GetEntity(); + _handler.HandleWithCaptureModeError(); + var subSegment = segment.Subsegments[0]; // Assert - recorder.DidNotReceive().AddMetadata( - Arg.Any(), - Arg.Any(), - Arg.Any() - ); + Assert.True(segment.IsSubsegmentsAdded); + Assert.Single(segment.Subsegments); + Assert.False(subSegment.IsMetadataAdded); // does not add metadata } [Fact] public void OnSuccess_WhenTracerCaptureModeIsDisabled_DoesNotCaptureResponse() { // Arrange - var methodName = Guid.NewGuid().ToString(); - var nameSpace = Guid.NewGuid().ToString(); - var configurations = Substitute.For(); - configurations.IsLambdaEnvironment.Returns(true); - configurations.TracingDisabled.Returns(false); - var recorder = Substitute.For(); - var results = new[] { "A", "B" }; - - var handler = new TracingAspectHandler(null, nameSpace, TracingCaptureMode.Disabled, - configurations, recorder); - var eventArgs = new AspectEventArgs { Name = methodName }; - + Environment.SetEnvironmentVariable("LAMBDA_TASK_ROOT", "AWS"); + Environment.SetEnvironmentVariable("POWERTOOLS_SERVICE_NAME", "POWERTOOLS"); + // Act - handler.OnSuccess(eventArgs, results); + var segment = AWSXRayRecorder.Instance.TraceContext.GetEntity(); + _handler.HandleWithCaptureModeDisabled(); + var subSegment = segment.Subsegments[0]; // Assert - recorder.DidNotReceive().AddMetadata( - Arg.Any(), - Arg.Any(), - Arg.Any() - ); + Assert.True(segment.IsSubsegmentsAdded); + Assert.Single(segment.Subsegments); + Assert.False(subSegment.IsMetadataAdded); // does not add metadata } #endregion @@ -557,176 +438,206 @@ public void OnSuccess_WhenTracerCaptureModeIsDisabled_DoesNotCaptureResponse() public void OnException_WhenTracerCaptureErrorEnvironmentVariableIsTrue_CapturesError() { // Arrange - var methodName = Guid.NewGuid().ToString(); - var nameSpace = Guid.NewGuid().ToString(); - var configurations = Substitute.For(); - configurations.IsLambdaEnvironment.Returns(true); - configurations.TracingDisabled.Returns(false); - configurations.TracerCaptureError.Returns(true); - var recorder = Substitute.For(); - var exception = new Exception("Test Exception"); - var message = GetException(exception); - - var handler = new TracingAspectHandler(null, nameSpace, TracingCaptureMode.EnvironmentVariable, - configurations, recorder); - var eventArgs = new AspectEventArgs { Name = methodName }; - + Environment.SetEnvironmentVariable("LAMBDA_TASK_ROOT", "AWS"); + Environment.SetEnvironmentVariable("POWERTOOLS_SERVICE_NAME", "POWERTOOLS"); + Environment.SetEnvironmentVariable("POWERTOOLS_TRACER_CAPTURE_ERROR", "true"); + // Act - void Act() => handler.OnException(eventArgs, exception); + var segment = AWSXRayRecorder.Instance.TraceContext.GetEntity(); + var exception = Record.Exception(() => + { + _handler.HandleThrowsException("My Exception"); + }); + var subSegment = segment.Subsegments[0]; + // Assert - Assert.Throws((Action)Act); - recorder.Received(1).AddMetadata( - Arg.Is(i => i == nameSpace), - Arg.Is(i => i == $"{methodName} error"), - Arg.Is(i => i == message) - ); + Assert.NotNull(exception); + 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("HandleThrowsException error", metadata.Keys.Cast().First()); + var handlerErrorMessage = metadata.Values.Cast().First(); + Assert.Contains(handlerErrorMessage, GetException(exception)); } [Fact] - public void OnException_WhenTracerCaptureErrorEnvironmentVariableIsTrueFalse_DoesNotCaptureError() + public void OnException_WhenTracerCaptureErrorEnvironmentVariableIsFalse_DoesNotCaptureError() { // Arrange - var methodName = Guid.NewGuid().ToString(); - var nameSpace = Guid.NewGuid().ToString(); - var configurations = Substitute.For(); - configurations.IsLambdaEnvironment.Returns(true); - configurations.TracingDisabled.Returns(false); - configurations.TracerCaptureError.Returns(false); - var recorder = Substitute.For(); - var exception = new Exception("Test Exception"); - - var handler = new TracingAspectHandler(null, nameSpace, TracingCaptureMode.EnvironmentVariable, - configurations, recorder); - var eventArgs = new AspectEventArgs { Name = methodName }; - + Environment.SetEnvironmentVariable("LAMBDA_TASK_ROOT", "AWS"); + Environment.SetEnvironmentVariable("POWERTOOLS_SERVICE_NAME", "POWERTOOLS"); + Environment.SetEnvironmentVariable("POWERTOOLS_TRACER_CAPTURE_ERROR", "false"); + // Act - void Act() => handler.OnException(eventArgs, exception); + var segment = AWSXRayRecorder.Instance.TraceContext.GetEntity(); + var exception = Record.Exception(() => + { + _handler.HandleThrowsException("My Exception"); + }); + var subSegment = segment.Subsegments[0]; + // Assert - Assert.Throws((Action)Act); - recorder.DidNotReceive().AddMetadata( - Arg.Any(), - Arg.Any(), - Arg.Any() - ); + Assert.NotNull(exception); + Assert.True(segment.IsSubsegmentsAdded); + Assert.Single(segment.Subsegments); + Assert.False(subSegment.IsMetadataAdded); // no metadata for errors added } [Fact] public void OnException_WhenTracerCaptureModeIsError_CapturesError() { // Arrange - var methodName = Guid.NewGuid().ToString(); - var nameSpace = Guid.NewGuid().ToString(); - var configurations = Substitute.For(); - configurations.IsLambdaEnvironment.Returns(true); - configurations.TracingDisabled.Returns(false); - var recorder = Substitute.For(); - var exception = new Exception("Test Exception"); - var message = GetException(exception); - - var handler = new TracingAspectHandler(null, nameSpace, TracingCaptureMode.Error, - configurations, recorder); - var eventArgs = new AspectEventArgs { Name = methodName }; + Environment.SetEnvironmentVariable("LAMBDA_TASK_ROOT", "AWS"); + Environment.SetEnvironmentVariable("POWERTOOLS_SERVICE_NAME", "POWERTOOLS"); + + // Act + var segment = AWSXRayRecorder.Instance.TraceContext.GetEntity(); + + var exception = Record.Exception(() => + { + _handler.HandleWithCaptureModeError(true); + }); + var subSegment = segment.Subsegments[0]; + + // Assert + Assert.NotNull(exception); + 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("HandleWithCaptureModeError error", metadata.Keys.Cast().First()); + var handlerErrorMessage = metadata.Values.Cast().First(); + Assert.Contains(handlerErrorMessage, GetException(exception)); + } + + [Fact] + public void OnException_WhenTracerCaptureModeIsError_CapturesError_Inner_Exception() + { + // Arrange + Environment.SetEnvironmentVariable("LAMBDA_TASK_ROOT", "AWS"); + Environment.SetEnvironmentVariable("POWERTOOLS_SERVICE_NAME", "POWERTOOLS"); + + // Act + var segment = AWSXRayRecorder.Instance.TraceContext.GetEntity(); + var exception = Record.Exception(() => + { + _handler.HandleWithCaptureModeErrorInner(true); + }); + var subSegment = segment.Subsegments[0]; + + // Assert + Assert.NotNull(exception); + 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("HandleWithCaptureModeErrorInner error", metadata.Keys.Cast().First()); + Assert.NotNull(exception.InnerException); + Assert.Equal("Inner Exception!!",exception.InnerException.Message); + } + + [Fact] + public void OnException_When_Tracing_Disabled_Does_Not_CapturesError() + { + // Arrange + Environment.SetEnvironmentVariable("LAMBDA_TASK_ROOT", "AWS"); + Environment.SetEnvironmentVariable("POWERTOOLS_SERVICE_NAME", "POWERTOOLS"); + Environment.SetEnvironmentVariable("POWERTOOLS_TRACE_DISABLED", "true"); + // Act - void Act() => handler.OnException(eventArgs, exception); + var segment = AWSXRayRecorder.Instance.TraceContext.GetEntity(); + var exception = Record.Exception(() => + { + _handler.HandleWithCaptureModeError(true); + }); + // Assert - Assert.Throws((Action)Act); - recorder.Received(1).AddMetadata( - Arg.Is(i => i == nameSpace), - Arg.Is(i => i == $"{methodName} error"), - Arg.Is(i => i == message) - ); + Assert.NotNull(exception); + Assert.False(segment.IsSubsegmentsAdded); + Assert.Empty(segment.Subsegments); + Assert.False(segment.IsMetadataAdded); } [Fact] public void OnException_WhenTracerCaptureModeIsResponseAndError_CapturesError() { // Arrange - var methodName = Guid.NewGuid().ToString(); - var nameSpace = Guid.NewGuid().ToString(); - var configurations = Substitute.For(); - configurations.IsLambdaEnvironment.Returns(true); - configurations.TracingDisabled.Returns(false); - var recorder = Substitute.For(); - var exception = new Exception("Test Exception"); - var message = GetException(exception); - - var handler = new TracingAspectHandler(null, nameSpace, TracingCaptureMode.ResponseAndError, - configurations, recorder); - var eventArgs = new AspectEventArgs { Name = methodName }; - + Environment.SetEnvironmentVariable("LAMBDA_TASK_ROOT", "AWS"); + Environment.SetEnvironmentVariable("POWERTOOLS_SERVICE_NAME", "POWERTOOLS"); + // Act - void Act() => handler.OnException(eventArgs, exception); + var segment = AWSXRayRecorder.Instance.TraceContext.GetEntity(); + var exception = Record.Exception(() => + { + _handler.HandleWithCaptureModeResponseAndError(true); + }); + var subSegment = segment.Subsegments[0]; + // Assert - Assert.Throws((Action)Act); - recorder.Received(1).AddMetadata( - Arg.Is(i => i == nameSpace), - Arg.Is(i => i == $"{methodName} error"), - Arg.Is(i => i == message) - ); + Assert.NotNull(exception); + 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("HandleWithCaptureModeResponseAndError error", metadata.Keys.Cast().First()); + var handlerErrorMessage = metadata.Values.Cast().First(); + Assert.Contains(handlerErrorMessage, GetException(exception)); } - [Fact] public void OnException_WhenTracerCaptureModeIsResponse_DoesNotCaptureError() { // Arrange - var methodName = Guid.NewGuid().ToString(); - var nameSpace = Guid.NewGuid().ToString(); - var configurations = Substitute.For(); - configurations.IsLambdaEnvironment.Returns(true); - configurations.TracingDisabled.Returns(false); - configurations.TracerCaptureError.Returns(false); - var recorder = Substitute.For(); - var exception = new Exception("Test Exception"); - - var handler = new TracingAspectHandler(null, nameSpace, TracingCaptureMode.Response, - configurations, recorder); - var eventArgs = new AspectEventArgs { Name = methodName }; - + Environment.SetEnvironmentVariable("LAMBDA_TASK_ROOT", "AWS"); + Environment.SetEnvironmentVariable("POWERTOOLS_SERVICE_NAME", "POWERTOOLS"); + // Act - void Act() => handler.OnException(eventArgs, exception); + var segment = AWSXRayRecorder.Instance.TraceContext.GetEntity(); + var exception = Record.Exception(() => + { + _handler.HandleWithCaptureModeResponse(true); + }); + var subSegment = segment.Subsegments[0]; + // Assert - Assert.Throws((Action)Act); - recorder.DidNotReceive().AddMetadata( - Arg.Any(), - Arg.Any(), - Arg.Any() - ); + Assert.NotNull(exception); + Assert.True(segment.IsSubsegmentsAdded); + Assert.Single(segment.Subsegments); + Assert.False(subSegment.IsMetadataAdded); // no metadata for errors added } [Fact] public void OnException_WhenTracerCaptureModeIsDisabled_DoesNotCaptureError() { // Arrange - var methodName = Guid.NewGuid().ToString(); - var nameSpace = Guid.NewGuid().ToString(); - var configurations = Substitute.For(); - configurations.IsLambdaEnvironment.Returns(true); - configurations.TracingDisabled.Returns(false); - configurations.TracerCaptureError.Returns(false); - var recorder = Substitute.For(); - var exception = new Exception("Test Exception"); - - var handler = new TracingAspectHandler(null, nameSpace, TracingCaptureMode.Disabled, - configurations, recorder); - var eventArgs = new AspectEventArgs { Name = methodName }; - + Environment.SetEnvironmentVariable("LAMBDA_TASK_ROOT", "AWS"); + Environment.SetEnvironmentVariable("POWERTOOLS_SERVICE_NAME", "POWERTOOLS"); + // Act - void Act() => handler.OnException(eventArgs, exception); + var segment = AWSXRayRecorder.Instance.TraceContext.GetEntity(); + var exception = Record.Exception(() => + { + _handler.HandleWithCaptureModeDisabled(true); + }); + var subSegment = segment.Subsegments[0]; + // Assert - Assert.Throws((Action)Act); - recorder.DidNotReceive().AddMetadata( - Arg.Any(), - Arg.Any(), - Arg.Any() - ); + Assert.NotNull(exception); + Assert.True(segment.IsSubsegmentsAdded); + Assert.Single(segment.Subsegments); + Assert.False(subSegment.IsMetadataAdded); // no metadata for errors added } #endregion @@ -760,24 +671,30 @@ static string GetException(Exception exception) public void OnExit_WhenOutsideOfLambdaEnvironment_DoesNotEndSubsegment() { // Arrange - var methodName = Guid.NewGuid().ToString(); - var configurations = Substitute.For(); - configurations.IsLambdaEnvironment.Returns(false); - configurations.TracingDisabled.Returns(true); - configurations.IsSamLocal.Returns(false); - var recorder = Substitute.For(); - - var handler = new TracingAspectHandler(null, null, TracingCaptureMode.EnvironmentVariable, - configurations, recorder); - var eventArgs = new AspectEventArgs { Name = methodName }; - + + AWSXRayRecorder.Instance.BeginSegment("foo"); + // Act - handler.OnExit(eventArgs); - + _handler.Handle(); + + var segment = AWSXRayRecorder.Instance.TraceContext.GetEntity(); + // Assert - recorder.DidNotReceive().EndSubsegment(); + Assert.True(segment.IsInProgress); + Assert.False(segment.IsSubsegmentsAdded); + Assert.False(segment.IsAnnotationsAdded); } #endregion + + public void Dispose() + { + Environment.SetEnvironmentVariable("LAMBDA_TASK_ROOT", ""); + Environment.SetEnvironmentVariable("POWERTOOLS_SERVICE_NAME", ""); + Environment.SetEnvironmentVariable("POWERTOOLS_TRACER_CAPTURE_RESPONSE", ""); + Environment.SetEnvironmentVariable("POWERTOOLS_TRACER_CAPTURE_ERROR", ""); + Environment.SetEnvironmentVariable("POWERTOOLS_TRACE_DISABLED", ""); + 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 ce4a16a3..a332bbf1 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/XRayRecorderTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/XRayRecorderTests.cs @@ -8,6 +8,8 @@ namespace AWS.Lambda.Powertools.Tracing.Tests; +// This has to be the last tests to run otherwise it will keep state and fail other random tests +[Collection("Sequential")] public class XRayRecorderTests { [Fact] @@ -54,7 +56,7 @@ public void Tracing_Instance() } [Fact] - public void Tracing_Being_Subsegment() + public void Tracing_Begin_Subsegment() { // Arrange var conf = Substitute.For(); diff --git a/poetry.lock b/poetry.lock index a2ed41a1..4781ea53 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "click" @@ -475,18 +475,18 @@ watchmedo = ["PyYAML (>=3.10)"] [[package]] name = "zipp" -version = "3.11.0" +version = "3.19.1" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "zipp-3.11.0-py3-none-any.whl", hash = "sha256:83a28fcb75844b5c0cdaf5aa4003c2d728c77e05f5aeabe8e95e56727005fbaa"}, - {file = "zipp-3.11.0.tar.gz", hash = "sha256:a7a22e05929290a67401440b39690ae6563279bced5f314609d9d03798f56766"}, + {file = "zipp-3.19.1-py3-none-any.whl", hash = "sha256:2828e64edb5386ea6a52e7ba7cdb17bb30a73a858f5eb6eb93d8d36f5ea26091"}, + {file = "zipp-3.19.1.tar.gz", hash = "sha256:35427f6d5594f4acf82d25541438348c26736fa9b3afa2754bcd63cdb99d8e8f"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"] -testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] [metadata] lock-version = "2.0" diff --git a/version.json b/version.json index 9fb16922..fa635067 100644 --- a/version.json +++ b/version.json @@ -2,7 +2,7 @@ "Core": { "Logging": "1.5.1", "Metrics": "1.7.1", - "Tracing": "1.4.2" + "Tracing": "1.5.0" }, "Utilities": { "Parameters": "1.3.0",