diff --git a/src/OpenTelemetry.Extensions.AWS/CHANGELOG.md b/src/OpenTelemetry.Extensions.AWS/CHANGELOG.md index b5bbdab22c..650d066b9e 100644 --- a/src/OpenTelemetry.Extensions.AWS/CHANGELOG.md +++ b/src/OpenTelemetry.Extensions.AWS/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +* Add support for Lambda lineage in X-Ray propagator. + ([#2480](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/2480)) * Updated OpenTelemetry core component version(s) to `1.11.1`. ([#2477](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/2477)) diff --git a/src/OpenTelemetry.Extensions.AWS/Trace/AWSXRayPropagator.cs b/src/OpenTelemetry.Extensions.AWS/Trace/AWSXRayPropagator.cs index 2a8d71b6f2..1822a2768c 100644 --- a/src/OpenTelemetry.Extensions.AWS/Trace/AWSXRayPropagator.cs +++ b/src/OpenTelemetry.Extensions.AWS/Trace/AWSXRayPropagator.cs @@ -8,6 +8,7 @@ using System.Globalization; using System.Numerics; using System.Text; +using System.Text.RegularExpressions; using OpenTelemetry.Context.Propagation; namespace OpenTelemetry.Extensions.AWS.Trace; @@ -37,6 +38,12 @@ public class AWSXRayPropagator : TextMapPropagator private const char SampledValue = '1'; private const char NotSampledValue = '0'; + private const string LineageKey = "Lineage"; + private const char LineageDelimiter = ':'; + private const int LineageHashLength = 8; + private const int LineageMaxCounter1 = 32767; + private const int LineageMaxCounter2 = 255; + /// public override ISet Fields => new HashSet() { AWSXRayTraceHeaderKey }; @@ -71,7 +78,7 @@ public override PropagationContext Extract(PropagationContext context, T carr var parentHeader = parentTraceHeader.First(); - return !TryParseXRayTraceHeader(parentHeader, out var newActivityContext) ? context : new PropagationContext(newActivityContext, context.Baggage); + return !TryParseXRayTraceHeader(context, parentHeader, out var newActivityContext, out var baggage) ? context : new PropagationContext(newActivityContext, baggage); } catch (Exception ex) { @@ -143,10 +150,21 @@ public override void Inject(PropagationContext context, T carrier, Action traceId = default; ReadOnlySpan parentId = default; char traceOptions = default; + baggage = context.Baggage; if (string.IsNullOrEmpty(rawHeader)) { @@ -212,6 +231,13 @@ internal static bool TryParseXRayTraceHeader(string rawHeader, out ActivityConte traceOptions = sampleDecision; } + else if (trimmedPart.StartsWith(LineageKey.AsSpan())) + { + if (IsLineageIdValid(value.ToString())) + { + baggage = baggage.SetBaggage(LineageKey, value.ToString()); + } + } } if (traceId.IsEmpty || parentId.IsEmpty || traceOptions == default) @@ -306,6 +332,33 @@ internal static bool TryParseSampleDecision(ReadOnlySpan sampleDecision, o return true; } + internal static bool IsLineageIdValid(string lineageId) + { + var lineageSubstrings = lineageId.Split(LineageDelimiter); + if (lineageSubstrings.Length != 3) + { + return false; + } + + if (!int.TryParse(lineageSubstrings[0], out var lineageCounter1)) + { + return false; + } + + var hashedString = lineageSubstrings[1]; + + if (!int.TryParse(lineageSubstrings[2], out var lineageCounter2)) + { + return false; + } + + var isValidHash = hashedString.Length == LineageHashLength && Regex.IsMatch(hashedString, "^[0-9a-fA-F]+$"); + var isValidCounter1 = lineageCounter1 >= 0 && lineageCounter1 <= LineageMaxCounter1; + var isValidCounter2 = lineageCounter2 >= 0 && lineageCounter2 <= LineageMaxCounter2; + + return isValidHash && isValidCounter1 && isValidCounter2; + } + internal static string ToXRayTraceIdFormat(string traceId) { var sb = new StringBuilder(); diff --git a/test/OpenTelemetry.Extensions.AWS.Tests/Trace/AWSXRayPropagatorTests.cs b/test/OpenTelemetry.Extensions.AWS.Tests/Trace/AWSXRayPropagatorTests.cs index cd036b27ba..70ad46ff90 100644 --- a/test/OpenTelemetry.Extensions.AWS.Tests/Trace/AWSXRayPropagatorTests.cs +++ b/test/OpenTelemetry.Extensions.AWS.Tests/Trace/AWSXRayPropagatorTests.cs @@ -16,6 +16,7 @@ public class AWSXRayPropagatorTests private const string AWSXRayTraceHeaderKey = "X-Amzn-Trace-Id"; private const string TraceId = "5759e988bd862e3fe1be46a994272793"; private const string ParentId = "53995c3f42cd8ad8"; + private const string LineageId = "100:e3b0c442:11"; private static readonly string[] Empty = []; @@ -114,6 +115,21 @@ public void TestInjectTraceHeaderAlreadyExistsButNotHttpRequestMessage() Assert.Equal("Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=0", carrier[AWSXRayTraceHeaderKey]); } + [Fact] + public void TestInjectTraceHeaderWithLineage() + { + var carrier = new Dictionary(); + var traceId = ActivityTraceId.CreateFromString(TraceId.AsSpan()); + var parentId = ActivitySpanId.CreateFromString(ParentId.AsSpan()); + var traceFlags = ActivityTraceFlags.Recorded; + var activityContext = new ActivityContext(traceId, parentId, traceFlags); + var baggage = Baggage.Create(new Dictionary { { "Lineage", LineageId } }); + this.awsXRayPropagator.Inject(new PropagationContext(activityContext, baggage), carrier, Setter); + + Assert.True(carrier.ContainsKey(AWSXRayTraceHeaderKey)); + Assert.Equal("Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1;Lineage=100:e3b0c442:11", carrier[AWSXRayTraceHeaderKey]); + } + [Fact] public void TestExtractTraceHeader() { @@ -159,6 +175,53 @@ public void TestExtractTraceHeaderDifferentOrder() Assert.Equal(new PropagationContext(activityContext, default), this.awsXRayPropagator.Extract(default, carrier, Getter)); } + [Fact] + public void TestExtractTraceHeaderWithLineage() + { + var carrier = new Dictionary() + { + { AWSXRayTraceHeaderKey, "Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1;Lineage=100:e3b0c442:11" }, + }; + var traceId = ActivityTraceId.CreateFromString(TraceId.AsSpan()); + var parentId = ActivitySpanId.CreateFromString(ParentId.AsSpan()); + var traceFlags = ActivityTraceFlags.Recorded; + var activityContext = new ActivityContext(traceId, parentId, traceFlags, isRemote: true); + var baggage = Baggage.Create(new Dictionary { { "Lineage", LineageId } }); + + Assert.Equal(new PropagationContext(activityContext, baggage), this.awsXRayPropagator.Extract(default, carrier, Getter)); + } + + [Fact] + public void TestExtractTraceHeaderWithInvalidLineage() + { + var invalidLineages = new List + { + string.Empty, + "::", + "1::", + "1::1", + "1:badc0de:13", + ":fbadc0de:13", + "32768:fbadc0de:1", + "1:fbadc0de:256", + }; + + foreach (var invalidLineage in invalidLineages) + { + var carrier = new Dictionary() + { + { AWSXRayTraceHeaderKey, $"Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1;Lineage={invalidLineage}" }, + }; + + var traceId = ActivityTraceId.CreateFromString(TraceId.AsSpan()); + var parentId = ActivitySpanId.CreateFromString(ParentId.AsSpan()); + var traceFlags = ActivityTraceFlags.Recorded; + var activityContext = new ActivityContext(traceId, parentId, traceFlags, isRemote: true); + + Assert.Equal(new PropagationContext(activityContext, default), this.awsXRayPropagator.Extract(default, carrier, Getter)); + } + } + [Fact] public void TestExtractTraceHeaderWithEmptyValue() {