Skip to content

Commit 7754dd4

Browse files
authored
fix(common): graceful fallback when LambdaTraceProvider is unavailable (#1176)
1 parent 1b0c8c6 commit 7754dd4

2 files changed

Lines changed: 158 additions & 2 deletions

File tree

libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsConfigurations.cs

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
using System;
12
using System.Globalization;
3+
using System.Runtime.CompilerServices;
4+
using System.Threading;
25
using Amazon.Lambda.Core;
36
using AWS.Lambda.Powertools.Common.Core;
47

@@ -28,6 +31,13 @@ public class PowertoolsConfigurations : IPowertoolsConfigurations
2831
/// </summary>
2932
private static IPowertoolsConfigurations _instance;
3033

34+
/// <summary>
35+
/// Whether LambdaTraceProvider is available in the loaded Amazon.Lambda.Core assembly.
36+
/// 0 = not yet checked, 1 = available, -1 = unavailable.
37+
/// Stored as int for atomic reads/writes via Volatile.
38+
/// </summary>
39+
private static int _traceProviderState; // 0 = unknown, 1 = available, -1 = unavailable
40+
3141
/// <summary>
3242
/// Initializes a new instance of the <see cref="PowertoolsConfigurations" /> class.
3343
/// </summary>
@@ -165,10 +175,12 @@ public bool GetEnvironmentVariableOrDefault(string variable, bool defaultValue)
165175

166176
/// <summary>
167177
/// Gets the X-Ray trace identifier.
178+
/// Uses LambdaTraceProvider.CurrentTraceId when available (Amazon.Lambda.Core >= 2.8.0)
179+
/// for correct trace ID isolation in concurrent Lambda executions (LMI).
180+
/// Falls back to the _X_AMZN_TRACE_ID environment variable for older runtimes.
168181
/// </summary>
169182
/// <value>The X-Ray trace identifier.</value>
170-
public string XRayTraceId =>
171-
LambdaTraceProvider.CurrentTraceId;
183+
public string XRayTraceId => GetTraceId(() => GetEnvironmentVariable(Constants.XrayTraceIdEnv));
172184

173185
/// <summary>
174186
/// Gets a value indicating whether this instance is Lambda.
@@ -212,4 +224,39 @@ public bool GetEnvironmentVariableOrDefault(string variable, bool defaultValue)
212224
/// <inheritdoc />
213225
public string AwsInitializationType =>
214226
GetEnvironmentVariable(Constants.AWSInitializationTypeEnv);
227+
228+
private static string GetTraceId(Func<string> fallback)
229+
{
230+
var state = Volatile.Read(ref _traceProviderState);
231+
232+
if (state == 1)
233+
return GetTraceIdFromProvider();
234+
235+
if (state == -1)
236+
return fallback();
237+
238+
// First call — probe whether LambdaTraceProvider exists in the loaded runtime
239+
try
240+
{
241+
var traceId = GetTraceIdFromProvider();
242+
Volatile.Write(ref _traceProviderState, 1);
243+
return traceId;
244+
}
245+
catch (TypeLoadException)
246+
{
247+
Volatile.Write(ref _traceProviderState, -1);
248+
return fallback();
249+
}
250+
}
251+
252+
/// <summary>
253+
/// Isolated call to LambdaTraceProvider.CurrentTraceId.
254+
/// Must not be inlined so that the TypeLoadException is thrown only
255+
/// when this method is invoked, not when the caller is compiled.
256+
/// </summary>
257+
[MethodImpl(MethodImplOptions.NoInlining)]
258+
private static string GetTraceIdFromProvider()
259+
{
260+
return LambdaTraceProvider.CurrentTraceId;
261+
}
215262
}

libraries/tests/AWS.Lambda.Powertools.Common.Tests/Core/PowertoolsConfigurationsTest.cs

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -534,5 +534,114 @@ public void IsLambdaEnvironment_WhenEnvironmentHasValue_ReturnsTrue()
534534
}
535535

