diff --git a/libraries/src/AWS.Lambda.Powertools.Common/Core/Constants.cs b/libraries/src/AWS.Lambda.Powertools.Common/Core/Constants.cs index 343faa68d..456a50921 100644 --- a/libraries/src/AWS.Lambda.Powertools.Common/Core/Constants.cs +++ b/libraries/src/AWS.Lambda.Powertools.Common/Core/Constants.cs @@ -20,6 +20,14 @@ namespace AWS.Lambda.Powertools.Common; /// internal static class Constants { + /// + /// Constant for AWS_LAMBDA_INITIALIZATION_TYPE environment variable + /// This is used to determine if the Lambda function is running in provisioned concurrency mode + /// or not. If the value is "provisioned-concurrency", it indicates that the function is running in provisioned + /// concurrency mode. Otherwise, it is running in standard mode. + /// + internal const string AWSInitializationTypeEnv = "AWS_LAMBDA_INITIALIZATION_TYPE"; + /// /// Constant for POWERTOOLS_SERVICE_NAME environment variable /// diff --git a/libraries/src/AWS.Lambda.Powertools.Common/Core/IPowertoolsConfigurations.cs b/libraries/src/AWS.Lambda.Powertools.Common/Core/IPowertoolsConfigurations.cs index 58955a50d..755d33ef7 100644 --- a/libraries/src/AWS.Lambda.Powertools.Common/Core/IPowertoolsConfigurations.cs +++ b/libraries/src/AWS.Lambda.Powertools.Common/Core/IPowertoolsConfigurations.cs @@ -167,4 +167,15 @@ public interface IPowertoolsConfigurations /// Gets a value indicating whether Metrics are disabled. /// bool MetricsDisabled { get; } + + /// + /// Indicates if the current execution is a cold start. + /// + bool IsColdStart { get; } + + /// + /// AWS Lambda initialization type. + /// This is set to "on-demand" for on-demand Lambda functions and "provisioned-concurrency" for provisioned concurrency. + /// + string AwsInitializationType { get; } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Common/Core/LambdaLifecycleTracker.cs b/libraries/src/AWS.Lambda.Powertools.Common/Core/LambdaLifecycleTracker.cs new file mode 100644 index 000000000..c802bd21c --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Common/Core/LambdaLifecycleTracker.cs @@ -0,0 +1,66 @@ +using System; +using System.Threading; + +namespace AWS.Lambda.Powertools.Common.Core; + +/// +/// Tracks Lambda lifecycle state including cold starts +/// +internal static class LambdaLifecycleTracker +{ + // Static flag that's true only for the first Lambda container initialization + private static bool _isFirstContainer = true; + + // Store the cold start state for the current invocation + private static readonly AsyncLocal CurrentInvocationColdStart = new AsyncLocal(); + + private static string _lambdaInitType; + private static string LambdaInitType => _lambdaInitType ?? Environment.GetEnvironmentVariable(Constants.AWSInitializationTypeEnv); + + /// + /// Returns true if the current Lambda invocation is a cold start + /// + public static bool IsColdStart + { + get + { + if(LambdaInitType == "provisioned-concurrency") + { + // If the Lambda is provisioned concurrency, it is not a cold start + return false; + } + + // Initialize the cold start state for this invocation if not already set + if (!CurrentInvocationColdStart.Value.HasValue) + { + // Capture the container's cold start state for this entire invocation + CurrentInvocationColdStart.Value = _isFirstContainer; + + // After detecting the first invocation, mark future ones as warm + if (_isFirstContainer) + { + _isFirstContainer = false; + } + } + + // Return the cold start state for this invocation (cannot change during the invocation) + return CurrentInvocationColdStart.Value ?? false; + } + } + + + + /// + /// Resets the cold start state for testing + /// + /// Whether to reset the container state (defaults to true) + internal static void Reset(bool resetContainer = true) + { + if (resetContainer) + { + _isFirstContainer = true; + } + CurrentInvocationColdStart.Value = null; + _lambdaInitType = null; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsConfigurations.cs b/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsConfigurations.cs index 3933972d8..e57bb42ee 100644 --- a/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsConfigurations.cs +++ b/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsConfigurations.cs @@ -14,6 +14,7 @@ */ using System.Globalization; +using AWS.Lambda.Powertools.Common.Core; namespace AWS.Lambda.Powertools.Common; @@ -222,4 +223,11 @@ public void SetExecutionEnvironment(T type) /// public bool MetricsDisabled => GetEnvironmentVariableOrDefault(Constants.PowertoolsMetricsDisabledEnv, false); + + /// + public bool IsColdStart => LambdaLifecycleTracker.IsColdStart; + + /// + public string AwsInitializationType => + GetEnvironmentVariable(Constants.AWSInitializationTypeEnv); } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Common/InternalsVisibleTo.cs b/libraries/src/AWS.Lambda.Powertools.Common/InternalsVisibleTo.cs index 575d005f5..641de17cb 100644 --- a/libraries/src/AWS.Lambda.Powertools.Common/InternalsVisibleTo.cs +++ b/libraries/src/AWS.Lambda.Powertools.Common/InternalsVisibleTo.cs @@ -17,6 +17,7 @@ [assembly: InternalsVisibleTo("AWS.Lambda.Powertools.Logging")] [assembly: InternalsVisibleTo("AWS.Lambda.Powertools.Metrics")] +[assembly: InternalsVisibleTo("AWS.Lambda.Powertools.Tracing")] [assembly: InternalsVisibleTo("AWS.Lambda.Powertools.Idempotency")] [assembly: InternalsVisibleTo("AWS.Lambda.Powertools.Common.Tests")] [assembly: InternalsVisibleTo("AWS.Lambda.Powertools.Tracing.Tests")] diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs index c92566e27..9a4444050 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs @@ -21,6 +21,7 @@ using System.Text.Json; using AspectInjector.Broker; using AWS.Lambda.Powertools.Common; +using AWS.Lambda.Powertools.Common.Core; using AWS.Lambda.Powertools.Logging.Serializers; using Microsoft.Extensions.Logging; @@ -34,11 +35,6 @@ namespace AWS.Lambda.Powertools.Logging.Internal; [Aspect(Scope.Global, Factory = typeof(LoggingAspectFactory))] public class LoggingAspect { - /// - /// The is cold start - /// - private bool _isColdStart = true; - /// /// The initialize context /// @@ -143,9 +139,8 @@ public void OnEntry( if (!_initializeContext) return; - Logger.AppendKey(LoggingConstants.KeyColdStart, _isColdStart); + Logger.AppendKey(LoggingConstants.KeyColdStart, LambdaLifecycleTracker.IsColdStart); - _isColdStart = false; _initializeContext = false; _isContextInitialized = true; diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics/Internal/MetricsAspect.cs b/libraries/src/AWS.Lambda.Powertools.Metrics/Internal/MetricsAspect.cs index 177e90a98..4b336fbae 100644 --- a/libraries/src/AWS.Lambda.Powertools.Metrics/Internal/MetricsAspect.cs +++ b/libraries/src/AWS.Lambda.Powertools.Metrics/Internal/MetricsAspect.cs @@ -20,6 +20,7 @@ using Amazon.Lambda.Core; using AspectInjector.Broker; using AWS.Lambda.Powertools.Common; +using AWS.Lambda.Powertools.Common.Core; namespace AWS.Lambda.Powertools.Metrics; @@ -30,22 +31,12 @@ namespace AWS.Lambda.Powertools.Metrics; [Aspect(Scope.Global)] public class MetricsAspect { - /// - /// The is cold start - /// - private static bool _isColdStart; - /// /// Gets the metrics instance. /// /// The metrics instance. private static IMetrics _metricsInstance; - static MetricsAspect() - { - _isColdStart = true; - } - /// /// Runs before the execution of the method marked with the Metrics Attribute /// @@ -89,10 +80,9 @@ public void Before( Triggers = triggers }; - if (_isColdStart) + if (LambdaLifecycleTracker.IsColdStart) { _metricsInstance.CaptureColdStartMetric(GetContext(eventArgs)); - _isColdStart = false; } } @@ -112,7 +102,7 @@ public void Exit() internal static void ResetForTest() { _metricsInstance = null; - _isColdStart = true; + LambdaLifecycleTracker.Reset(); Metrics.ResetForTest(); } diff --git a/libraries/src/AWS.Lambda.Powertools.Tracing/Internal/TracingAspect.cs b/libraries/src/AWS.Lambda.Powertools.Tracing/Internal/TracingAspect.cs index 2e860ddd3..429aa8510 100644 --- a/libraries/src/AWS.Lambda.Powertools.Tracing/Internal/TracingAspect.cs +++ b/libraries/src/AWS.Lambda.Powertools.Tracing/Internal/TracingAspect.cs @@ -20,6 +20,7 @@ using System.Threading.Tasks; using AspectInjector.Broker; using AWS.Lambda.Powertools.Common; +using AWS.Lambda.Powertools.Common.Core; using AWS.Lambda.Powertools.Common.Utils; namespace AWS.Lambda.Powertools.Tracing.Internal; @@ -41,11 +42,6 @@ public class TracingAspect /// private readonly IXRayRecorder _xRayRecorder; - /// - /// If true, then is cold start - /// - private static bool _isColdStart = true; - /// /// If true, capture annotations /// @@ -148,7 +144,7 @@ private void BeginSegment(string segmentName, string @namespace) if (_captureAnnotations) { - _xRayRecorder.AddAnnotation("ColdStart", _isColdStart); + _xRayRecorder.AddAnnotation("ColdStart", LambdaLifecycleTracker.IsColdStart); _captureAnnotations = false; _isAnnotationsCaptured = true; @@ -156,8 +152,6 @@ private void BeginSegment(string segmentName, string @namespace) if (_powertoolsConfigurations.IsServiceDefined) _xRayRecorder.AddAnnotation("Service", _powertoolsConfigurations.Service); } - - _isColdStart = false; } private void HandleResponse(string name, object result, TracingCaptureMode captureMode, string @namespace) @@ -253,7 +247,7 @@ private bool CaptureError(TracingCaptureMode captureMode) internal static void ResetForTest() { - _isColdStart = true; + LambdaLifecycleTracker.Reset(); _captureAnnotations = true; } } \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Common.Tests/Core/LambdaLifecycleTrackerTests.cs b/libraries/tests/AWS.Lambda.Powertools.Common.Tests/Core/LambdaLifecycleTrackerTests.cs new file mode 100644 index 000000000..5b69f4578 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Common.Tests/Core/LambdaLifecycleTrackerTests.cs @@ -0,0 +1,105 @@ +using System; +using AWS.Lambda.Powertools.Common.Core; +using Xunit; + +namespace AWS.Lambda.Powertools.Common.Tests; + +public class LambdaLifecycleTrackerTests : IDisposable + { + public LambdaLifecycleTrackerTests() + { + // Reset before each test to ensure clean state + LambdaLifecycleTracker.Reset(); + Environment.SetEnvironmentVariable(Constants.AWSInitializationTypeEnv, null); + } + + public void Dispose() + { + // Reset after each test + LambdaLifecycleTracker.Reset(); + Environment.SetEnvironmentVariable(Constants.AWSInitializationTypeEnv, null); + } + + [Fact] + public void IsColdStart_FirstInvocation_ReturnsTrue() + { + // Act + var result = LambdaLifecycleTracker.IsColdStart; + + // Assert + Assert.True(result); + } + + [Fact] + public void IsColdStart_SecondInvocation_ReturnsFalse() + { + // Arrange - first access to trigger cold start + _ = LambdaLifecycleTracker.IsColdStart; + + // Clear just the AsyncLocal value to simulate new invocation in same container + LambdaLifecycleTracker.Reset(resetContainer: false); + + // Act - second invocation on same container + var result = LambdaLifecycleTracker.IsColdStart; + + // Assert + Assert.False(result); + } + + [Fact] + public void IsColdStart_WithProvisionedConcurrency_ReturnsFalse() + { + // Arrange + Environment.SetEnvironmentVariable(Constants.AWSInitializationTypeEnv, "provisioned-concurrency"); + + // Act + var result = LambdaLifecycleTracker.IsColdStart; + + // Assert + Assert.False(result); + } + + [Fact] + public void IsColdStart_ReturnsSameValueWithinInvocation() + { + // Act - access multiple times in the same invocation + var firstAccess = LambdaLifecycleTracker.IsColdStart; + var secondAccess = LambdaLifecycleTracker.IsColdStart; + var thirdAccess = LambdaLifecycleTracker.IsColdStart; + + // Assert + Assert.True(firstAccess); + Assert.Equal(firstAccess, secondAccess); + Assert.Equal(firstAccess, thirdAccess); + } + + [Fact] + public void Reset_ResetsState() + { + // Arrange + _ = LambdaLifecycleTracker.IsColdStart; // First invocation + + // Act + LambdaLifecycleTracker.Reset(); + var result = LambdaLifecycleTracker.IsColdStart; + + // Assert + Assert.True(result); // Should be true again after reset + } + + [Fact] + public void Reset_ClearsEnvironmentSetting() + { + // Arrange + Environment.SetEnvironmentVariable(Constants.AWSInitializationTypeEnv, "provisioned-concurrency"); + _ = LambdaLifecycleTracker.IsColdStart; // Load the environment variable + + // Act + LambdaLifecycleTracker.Reset(); + Environment.SetEnvironmentVariable(Constants.AWSInitializationTypeEnv, null); // Clear the environment + var result = LambdaLifecycleTracker.IsColdStart; + + // Assert + Assert.True(result); // Should be true when env var is cleared + } + } \ 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 f02619c1f..e33761360 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/TracingAttributeTest.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/TracingAttributeTest.cs @@ -17,6 +17,7 @@ using System.Linq; using System.Text; using Amazon.XRay.Recorder.Core; +using AWS.Lambda.Powertools.Common.Core; using AWS.Lambda.Powertools.Tracing.Internal; using Xunit; @@ -50,6 +51,8 @@ public void OnEntry_WhenFirstCall_CapturesColdStart() var subSegmentCold = segmentCold.Subsegments[0]; // Warm Start Execution + // Clear just the AsyncLocal value to simulate new invocation in same container + LambdaLifecycleTracker.Reset(resetContainer: false); // Start segment var segmentWarm = AWSXRayRecorder.Instance.TraceContext.GetEntity(); _handler.Handle(); @@ -87,6 +90,9 @@ public void OnEntry_WhenFirstCall_And_Service_Not_Set_CapturesColdStart() var subSegmentCold = segmentCold.Subsegments[0]; // Warm Start Execution + // Clear just the AsyncLocal value to simulate new invocation in same container + LambdaLifecycleTracker.Reset(resetContainer: false); + // Start segment var segmentWarm = AWSXRayRecorder.Instance.TraceContext.GetEntity(); _handler.Handle(); diff --git a/version.json b/version.json index e4d279449..9418e41b6 100644 --- a/version.json +++ b/version.json @@ -1,8 +1,8 @@ { "Core": { - "Logging": "1.6.5", - "Metrics": "2.0.0", - "Tracing": "1.6.1", + "Logging": "1.7.0", + "Metrics": "2.0.1", + "Tracing": "1.6.2", "Metrics.AspNetCore": "0.1.0" }, "Utilities": {