From db957fc0790418184273ea60c9501b70244aedc8 Mon Sep 17 00:00:00 2001
From: Henrique Graca <999396+hjgraca@users.noreply.github.com>
Date: Fri, 10 Apr 2026 13:10:30 +0100
Subject: [PATCH 1/3] fix(common): graceful fallback when LambdaTraceProvider
is unavailable (#1173)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
XRayTraceId now probes for LambdaTraceProvider.CurrentTraceId on first
access and caches the result. When the type is not available (customers
on Amazon.Lambda.Core < 2.8.0), it falls back to reading the
_X_AMZN_TRACE_ID environment variable — matching pre-3.1.0 behaviour —
instead of throwing a TypeLoadException at runtime.
Closes #1173
---
.../Core/PowertoolsConfigurations.cs | 47 ++++++++++-
.../Core/PowertoolsConfigurationsTest.cs | 82 +++++++++++++++++++
2 files changed, 127 insertions(+), 2 deletions(-)
diff --git a/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsConfigurations.cs b/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsConfigurations.cs
index c13710131..745981f6f 100644
--- a/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsConfigurations.cs
+++ b/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsConfigurations.cs
@@ -1,4 +1,6 @@
+using System;
using System.Globalization;
+using System.Runtime.CompilerServices;
using Amazon.Lambda.Core;
using AWS.Lambda.Powertools.Common.Core;
@@ -28,6 +30,12 @@ public class PowertoolsConfigurations : IPowertoolsConfigurations
///
private static IPowertoolsConfigurations _instance;
+ ///
+ /// Whether LambdaTraceProvider is available in the loaded Amazon.Lambda.Core assembly.
+ /// Null means not yet checked, true/false is the cached result.
+ ///
+ private static bool? _isTraceProviderAvailable;
+
///
/// Initializes a new instance of the class.
///
@@ -165,10 +173,12 @@ public bool GetEnvironmentVariableOrDefault(string variable, bool defaultValue)
///
/// Gets the X-Ray trace identifier.
+ /// Uses LambdaTraceProvider.CurrentTraceId when available (Amazon.Lambda.Core >= 2.8.0)
+ /// for correct trace ID isolation in concurrent Lambda executions (LMI).
+ /// Falls back to the _X_AMZN_TRACE_ID environment variable for older runtimes.
///
/// The X-Ray trace identifier.
- public string XRayTraceId =>
- LambdaTraceProvider.CurrentTraceId;
+ public string XRayTraceId => GetTraceId();
///
/// Gets a value indicating whether this instance is Lambda.
@@ -212,4 +222,37 @@ public bool GetEnvironmentVariableOrDefault(string variable, bool defaultValue)
///
public string AwsInitializationType =>
GetEnvironmentVariable(Constants.AWSInitializationTypeEnv);
+
+ private string GetTraceId()
+ {
+ if (_isTraceProviderAvailable == true)
+ return GetTraceIdFromProvider();
+
+ if (_isTraceProviderAvailable == false)
+ return GetEnvironmentVariable(Constants.XrayTraceIdEnv);
+
+ // First call — probe whether LambdaTraceProvider exists in the loaded runtime
+ try
+ {
+ var traceId = GetTraceIdFromProvider();
+ _isTraceProviderAvailable = true;
+ return traceId;
+ }
+ catch (TypeLoadException)
+ {
+ _isTraceProviderAvailable = false;
+ return GetEnvironmentVariable(Constants.XrayTraceIdEnv);
+ }
+ }
+
+ ///
+ /// Isolated call to LambdaTraceProvider.CurrentTraceId.
+ /// Must not be inlined so that the TypeLoadException is thrown only
+ /// when this method is invoked, not when the caller is compiled.
+ ///
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private static string GetTraceIdFromProvider()
+ {
+ return LambdaTraceProvider.CurrentTraceId;
+ }
}
\ No newline at end of file
diff --git a/libraries/tests/AWS.Lambda.Powertools.Common.Tests/Core/PowertoolsConfigurationsTest.cs b/libraries/tests/AWS.Lambda.Powertools.Common.Tests/Core/PowertoolsConfigurationsTest.cs
index 5e4503072..ed5da8e79 100644
--- a/libraries/tests/AWS.Lambda.Powertools.Common.Tests/Core/PowertoolsConfigurationsTest.cs
+++ b/libraries/tests/AWS.Lambda.Powertools.Common.Tests/Core/PowertoolsConfigurationsTest.cs
@@ -534,5 +534,87 @@ public void IsLambdaEnvironment_WhenEnvironmentHasValue_ReturnsTrue()
}
#endregion
+
+ #region XRayTraceId Tests
+
+ [Fact]
+ public void XRayTraceId_WhenLambdaTraceProviderAvailable_ReturnsTraceId()
+ {
+ // Arrange
+ var environment = Substitute.For();
+ var configurations = new PowertoolsConfigurations(environment);
+
+ // Reset cached state so the probe runs fresh
+ ResetTraceProviderState();
+
+ // Act - LambdaTraceProvider is available in test env (Amazon.Lambda.Core 2.8.0)
+ var result = configurations.XRayTraceId;
+
+ // Assert - should not throw and should not fall back to env var
+ environment.DidNotReceive()
+ .GetEnvironmentVariable(Arg.Is(i => i == Constants.XrayTraceIdEnv));
+ }
+
+ [Fact]
+ public void XRayTraceId_WhenLambdaTraceProviderUnavailable_FallsBackToEnvironmentVariable()
+ {
+ // Arrange
+ var traceId = "Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1";
+ var environment = Substitute.For();
+ environment.GetEnvironmentVariable(Constants.XrayTraceIdEnv).Returns(traceId);
+ var configurations = new PowertoolsConfigurations(environment);
+
+ // Simulate LambdaTraceProvider not being available (older Amazon.Lambda.Core)
+ SetTraceProviderAvailable(false);
+
+ // Act
+ var result = configurations.XRayTraceId;
+
+ // Assert
+ environment.Received(1)
+ .GetEnvironmentVariable(Arg.Is(i => i == Constants.XrayTraceIdEnv));
+ Assert.Equal(traceId, result);
+ }
+
+ [Fact]
+ public void XRayTraceId_CachesTraceProviderAvailability_OnSubsequentCalls()
+ {
+ // Arrange
+ var environment = Substitute.For();
+ var configurations = new PowertoolsConfigurations(environment);
+
+ // Simulate LambdaTraceProvider not being available
+ SetTraceProviderAvailable(false);
+
+ var traceId1 = "Root=1-aaa;Parent=bbb;Sampled=1";
+ var traceId2 = "Root=1-ccc;Parent=ddd;Sampled=1";
+ environment.GetEnvironmentVariable(Constants.XrayTraceIdEnv).Returns(traceId1, traceId2);
+
+ // Act
+ var result1 = configurations.XRayTraceId;
+ var result2 = configurations.XRayTraceId;
+
+ // Assert - should go straight to env var on second call (cached false)
+ environment.Received(2)
+ .GetEnvironmentVariable(Arg.Is(i => i == Constants.XrayTraceIdEnv));
+ Assert.Equal(traceId1, result1);
+ Assert.Equal(traceId2, result2);
+ }
+
+ private static void ResetTraceProviderState()
+ {
+ var field = typeof(PowertoolsConfigurations).GetField("_isTraceProviderAvailable",
+ System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);
+ field?.SetValue(null, null);
+ }
+
+ private static void SetTraceProviderAvailable(bool available)
+ {
+ var field = typeof(PowertoolsConfigurations).GetField("_isTraceProviderAvailable",
+ System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);
+ field?.SetValue(null, (bool?)available);
+ }
+
+ #endregion
}
}
\ No newline at end of file
From ba1859c7185245cbbf47f2556e56a987f2d64193 Mon Sep 17 00:00:00 2001
From: Henrique Graca <999396+hjgraca@users.noreply.github.com>
Date: Fri, 10 Apr 2026 13:15:35 +0100
Subject: [PATCH 2/3] fix: make GetTraceId static to satisfy Sonar S2696
Instance method was setting a static field. Refactored to accept a
Func fallback so the method can be fully static.
---
.../Core/PowertoolsConfigurations.cs | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsConfigurations.cs b/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsConfigurations.cs
index 745981f6f..8b22b3e7a 100644
--- a/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsConfigurations.cs
+++ b/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsConfigurations.cs
@@ -178,7 +178,7 @@ public bool GetEnvironmentVariableOrDefault(string variable, bool defaultValue)
/// Falls back to the _X_AMZN_TRACE_ID environment variable for older runtimes.
///
/// The X-Ray trace identifier.
- public string XRayTraceId => GetTraceId();
+ public string XRayTraceId => GetTraceId(() => GetEnvironmentVariable(Constants.XrayTraceIdEnv));
///
/// Gets a value indicating whether this instance is Lambda.
@@ -223,13 +223,13 @@ public bool GetEnvironmentVariableOrDefault(string variable, bool defaultValue)
public string AwsInitializationType =>
GetEnvironmentVariable(Constants.AWSInitializationTypeEnv);
- private string GetTraceId()
+ private static string GetTraceId(Func fallback)
{
if (_isTraceProviderAvailable == true)
return GetTraceIdFromProvider();
if (_isTraceProviderAvailable == false)
- return GetEnvironmentVariable(Constants.XrayTraceIdEnv);
+ return fallback();
// First call — probe whether LambdaTraceProvider exists in the loaded runtime
try
@@ -241,7 +241,7 @@ private string GetTraceId()
catch (TypeLoadException)
{
_isTraceProviderAvailable = false;
- return GetEnvironmentVariable(Constants.XrayTraceIdEnv);
+ return fallback();
}
}
From 1f245d6ecbbe4ee75a75f95cff811afdd305c11f Mon Sep 17 00:00:00 2001
From: Henrique Graca <999396+hjgraca@users.noreply.github.com>
Date: Fri, 10 Apr 2026 13:21:13 +0100
Subject: [PATCH 3/3] fix: address Copilot review feedback
- Thread safety: replace bool? with int + Volatile.Read/Write for
atomic access to _traceProviderState across concurrent invocations
- Tests: add try/finally with ResetTraceProviderState to prevent
cross-test contamination of static state
- Tests: Assert.NotNull on reflection field lookups so tests fail
loudly if the field is renamed
- Tests: rename caching test to clarify it covers cached-unavailable
path, not the initial probe
- Tests: remove invalid assertion on return value (LambdaTraceProvider
returns null in test env with no active trace)
---
.../Core/PowertoolsConfigurations.cs | 16 ++-
.../Core/PowertoolsConfigurationsTest.cs | 129 +++++++++++-------
2 files changed, 88 insertions(+), 57 deletions(-)
diff --git a/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsConfigurations.cs b/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsConfigurations.cs
index 8b22b3e7a..d5bb32a3e 100644
--- a/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsConfigurations.cs
+++ b/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsConfigurations.cs
@@ -1,6 +1,7 @@
using System;
using System.Globalization;
using System.Runtime.CompilerServices;
+using System.Threading;
using Amazon.Lambda.Core;
using AWS.Lambda.Powertools.Common.Core;
@@ -32,9 +33,10 @@ public class PowertoolsConfigurations : IPowertoolsConfigurations
///
/// Whether LambdaTraceProvider is available in the loaded Amazon.Lambda.Core assembly.
- /// Null means not yet checked, true/false is the cached result.
+ /// 0 = not yet checked, 1 = available, -1 = unavailable.
+ /// Stored as int for atomic reads/writes via Volatile.
///
- private static bool? _isTraceProviderAvailable;
+ private static int _traceProviderState; // 0 = unknown, 1 = available, -1 = unavailable
///
/// Initializes a new instance of the class.
@@ -225,22 +227,24 @@ public bool GetEnvironmentVariableOrDefault(string variable, bool defaultValue)
private static string GetTraceId(Func fallback)
{
- if (_isTraceProviderAvailable == true)
+ var state = Volatile.Read(ref _traceProviderState);
+
+ if (state == 1)
return GetTraceIdFromProvider();
- if (_isTraceProviderAvailable == false)
+ if (state == -1)
return fallback();
// First call — probe whether LambdaTraceProvider exists in the loaded runtime
try
{
var traceId = GetTraceIdFromProvider();
- _isTraceProviderAvailable = true;
+ Volatile.Write(ref _traceProviderState, 1);
return traceId;
}
catch (TypeLoadException)
{
- _isTraceProviderAvailable = false;
+ Volatile.Write(ref _traceProviderState, -1);
return fallback();
}
}
diff --git a/libraries/tests/AWS.Lambda.Powertools.Common.Tests/Core/PowertoolsConfigurationsTest.cs b/libraries/tests/AWS.Lambda.Powertools.Common.Tests/Core/PowertoolsConfigurationsTest.cs
index ed5da8e79..784cda2f3 100644
--- a/libraries/tests/AWS.Lambda.Powertools.Common.Tests/Core/PowertoolsConfigurationsTest.cs
+++ b/libraries/tests/AWS.Lambda.Powertools.Common.Tests/Core/PowertoolsConfigurationsTest.cs
@@ -540,79 +540,106 @@ public void IsLambdaEnvironment_WhenEnvironmentHasValue_ReturnsTrue()
[Fact]
public void XRayTraceId_WhenLambdaTraceProviderAvailable_ReturnsTraceId()
{
- // Arrange
- var environment = Substitute.For();
- var configurations = new PowertoolsConfigurations(environment);
-
- // Reset cached state so the probe runs fresh
ResetTraceProviderState();
- // Act - LambdaTraceProvider is available in test env (Amazon.Lambda.Core 2.8.0)
- var result = configurations.XRayTraceId;
-
- // Assert - should not throw and should not fall back to env var
- environment.DidNotReceive()
- .GetEnvironmentVariable(Arg.Is(i => i == Constants.XrayTraceIdEnv));
+ try
+ {
+ // Arrange
+ var environment = Substitute.For();
+ var configurations = new PowertoolsConfigurations(environment);
+
+ // Act - LambdaTraceProvider is available in test env (Amazon.Lambda.Core 2.8.0)
+ // Returns null/empty in test env (no active Lambda trace), but should not throw
+ var result = configurations.XRayTraceId;
+
+ // Assert - should not fall back to env var (provider was used instead)
+ environment.DidNotReceive()
+ .GetEnvironmentVariable(Arg.Is(i => i == Constants.XrayTraceIdEnv));
+ }
+ finally
+ {
+ ResetTraceProviderState();
+ }
}
[Fact]
public void XRayTraceId_WhenLambdaTraceProviderUnavailable_FallsBackToEnvironmentVariable()
{
- // Arrange
- var traceId = "Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1";
- var environment = Substitute.For();
- environment.GetEnvironmentVariable(Constants.XrayTraceIdEnv).Returns(traceId);
- var configurations = new PowertoolsConfigurations(environment);
-
- // Simulate LambdaTraceProvider not being available (older Amazon.Lambda.Core)
- SetTraceProviderAvailable(false);
-
- // Act
- var result = configurations.XRayTraceId;
+ ResetTraceProviderState();
- // Assert
- environment.Received(1)
- .GetEnvironmentVariable(Arg.Is(i => i == Constants.XrayTraceIdEnv));
- Assert.Equal(traceId, result);
+ try
+ {
+ // Arrange
+ var traceId = "Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1";
+ var environment = Substitute.For();
+ environment.GetEnvironmentVariable(Constants.XrayTraceIdEnv).Returns(traceId);
+ var configurations = new PowertoolsConfigurations(environment);
+
+ // Simulate LambdaTraceProvider not being available (older Amazon.Lambda.Core)
+ SetTraceProviderState(-1);
+
+ // Act
+ var result = configurations.XRayTraceId;
+
+ // Assert
+ environment.Received(1)
+ .GetEnvironmentVariable(Arg.Is(i => i == Constants.XrayTraceIdEnv));
+ Assert.Equal(traceId, result);
+ }
+ finally
+ {
+ ResetTraceProviderState();
+ }
}
[Fact]
- public void XRayTraceId_CachesTraceProviderAvailability_OnSubsequentCalls()
+ public void XRayTraceId_WhenProviderCachedUnavailable_UsesEnvVarOnSubsequentCalls()
{
- // Arrange
- var environment = Substitute.For();
- var configurations = new PowertoolsConfigurations(environment);
-
- // Simulate LambdaTraceProvider not being available
- SetTraceProviderAvailable(false);
-
- var traceId1 = "Root=1-aaa;Parent=bbb;Sampled=1";
- var traceId2 = "Root=1-ccc;Parent=ddd;Sampled=1";
- environment.GetEnvironmentVariable(Constants.XrayTraceIdEnv).Returns(traceId1, traceId2);
+ ResetTraceProviderState();
- // Act
- var result1 = configurations.XRayTraceId;
- var result2 = configurations.XRayTraceId;
-
- // Assert - should go straight to env var on second call (cached false)
- environment.Received(2)
- .GetEnvironmentVariable(Arg.Is(i => i == Constants.XrayTraceIdEnv));
- Assert.Equal(traceId1, result1);
- Assert.Equal(traceId2, result2);
+ try
+ {
+ // Arrange
+ var environment = Substitute.For();
+ var configurations = new PowertoolsConfigurations(environment);
+
+ // Simulate LambdaTraceProvider not being available (cached)
+ SetTraceProviderState(-1);
+
+ var traceId1 = "Root=1-aaa;Parent=bbb;Sampled=1";
+ var traceId2 = "Root=1-ccc;Parent=ddd;Sampled=1";
+ environment.GetEnvironmentVariable(Constants.XrayTraceIdEnv).Returns(traceId1, traceId2);
+
+ // Act
+ var result1 = configurations.XRayTraceId;
+ var result2 = configurations.XRayTraceId;
+
+ // Assert - should go straight to env var on both calls (cached unavailable)
+ environment.Received(2)
+ .GetEnvironmentVariable(Arg.Is(i => i == Constants.XrayTraceIdEnv));
+ Assert.Equal(traceId1, result1);
+ Assert.Equal(traceId2, result2);
+ }
+ finally
+ {
+ ResetTraceProviderState();
+ }
}
private static void ResetTraceProviderState()
{
- var field = typeof(PowertoolsConfigurations).GetField("_isTraceProviderAvailable",
+ var field = typeof(PowertoolsConfigurations).GetField("_traceProviderState",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);
- field?.SetValue(null, null);
+ Assert.NotNull(field);
+ field.SetValue(null, 0);
}
- private static void SetTraceProviderAvailable(bool available)
+ private static void SetTraceProviderState(int state)
{
- var field = typeof(PowertoolsConfigurations).GetField("_isTraceProviderAvailable",
+ var field = typeof(PowertoolsConfigurations).GetField("_traceProviderState",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);
- field?.SetValue(null, (bool?)available);
+ Assert.NotNull(field);
+ field.SetValue(null, state);
}
#endregion