536536
#endregion
537+
538+
#region XRayTraceId Tests
539+
540+
[Fact]
541+
public void XRayTraceId_WhenLambdaTraceProviderAvailable_ReturnsTraceId()
542+
{
543+
ResetTraceProviderState();
544+
545+
try
546+
{
547+
// Arrange
548+
var environment = Substitute.For<IPowertoolsEnvironment>();
549+
var configurations = new PowertoolsConfigurations(environment);
550+
551+
// Act - LambdaTraceProvider is available in test env (Amazon.Lambda.Core 2.8.0)
552+
// Returns null/empty in test env (no active Lambda trace), but should not throw
553+
var result = configurations.XRayTraceId;
554+
555+
// Assert - should not fall back to env var (provider was used instead)
556+
environment.DidNotReceive()
557+
.GetEnvironmentVariable(Arg.Is<string>(i => i == Constants.XrayTraceIdEnv));
558+
}
559+
finally
560+
{
561+
ResetTraceProviderState();
562+
}
563+
}
564+
565+
[Fact]
566+
public void XRayTraceId_WhenLambdaTraceProviderUnavailable_FallsBackToEnvironmentVariable()
567+
{
568+
ResetTraceProviderState();
569+
570+
try
571+
{
572+
// Arrange
573+
var traceId = "Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1";
574+
var environment = Substitute.For<IPowertoolsEnvironment>();
575+
environment.GetEnvironmentVariable(Constants.XrayTraceIdEnv).Returns(traceId);
576+
var configurations = new PowertoolsConfigurations(environment);
577+
578+
// Simulate LambdaTraceProvider not being available (older Amazon.Lambda.Core)
579+
SetTraceProviderState(-1);
580+
581+
// Act
582+
var result = configurations.XRayTraceId;
583+
584+
// Assert
585+
environment.Received(1)
586+
.GetEnvironmentVariable(Arg.Is<string>(i => i == Constants.XrayTraceIdEnv));
587+
Assert.Equal(traceId, result);
588+
}
589+
finally
590+
{
591+
ResetTraceProviderState();
592+
}
593+
}
594+
595+
[Fact]
596+
public void XRayTraceId_WhenProviderCachedUnavailable_UsesEnvVarOnSubsequentCalls()
597+
{
598+
ResetTraceProviderState();
599+
600+
try
601+
{
602+
// Arrange
603+
var environment = Substitute.For<IPowertoolsEnvironment>();
604+
var configurations = new PowertoolsConfigurations(environment);
605+
606+
// Simulate LambdaTraceProvider not being available (cached)
607+
SetTraceProviderState(-1);
608+
609+
var traceId1 = "Root=1-aaa;Parent=bbb;Sampled=1";
610+
var traceId2 = "Root=1-ccc;Parent=ddd;Sampled=1";
611+
environment.GetEnvironmentVariable(Constants.XrayTraceIdEnv).Returns(traceId1, traceId2);
612+
613+
// Act
614+
var result1 = configurations.XRayTraceId;
615+
var result2 = configurations.XRayTraceId;
616+
617+
// Assert - should go straight to env var on both calls (cached unavailable)
618+
environment.Received(2)
619+
.GetEnvironmentVariable(Arg.Is<string>(i => i == Constants.XrayTraceIdEnv));
620+
Assert.Equal(traceId1, result1);
621+
Assert.Equal(traceId2, result2);
622+
}
623+
finally
624+
{
625+
ResetTraceProviderState();
626+
}
627+
}
628+
629+
private static void ResetTraceProviderState()
630+
{
631+
var field = typeof(PowertoolsConfigurations).GetField("_traceProviderState",
632+
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);
633+
Assert.NotNull(field);
634+
field.SetValue(null, 0);
635+
}
636+
637+
private static void SetTraceProviderState(int state)
638+
{
639+
var field = typeof(PowertoolsConfigurations).GetField("_traceProviderState",
640+
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);
641+
Assert.NotNull(field);
642+
field.SetValue(null, state);
643+
}
644+
645+
#endregion
537646
}
538647
}

0 commit comments

Comments
 (0)