From 61c6062f3fb5e52c9b2be001d66b61c260c46ef1 Mon Sep 17 00:00:00 2001 From: martincostello Date: Fri, 15 May 2026 17:08:10 +0100 Subject: [PATCH 1/6] [API] Add support for W3C randomness flag Update `TraceContextPropagator` to handle the W3C randomness flag. Contributes to #6768. --- src/OpenTelemetry.Api/CHANGELOG.md | 3 + .../Propagation/TraceContextPropagator.cs | 50 ++++++++------ src/OpenTelemetry/CHANGELOG.md | 3 + .../Internal/OpenTelemetrySdkEventSource.cs | 6 +- .../EnvironmentVariableCarrierTests.cs | 16 +++-- .../Dockerfile | 2 +- .../TraceContextPropagatorTests.cs | 68 +++++++++++++++++++ 7 files changed, 119 insertions(+), 29 deletions(-) diff --git a/src/OpenTelemetry.Api/CHANGELOG.md b/src/OpenTelemetry.Api/CHANGELOG.md index 9084399a13c..e4fbfef0ea0 100644 --- a/src/OpenTelemetry.Api/CHANGELOG.md +++ b/src/OpenTelemetry.Api/CHANGELOG.md @@ -10,6 +10,9 @@ Notes](../../RELEASENOTES.md). Add support for using environment variables as context propagation carriers. ([#7174](https://github.com/open-telemetry/opentelemetry-dotnet/pull/7174)) +* Update `TraceContextPropagator` to support the W3C randomness flag. + ([#TODO](https://github.com/open-telemetry/opentelemetry-dotnet/pull/TODO)) + ## 1.15.3 Released 2026-Apr-21 diff --git a/src/OpenTelemetry.Api/Context/Propagation/TraceContextPropagator.cs b/src/OpenTelemetry.Api/Context/Propagation/TraceContextPropagator.cs index 237e2897b8e..477980e95a7 100644 --- a/src/OpenTelemetry.Api/Context/Propagation/TraceContextPropagator.cs +++ b/src/OpenTelemetry.Api/Context/Propagation/TraceContextPropagator.cs @@ -113,7 +113,8 @@ public override void Inject(PropagationContext context, T carrier, Action? tracestateCollecti } hasTraceState = true; - if (list.Count == 1) - { - return TryExtractSingleTracestate(list[0], out tracestateResult); - } - return TryExtractMultipleTracestate(list, out tracestateResult); + return list.Count == 1 ? + TryExtractSingleTracestate(list[0], out tracestateResult) : + TryExtractMultipleTracestate(list, out tracestateResult); } if (tracestateCollection is IReadOnlyList readOnlyList) @@ -264,12 +270,11 @@ private static bool TryExtractTracestate(IEnumerable? tracestateCollecti } hasTraceState = true; - if (readOnlyList.Count == 1) - { - return TryExtractSingleTracestate(readOnlyList[0], out tracestateResult); - } - return TryExtractMultipleTracestate(readOnlyList, out tracestateResult); + return + readOnlyList.Count == 1 ? + TryExtractSingleTracestate(readOnlyList[0], out tracestateResult) : + TryExtractMultipleTracestate(readOnlyList, out tracestateResult); } using var enumerator = tracestateCollection.GetEnumerator(); @@ -280,12 +285,10 @@ private static bool TryExtractTracestate(IEnumerable? tracestateCollecti hasTraceState = true; var singleTraceState = enumerator.Current; - if (!enumerator.MoveNext()) - { - return TryExtractSingleTracestate(singleTraceState, out tracestateResult); - } - return TryExtractMultipleTracestate(EnumerateFrom(singleTraceState, enumerator), out tracestateResult); + return enumerator.MoveNext() ? + TryExtractMultipleTracestate(EnumerateFrom(singleTraceState, enumerator), out tracestateResult) : + TryExtractSingleTracestate(singleTraceState, out tracestateResult); } private static IEnumerable EnumerateFrom(string first, IEnumerator enumerator) @@ -729,13 +732,16 @@ private static void WriteTraceParentIntoSpan(Span destination, ActivityCon context.TraceId.ToHexString().CopyTo(destination.Slice(3)); destination[35] = '-'; context.SpanId.ToHexString().CopyTo(destination.Slice(36)); - if ((context.TraceFlags & ActivityTraceFlags.Recorded) != 0) - { - "-01".CopyTo(destination.Slice(52)); - } - else + + var flags = (byte)context.TraceFlags; + destination[52] = '-'; + destination[53] = GetHexChar(flags >> 4); + destination[54] = GetHexChar(flags & 0xF); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static char GetHexChar(int value) { - "-00".CopyTo(destination.Slice(52)); + return (char)(value + (value < 10 ? '0' : 'a' - 10)); } } #endif diff --git a/src/OpenTelemetry/CHANGELOG.md b/src/OpenTelemetry/CHANGELOG.md index 4ee7bea15a7..ae1b3ee480a 100644 --- a/src/OpenTelemetry/CHANGELOG.md +++ b/src/OpenTelemetry/CHANGELOG.md @@ -14,6 +14,9 @@ Notes](../../RELEASENOTES.md). once per collection cycle. ([#7188](https://github.com/open-telemetry/opentelemetry-dotnet/pull/7188)) +* Update `OpenTelemetrySdkEventSource` to support the W3C randomness flag. + ([#TODO](https://github.com/open-telemetry/opentelemetry-dotnet/pull/TODO)) + ## 1.15.3 Released 2026-Apr-21 diff --git a/src/OpenTelemetry/Internal/OpenTelemetrySdkEventSource.cs b/src/OpenTelemetry/Internal/OpenTelemetrySdkEventSource.cs index 9cd69870706..dcf6300925c 100644 --- a/src/OpenTelemetry/Internal/OpenTelemetrySdkEventSource.cs +++ b/src/OpenTelemetry/Internal/OpenTelemetrySdkEventSource.cs @@ -6,6 +6,7 @@ using System.Diagnostics.CodeAnalysis; #endif using System.Diagnostics.Tracing; +using System.Globalization; using Microsoft.Extensions.Configuration; namespace OpenTelemetry.Internal; @@ -69,7 +70,8 @@ public void ActivityStarted(Activity activity) // correct sampling flags // https://github.com/dotnet/runtime/issues/61857 var activityId = string.Concat("00-", activity.TraceId.ToHexString(), "-", activity.SpanId.ToHexString()); - activityId = string.Concat(activityId, activity.ActivityTraceFlags.HasFlag(ActivityTraceFlags.Recorded) ? "-01" : "-00"); + var traceFlags = ((byte)activity.ActivityTraceFlags).ToString("x2", CultureInfo.InvariantCulture); + activityId = string.Concat(activityId, "-", traceFlags); this.ActivityStarted(activity.DisplayName, activityId); } } @@ -355,7 +357,7 @@ protected override void OnEventWritten(EventWrittenEventArgs e) } var message = e.Message != null && e.Payload != null && e.Payload.Count > 0 - ? string.Format(System.Globalization.CultureInfo.CurrentCulture, e.Message, [.. e.Payload]) + ? string.Format(CultureInfo.CurrentCulture, e.Message, [.. e.Payload]) : e.Message; Debug.WriteLine($"{e.EventSource.Name} - Level: [{e.Level}], EventId: [{e.EventId}], EventName: [{e.EventName}], Message: [{message}]"); diff --git a/test/OpenTelemetry.Api.Tests/Context/Propagation/EnvironmentVariableCarrierTests.cs b/test/OpenTelemetry.Api.Tests/Context/Propagation/EnvironmentVariableCarrierTests.cs index 3130fb661ec..cdacdb3d90a 100644 --- a/test/OpenTelemetry.Api.Tests/Context/Propagation/EnvironmentVariableCarrierTests.cs +++ b/test/OpenTelemetry.Api.Tests/Context/Propagation/EnvironmentVariableCarrierTests.cs @@ -209,13 +209,21 @@ public void Set_TargetsEnvironmentCopyWithoutMutatingProcessEnvironment() } } - [Fact] - public void TraceContextPropagator_RoundTripsThroughEnvironmentVariableCarrier() + [Theory] + [InlineData(ActivityTraceFlags.None, "00")] + [InlineData(ActivityTraceFlags.Recorded, "01")] + //// https://github.com/open-telemetry/opentelemetry-dotnet/pull/6899 + //// will change this to use ActivityTraceFlags.RandomTraceId instead. + [InlineData((ActivityTraceFlags)2, "02")] + [InlineData((ActivityTraceFlags)2 | ActivityTraceFlags.Recorded, "03")] + public void TraceContextPropagator_RoundTripsThroughEnvironmentVariableCarrier( + ActivityTraceFlags flags, + string expectedSuffix) { var activityContext = new ActivityContext( ActivityTraceId.CreateFromString("0af7651916cd43dd8448eb211c80319c"), ActivitySpanId.CreateFromString("b9c7c989f97918e1"), - ActivityTraceFlags.Recorded, + flags, "key1=value1,key2=value2"); var carrier = new Dictionary(StringComparer.Ordinal); @@ -226,7 +234,7 @@ public void TraceContextPropagator_RoundTripsThroughEnvironmentVariableCarrier() var extracted = propagator.Extract(default, EnvironmentVariableCarrier.Capture(carrier), EnvironmentVariableCarrier.Get); - Assert.Equal("00-0af7651916cd43dd8448eb211c80319c-b9c7c989f97918e1-01", carrier["TRACEPARENT"]); + Assert.Equal($"00-0af7651916cd43dd8448eb211c80319c-b9c7c989f97918e1-{expectedSuffix}", carrier["TRACEPARENT"]); Assert.Equal("key1=value1,key2=value2", carrier["TRACESTATE"]); Assert.Equal(activityContext.TraceId, extracted.ActivityContext.TraceId); Assert.Equal(activityContext.SpanId, extracted.ActivityContext.SpanId); diff --git a/test/OpenTelemetry.Instrumentation.W3cTraceContext.Tests/Dockerfile b/test/OpenTelemetry.Instrumentation.W3cTraceContext.Tests/Dockerfile index 1257df7ede8..0b33d4258b2 100644 --- a/test/OpenTelemetry.Instrumentation.W3cTraceContext.Tests/Dockerfile +++ b/test/OpenTelemetry.Instrumentation.W3cTraceContext.Tests/Dockerfile @@ -37,7 +37,7 @@ RUN tdnf install -y python3-pip \ && ln -sf /usr/bin/python3 /usr/bin/python \ && python3 -m pip install --requirement requirements.txt --require-hashes --break-system-packages \ && tdnf clean all -ENV SPEC_LEVEL=1 +ENV SPEC_LEVEL=2 ENV STRICT_LEVEL=1 ENTRYPOINT ["dotnet", "vstest", "OpenTelemetry.Instrumentation.W3cTraceContext.Tests.dll", "--logger:console;verbosity=detailed"] diff --git a/test/OpenTelemetry.Tests/Trace/Propagation/TraceContextPropagatorTests.cs b/test/OpenTelemetry.Tests/Trace/Propagation/TraceContextPropagatorTests.cs index 5359905da28..df91c7ef897 100644 --- a/test/OpenTelemetry.Tests/Trace/Propagation/TraceContextPropagatorTests.cs +++ b/test/OpenTelemetry.Tests/Trace/Propagation/TraceContextPropagatorTests.cs @@ -66,6 +66,50 @@ public void NotSampled() Assert.True(ctx.ActivityContext.IsValid()); } + [Fact] + public void RandomTraceId() + { + var headers = new Dictionary + { + { TraceParent, $"00-{TraceId}-{SpanId}-02" }, + }; + + var f = new TraceContextPropagator(); + var ctx = f.Extract(default, headers, Getter); + + Assert.Equal(ActivityTraceId.CreateFromString(TraceId.AsSpan()), ctx.ActivityContext.TraceId); + Assert.Equal(ActivitySpanId.CreateFromString(SpanId.AsSpan()), ctx.ActivityContext.SpanId); + + // https://github.com/open-telemetry/opentelemetry-dotnet/pull/6899 + // will change this to use ActivityTraceFlags.RandomTraceId instead. + Assert.Equal((ActivityTraceFlags)2, ctx.ActivityContext.TraceFlags); + + Assert.True(ctx.ActivityContext.IsValid()); + } + + [Fact] + public void RandomTraceIdAndRecorded() + { + var headers = new Dictionary + { + { TraceParent, $"00-{TraceId}-{SpanId}-03" }, + }; + + var f = new TraceContextPropagator(); + var ctx = f.Extract(default, headers, Getter); + + Assert.Equal(ActivityTraceId.CreateFromString(TraceId.AsSpan()), ctx.ActivityContext.TraceId); + Assert.Equal(ActivitySpanId.CreateFromString(SpanId.AsSpan()), ctx.ActivityContext.SpanId); + + Assert.True(ctx.ActivityContext.TraceFlags.HasFlag(ActivityTraceFlags.Recorded)); + + // https://github.com/open-telemetry/opentelemetry-dotnet/pull/6899 + // will change this to use ActivityTraceFlags.RandomTraceId instead. + Assert.True(ctx.ActivityContext.TraceFlags.HasFlag((ActivityTraceFlags)2)); + + Assert.True(ctx.ActivityContext.IsValid()); + } + [Fact] public void IsBlankIfNoHeader() { @@ -79,6 +123,8 @@ public void IsBlankIfNoHeader() [Theory] [InlineData($"00-xyz7651916cd43dd8448eb211c80319c-{SpanId}-01")] + [InlineData($"00-xyz7651916cd43dd8448eb211c80319c-{SpanId}-02")] + [InlineData($"00-xyz7651916cd43dd8448eb211c80319c-{SpanId}-03")] [InlineData($"00-{TraceId}-xyz7c989f97918e1-01")] [InlineData($"00-{TraceId}-{SpanId}-x1")] [InlineData($"00-{TraceId}-{SpanId}-1x")] @@ -311,6 +357,28 @@ public void Inject_WithTracestate() Assert.Equal(expectedHeaders, carrier); } + [Fact] + public void Inject_WithRandomTraceId() + { + var traceId = ActivityTraceId.CreateRandom(); + var spanId = ActivitySpanId.CreateRandom(); + var expectedHeaders = new Dictionary + { + { TraceParent, $"00-{traceId}-{spanId}-02" }, + { TraceState, $"congo=lZWRzIHRoNhcm5hbCBwbGVhc3VyZS4,rojo=00-{traceId}-00f067aa0ba902b7-02" }, + }; + + // https://github.com/open-telemetry/opentelemetry-dotnet/pull/6899 + // will change this to use ActivityTraceFlags.RandomTraceId instead. + var activityContext = new ActivityContext(traceId, spanId, (ActivityTraceFlags)2, expectedHeaders[TraceState]); + var propagationContext = new PropagationContext(activityContext, default); + var carrier = new Dictionary(); + var f = new TraceContextPropagator(); + f.Inject(propagationContext, carrier, Setter); + + Assert.Equal(expectedHeaders, carrier); + } + [Fact] public void Inject_TruncatesOversizedTracestate() { From 639ce8576e399422eee36ddd7ebcaed8bcc80f46 Mon Sep 17 00:00:00 2001 From: Martin Costello Date: Fri, 15 May 2026 17:13:30 +0100 Subject: [PATCH 2/6] [API] Update CHANGELOGs Add PR number. --- src/OpenTelemetry.Api/CHANGELOG.md | 2 +- src/OpenTelemetry/CHANGELOG.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/OpenTelemetry.Api/CHANGELOG.md b/src/OpenTelemetry.Api/CHANGELOG.md index e4fbfef0ea0..92535998fc0 100644 --- a/src/OpenTelemetry.Api/CHANGELOG.md +++ b/src/OpenTelemetry.Api/CHANGELOG.md @@ -11,7 +11,7 @@ Notes](../../RELEASENOTES.md). ([#7174](https://github.com/open-telemetry/opentelemetry-dotnet/pull/7174)) * Update `TraceContextPropagator` to support the W3C randomness flag. - ([#TODO](https://github.com/open-telemetry/opentelemetry-dotnet/pull/TODO)) + ([#7301](https://github.com/open-telemetry/opentelemetry-dotnet/pull/7301)) ## 1.15.3 diff --git a/src/OpenTelemetry/CHANGELOG.md b/src/OpenTelemetry/CHANGELOG.md index ae1b3ee480a..b2dd3221336 100644 --- a/src/OpenTelemetry/CHANGELOG.md +++ b/src/OpenTelemetry/CHANGELOG.md @@ -15,7 +15,7 @@ Notes](../../RELEASENOTES.md). ([#7188](https://github.com/open-telemetry/opentelemetry-dotnet/pull/7188)) * Update `OpenTelemetrySdkEventSource` to support the W3C randomness flag. - ([#TODO](https://github.com/open-telemetry/opentelemetry-dotnet/pull/TODO)) + ([#7301](https://github.com/open-telemetry/opentelemetry-dotnet/pull/7301)) ## 1.15.3 From a474c85bcba45ab03414dc2917e3a6b4314355a2 Mon Sep 17 00:00:00 2001 From: martincostello Date: Mon, 18 May 2026 12:02:42 +0100 Subject: [PATCH 3/6] [OpenTelemetry] Rename variables - Rename `f` to `propagator`. - Rename `ctx` to `context`. --- .../TraceContextPropagatorTests.cs | 114 +++++++++--------- 1 file changed, 57 insertions(+), 57 deletions(-) diff --git a/test/OpenTelemetry.Tests/Trace/Propagation/TraceContextPropagatorTests.cs b/test/OpenTelemetry.Tests/Trace/Propagation/TraceContextPropagatorTests.cs index df91c7ef897..0866fdd3029 100644 --- a/test/OpenTelemetry.Tests/Trace/Propagation/TraceContextPropagatorTests.cs +++ b/test/OpenTelemetry.Tests/Trace/Propagation/TraceContextPropagatorTests.cs @@ -34,17 +34,17 @@ public void CanParseExampleFromSpec() { TraceState, $"congo=lZWRzIHRoNhcm5hbCBwbGVhc3VyZS4,rojo=00-{TraceId}-00f067aa0ba902b7-01" }, }; - var f = new TraceContextPropagator(); - var ctx = f.Extract(default, headers, Getter); + var propagator = new TraceContextPropagator(); + var context = propagator.Extract(default, headers, Getter); - Assert.Equal(ActivityTraceId.CreateFromString(TraceId.AsSpan()), ctx.ActivityContext.TraceId); - Assert.Equal(ActivitySpanId.CreateFromString(SpanId.AsSpan()), ctx.ActivityContext.SpanId); + Assert.Equal(ActivityTraceId.CreateFromString(TraceId.AsSpan()), context.ActivityContext.TraceId); + Assert.Equal(ActivitySpanId.CreateFromString(SpanId.AsSpan()), context.ActivityContext.SpanId); - Assert.True(ctx.ActivityContext.IsRemote); - Assert.True(ctx.ActivityContext.IsValid()); - Assert.NotEqual(0, (int)(ctx.ActivityContext.TraceFlags & ActivityTraceFlags.Recorded)); + Assert.True(context.ActivityContext.IsRemote); + Assert.True(context.ActivityContext.IsValid()); + Assert.NotEqual(0, (int)(context.ActivityContext.TraceFlags & ActivityTraceFlags.Recorded)); - Assert.Equal($"congo=lZWRzIHRoNhcm5hbCBwbGVhc3VyZS4,rojo=00-{TraceId}-00f067aa0ba902b7-01", ctx.ActivityContext.TraceState); + Assert.Equal($"congo=lZWRzIHRoNhcm5hbCBwbGVhc3VyZS4,rojo=00-{TraceId}-00f067aa0ba902b7-01", context.ActivityContext.TraceState); } [Fact] @@ -55,15 +55,15 @@ public void NotSampled() { TraceParent, $"00-{TraceId}-{SpanId}-00" }, }; - var f = new TraceContextPropagator(); - var ctx = f.Extract(default, headers, Getter); + var propagator = new TraceContextPropagator(); + var context = propagator.Extract(default, headers, Getter); - Assert.Equal(ActivityTraceId.CreateFromString(TraceId.AsSpan()), ctx.ActivityContext.TraceId); - Assert.Equal(ActivitySpanId.CreateFromString(SpanId.AsSpan()), ctx.ActivityContext.SpanId); - Assert.Equal(0, (int)(ctx.ActivityContext.TraceFlags & ActivityTraceFlags.Recorded)); + Assert.Equal(ActivityTraceId.CreateFromString(TraceId.AsSpan()), context.ActivityContext.TraceId); + Assert.Equal(ActivitySpanId.CreateFromString(SpanId.AsSpan()), context.ActivityContext.SpanId); + Assert.Equal(0, (int)(context.ActivityContext.TraceFlags & ActivityTraceFlags.Recorded)); - Assert.True(ctx.ActivityContext.IsRemote); - Assert.True(ctx.ActivityContext.IsValid()); + Assert.True(context.ActivityContext.IsRemote); + Assert.True(context.ActivityContext.IsValid()); } [Fact] @@ -74,17 +74,17 @@ public void RandomTraceId() { TraceParent, $"00-{TraceId}-{SpanId}-02" }, }; - var f = new TraceContextPropagator(); - var ctx = f.Extract(default, headers, Getter); + var propagator = new TraceContextPropagator(); + var context = propagator.Extract(default, headers, Getter); - Assert.Equal(ActivityTraceId.CreateFromString(TraceId.AsSpan()), ctx.ActivityContext.TraceId); - Assert.Equal(ActivitySpanId.CreateFromString(SpanId.AsSpan()), ctx.ActivityContext.SpanId); + Assert.Equal(ActivityTraceId.CreateFromString(TraceId.AsSpan()), context.ActivityContext.TraceId); + Assert.Equal(ActivitySpanId.CreateFromString(SpanId.AsSpan()), context.ActivityContext.SpanId); // https://github.com/open-telemetry/opentelemetry-dotnet/pull/6899 // will change this to use ActivityTraceFlags.RandomTraceId instead. - Assert.Equal((ActivityTraceFlags)2, ctx.ActivityContext.TraceFlags); + Assert.Equal((ActivityTraceFlags)2, context.ActivityContext.TraceFlags); - Assert.True(ctx.ActivityContext.IsValid()); + Assert.True(context.ActivityContext.IsValid()); } [Fact] @@ -95,19 +95,19 @@ public void RandomTraceIdAndRecorded() { TraceParent, $"00-{TraceId}-{SpanId}-03" }, }; - var f = new TraceContextPropagator(); - var ctx = f.Extract(default, headers, Getter); + var propagator = new TraceContextPropagator(); + var context = propagator.Extract(default, headers, Getter); - Assert.Equal(ActivityTraceId.CreateFromString(TraceId.AsSpan()), ctx.ActivityContext.TraceId); - Assert.Equal(ActivitySpanId.CreateFromString(SpanId.AsSpan()), ctx.ActivityContext.SpanId); + Assert.Equal(ActivityTraceId.CreateFromString(TraceId.AsSpan()), context.ActivityContext.TraceId); + Assert.Equal(ActivitySpanId.CreateFromString(SpanId.AsSpan()), context.ActivityContext.SpanId); - Assert.True(ctx.ActivityContext.TraceFlags.HasFlag(ActivityTraceFlags.Recorded)); + Assert.True(context.ActivityContext.TraceFlags.HasFlag(ActivityTraceFlags.Recorded)); // https://github.com/open-telemetry/opentelemetry-dotnet/pull/6899 // will change this to use ActivityTraceFlags.RandomTraceId instead. - Assert.True(ctx.ActivityContext.TraceFlags.HasFlag((ActivityTraceFlags)2)); + Assert.True(context.ActivityContext.TraceFlags.HasFlag((ActivityTraceFlags)2)); - Assert.True(ctx.ActivityContext.IsValid()); + Assert.True(context.ActivityContext.IsValid()); } [Fact] @@ -115,10 +115,10 @@ public void IsBlankIfNoHeader() { var headers = new Dictionary(); - var f = new TraceContextPropagator(); - var ctx = f.Extract(default, headers, Getter); + var propagator = new TraceContextPropagator(); + var context = propagator.Extract(default, headers, Getter); - Assert.False(ctx.ActivityContext.IsValid()); + Assert.False(context.ActivityContext.IsValid()); } [Theory] @@ -135,10 +135,10 @@ public void IsBlankIfInvalid(string invalidTraceParent) { TraceParent, invalidTraceParent }, }; - var f = new TraceContextPropagator(); - var ctx = f.Extract(default, headers, Getter); + var propagator = new TraceContextPropagator(); + var context = propagator.Extract(default, headers, Getter); - Assert.False(ctx.ActivityContext.IsValid()); + Assert.False(context.ActivityContext.IsValid()); } [Fact] @@ -149,10 +149,10 @@ public void TracestateToStringEmpty() { TraceParent, $"00-{TraceId}-{SpanId}-01" }, }; - var f = new TraceContextPropagator(); - var ctx = f.Extract(default, headers, Getter); + var propagator = new TraceContextPropagator(); + var context = propagator.Extract(default, headers, Getter); - Assert.Null(ctx.ActivityContext.TraceState); + Assert.Null(context.ActivityContext.TraceState); } [Fact] @@ -164,10 +164,10 @@ public void TracestateToString() { TraceState, "k1=v1,k2=v2,k3=v3" }, }; - var f = new TraceContextPropagator(); - var ctx = f.Extract(default, headers, Getter); + var propagator = new TraceContextPropagator(); + var context = propagator.Extract(default, headers, Getter); - Assert.Equal("k1=v1,k2=v2,k3=v3", ctx.ActivityContext.TraceState); + Assert.Equal("k1=v1,k2=v2,k3=v3", context.ActivityContext.TraceState); } [Fact] @@ -331,8 +331,8 @@ public void Inject_NoTracestate() var activityContext = new ActivityContext(traceId, spanId, ActivityTraceFlags.Recorded, traceState: null); var propagationContext = new PropagationContext(activityContext, default); var carrier = new Dictionary(); - var f = new TraceContextPropagator(); - f.Inject(propagationContext, carrier, Setter); + var propagator = new TraceContextPropagator(); + propagator.Inject(propagationContext, carrier, Setter); Assert.Equal(expectedHeaders, carrier); } @@ -351,8 +351,8 @@ public void Inject_WithTracestate() var activityContext = new ActivityContext(traceId, spanId, ActivityTraceFlags.Recorded, expectedHeaders[TraceState]); var propagationContext = new PropagationContext(activityContext, default); var carrier = new Dictionary(); - var f = new TraceContextPropagator(); - f.Inject(propagationContext, carrier, Setter); + var propagator = new TraceContextPropagator(); + propagator.Inject(propagationContext, carrier, Setter); Assert.Equal(expectedHeaders, carrier); } @@ -373,8 +373,8 @@ public void Inject_WithRandomTraceId() var activityContext = new ActivityContext(traceId, spanId, (ActivityTraceFlags)2, expectedHeaders[TraceState]); var propagationContext = new PropagationContext(activityContext, default); var carrier = new Dictionary(); - var f = new TraceContextPropagator(); - f.Inject(propagationContext, carrier, Setter); + var propagator = new TraceContextPropagator(); + propagator.Inject(propagationContext, carrier, Setter); Assert.Equal(expectedHeaders, carrier); } @@ -390,8 +390,8 @@ public void Inject_TruncatesOversizedTracestate() var activityContext = new ActivityContext(traceId, spanId, ActivityTraceFlags.Recorded, oversizedTraceState); var propagationContext = new PropagationContext(activityContext, default); var carrier = new Dictionary(); - var f = new TraceContextPropagator(); - f.Inject(propagationContext, carrier, Setter); + var propagator = new TraceContextPropagator(); + propagator.Inject(propagationContext, carrier, Setter); Assert.Equal($"00-{traceId}-{spanId}-01", carrier[TraceParent]); Assert.Equal(expectedTraceState, carrier[TraceState]); @@ -519,9 +519,9 @@ private static string CallTraceContextPropagatorWithTraceParent(string tracepare { { TraceParent, traceparent }, }; - var f = new TraceContextPropagator(); - var ctx = f.Extract(default, headers, Getter); - return ctx.ActivityContext.TraceId.ToString(); + var propagator = new TraceContextPropagator(); + var context = propagator.Extract(default, headers, Getter); + return context.ActivityContext.TraceId.ToString(); } private static string CallTraceContextPropagator(string tracestate) @@ -531,10 +531,10 @@ private static string CallTraceContextPropagator(string tracestate) { TraceParent, $"00-{TraceId}-{SpanId}-01" }, { TraceState, tracestate }, }; - var f = new TraceContextPropagator(); - var ctx = f.Extract(default, headers, Getter); + var propagator = new TraceContextPropagator(); + var context = propagator.Extract(default, headers, Getter); - var traceState = ctx.ActivityContext.TraceState; + var traceState = context.ActivityContext.TraceState; Assert.NotNull(traceState); return traceState; } @@ -546,10 +546,10 @@ private static string CallTraceContextPropagator(string[] tracestate) { TraceParent, [$"00-{TraceId}-{SpanId}-01"] }, { TraceState, tracestate }, }; - var f = new TraceContextPropagator(); - var ctx = f.Extract(default, headers, ArrayGetter); + var propagator = new TraceContextPropagator(); + var context = propagator.Extract(default, headers, ArrayGetter); - var traceState = ctx.ActivityContext.TraceState; + var traceState = context.ActivityContext.TraceState; Assert.NotNull(traceState); return traceState; } From bb72bb59fd77ad610e260dd7722f642d5fe3659f Mon Sep 17 00:00:00 2001 From: martincostello Date: Mon, 18 May 2026 12:07:27 +0100 Subject: [PATCH 4/6] [API] Improve performance Optimise the implementation. --- .../Propagation/TraceContextPropagator.cs | 97 ++++++++++++++----- 1 file changed, 74 insertions(+), 23 deletions(-) diff --git a/src/OpenTelemetry.Api/Context/Propagation/TraceContextPropagator.cs b/src/OpenTelemetry.Api/Context/Propagation/TraceContextPropagator.cs index 477980e95a7..a8ed2fd792b 100644 --- a/src/OpenTelemetry.Api/Context/Propagation/TraceContextPropagator.cs +++ b/src/OpenTelemetry.Api/Context/Propagation/TraceContextPropagator.cs @@ -113,8 +113,8 @@ public override void Inject(PropagationContext context, T carrier, Action(PropagationContext context, T carrier, Action destination, ActivityContext context) + { + "00-".CopyTo(destination); + + context.TraceId.ToHexString().CopyTo(destination.Slice(3)); + + destination[35] = '-'; + + context.SpanId.ToHexString().CopyTo(destination.Slice(36)); + + // https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/3867 + // will change this code to use ActivityTraceFlags.RandomTraceId instead of 2. + // If new enem values are added in the future the Fallback path will ensure + // that the handling is functionally correct, but the condition should be updated + // to include the new value(s) for better readability and performance where possible. + if (context.TraceFlags == ActivityTraceFlags.Recorded) + { + "-01".CopyTo(destination.Slice(52)); + } + else if (context.TraceFlags == (ActivityTraceFlags)2) + { + "-02".CopyTo(destination.Slice(52)); + } + else if (context.TraceFlags == (ActivityTraceFlags.Recorded | (ActivityTraceFlags)2)) + { + "-03".CopyTo(destination.Slice(52)); + } + else + { + var flags = (byte)context.TraceFlags; + destination[52] = '-'; + destination[53] = GetHexChar(flags >> 4); + destination[54] = GetHexChar(flags & 0xF); + } + } +#else + static string FormatActivityTraceFlags(ActivityTraceFlags flags) + { + // https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/3867 + // will change this code to use ActivityTraceFlags.RandomTraceId instead of 2. + // If new enem values are added in the future the Fallback path will ensure + // that the handling is functionally correct, but the switch should be updated + // to include the new value(s) for better readability and performance where possible. + return flags switch + { + ActivityTraceFlags.None => "-00", + ActivityTraceFlags.Recorded => "-01", + (ActivityTraceFlags)2 => "-02", + ActivityTraceFlags.Recorded | (ActivityTraceFlags)2 => "-03", + _ => Fallback((byte)flags), + }; + + static string Fallback(byte flags) + { + Span buffer = stackalloc char[3]; + + buffer[0] = '-'; + buffer[1] = GetHexChar(flags >> 4); + buffer[2] = GetHexChar(flags & 0xF); + + return buffer.ToString(); + } + } +#endif + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static char GetHexChar(int value) + { + return (char)(value + (value < 10 ? '0' : 'a' - 10)); + } } internal static bool TryExtractTraceparent(string traceparent, out ActivityTraceId traceId, out ActivitySpanId spanId, out ActivityTraceFlags traceOptions) @@ -724,25 +796,4 @@ private static bool ValidateValue(ReadOnlySpan value) [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool IsAsciiLetterOrDigitLower(char c) => char.IsAsciiDigit(c) || char.IsAsciiLetterLower(c); - -#if NET - private static void WriteTraceParentIntoSpan(Span destination, ActivityContext context) - { - "00-".CopyTo(destination); - context.TraceId.ToHexString().CopyTo(destination.Slice(3)); - destination[35] = '-'; - context.SpanId.ToHexString().CopyTo(destination.Slice(36)); - - var flags = (byte)context.TraceFlags; - destination[52] = '-'; - destination[53] = GetHexChar(flags >> 4); - destination[54] = GetHexChar(flags & 0xF); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - static char GetHexChar(int value) - { - return (char)(value + (value < 10 ? '0' : 'a' - 10)); - } - } -#endif } From 05e2a182dbedb2061be820bb0e278e5fe65b862d Mon Sep 17 00:00:00 2001 From: martincostello Date: Mon, 18 May 2026 12:08:02 +0100 Subject: [PATCH 5/6] [API] Fix typo Fix typo in comment. --- .../Context/Propagation/TraceContextPropagator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenTelemetry.Api/Context/Propagation/TraceContextPropagator.cs b/src/OpenTelemetry.Api/Context/Propagation/TraceContextPropagator.cs index a8ed2fd792b..a20f00e47c1 100644 --- a/src/OpenTelemetry.Api/Context/Propagation/TraceContextPropagator.cs +++ b/src/OpenTelemetry.Api/Context/Propagation/TraceContextPropagator.cs @@ -174,7 +174,7 @@ static string FormatActivityTraceFlags(ActivityTraceFlags flags) { // https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/3867 // will change this code to use ActivityTraceFlags.RandomTraceId instead of 2. - // If new enem values are added in the future the Fallback path will ensure + // If new enum values are added in the future the Fallback path will ensure // that the handling is functionally correct, but the switch should be updated // to include the new value(s) for better readability and performance where possible. return flags switch From 1f733c9c7fe92bbea5bebaaa13edb8c7ce98fd5d Mon Sep 17 00:00:00 2001 From: Martin Costello Date: Mon, 18 May 2026 12:09:59 +0100 Subject: [PATCH 6/6] [API] Fix typo Fix typo in comment. --- .../Context/Propagation/TraceContextPropagator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenTelemetry.Api/Context/Propagation/TraceContextPropagator.cs b/src/OpenTelemetry.Api/Context/Propagation/TraceContextPropagator.cs index a20f00e47c1..146b4552a17 100644 --- a/src/OpenTelemetry.Api/Context/Propagation/TraceContextPropagator.cs +++ b/src/OpenTelemetry.Api/Context/Propagation/TraceContextPropagator.cs @@ -146,7 +146,7 @@ static void WriteTraceParentIntoSpan(Span destination, ActivityContext con // https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/3867 // will change this code to use ActivityTraceFlags.RandomTraceId instead of 2. - // If new enem values are added in the future the Fallback path will ensure + // If new enum values are added in the future the Fallback path will ensure // that the handling is functionally correct, but the condition should be updated // to include the new value(s) for better readability and performance where possible. if (context.TraceFlags == ActivityTraceFlags.Recorded)