Skip to content

[Tracing] Add interoperability between the Datadog Baggage API and the OpenTelemetry Baggage API#7921

Merged
zacharycmontoya merged 18 commits intomasterfrom
zach.montoya/otel-baggage-sync-APMAPI-1736
Mar 19, 2026
Merged

[Tracing] Add interoperability between the Datadog Baggage API and the OpenTelemetry Baggage API#7921
zacharycmontoya merged 18 commits intomasterfrom
zach.montoya/otel-baggage-sync-APMAPI-1736

Conversation

@zacharycmontoya
Copy link
Copy Markdown
Contributor

@zacharycmontoya zacharycmontoya commented Dec 9, 2025

Summary of changes

Enable updates to Baggage from either the Datadog Baggage API and the OpenTelemetry Baggage API to be visible to the other.

Relies on #7920 for properly instrumenting the static methods in the OpenTelemetry.Baggage struct.

Reason for change

This allows users of either Baggage API to make changes to the automatically-propagated Baggage header. Before, only changes made through the Datadog Baggage API would be reflected.

Implementation details

APIs supported

Instruments the following methods in the OpenTelemetry Baggage API:

  • static OpenTelemetry.Baggage.ClearBaggage(Baggage)
  • static OpenTelemetry.Baggage.Current { get; set; }
  • static OpenTelemetry.Baggage.RemoveBaggage(string, Baggage)
  • static OpenTelemetry.Baggage.SetBaggage(string, string, Baggage)
  • static OpenTelemetry.Baggage.SetBaggage(IEnumerable<KeyValuePair<string, string?>>, Baggage)

The following public API's are also supported indirectly by our instrumentation of the OpenTelemetry.Baggage.Current getter:

  • static OpenTelemetry.Baggage.GetBaggage(Baggage)
  • static OpenTelemetry.Baggage.GetBaggage(string, Baggage)
  • static OpenTelemetry.Baggage.GetEnumerator(Baggage)

OpenTelemetry Baggage Model

In the OpenTelemetry model, baggage is immutable. This means every time the user wants to update OpenTelemetry.Baggage.Current, they must either call the setter directly with an new instance of OpenTelemetry.Baggage, or they must call one of the static APIs.

The static APIs are largely intuitive, but it's worth calling two interesting behaviors of the APIs that modify baggage:

  1. If a OpenTelemetry.Baggage argument is provided (optional), that object will be the source from which the modification will be enacted on. If not, then OpenTelemetry.Baggage.Current will be the source.
  2. Before returning from the API call, the resulting (immutable) OpenTelemetry.Baggage is set as the new OpenTelemetry.Baggage.Current value.

Interoperability Approach

For this approach, Datadog.Trace.Baggage.Current will be the source of truth for the "current" baggage. For example, our HTTP server instrumentations will first populate Datadog.Trace.Baggage.Current with the contents of the incoming baggage HTTP header, and users of the Datadog.Trace.Baggage API can update the mutable Datadog.Trace.Baggage.Current. Then, if the user wants get or set OpenTelemetry.Baggage.Current, that is where our instrumentation must keep it in sync with Datadog.Trace.Baggage.Current.

Each time OpenTelemetry.Baggage.set_Current is invoked, we must replace the contents of Datadog.Trace.Baggage.Current entirely by clearing out the existing values and add all of the new KeyValuePairs.

Each time OpenTelemetry.Baggage.get_Current is invoked, we must create a new (immutable) OpenTelemetry.Baggage using the contents of Datadog.Trace.Baggage.Current and assign it to OpenTelemetry.Baggage.Current before the property is accessed and returned to the API caller.

By design, no work is done if only the Datadog.Trace.Baggage APIs are used. Also, users are aware of the immutable nature of the OpenTelemetry.Baggage APIs, so they will be used to doing at least one allocation per baggage modification, but we can certainly improve the overhead of this interop with approaches noted in the 'Other details' section below.

Test coverage

Added a new OtelBaggageController to the Samples.AspNetCoreMinimalApis and Samples.AspNetCoreMvc* applications, and added integration test cases (with snapshots) to tracer/test/Datadog.Trace.ClrProfiler.IntegrationTests/AspNetCore/AspNetCoreMinimalApisTests.cs.

Other details

We can follow up this PR with changes to optimize the sharing of Baggage state between the two API's. In order to do this, we would need to do the following:

  • Change the internal storage mechanism of items in Datadog.Trace.Baggage to be Dictionary<string,string>
  • Change Datadog.Trace.Baggage to disallow null values in baggage key-value pairs, to align with the OpenTelemetry storage model
  • When returning from the static OpenTelemetry.Baggage methods that modify state, take the reference to the internal Dictionary<string,string> object and assign it to the Datadog.Trace.Baggage.Current items dictionary

More immediately, we could implement tracking of writes to Datadog.Trace.Baggage.Current so we don't have to create a fresh copy of it every time OpenTelemetry.Baggage.get_Current is called, but I'd rather separate that out from this PR.

@zacharycmontoya zacharycmontoya changed the title [Tracer] Add interoperability between the Datadog Baggage API and the OpenTelemetry Baggage API [Tracing] Add interoperability between the Datadog Baggage API and the OpenTelemetry Baggage API Dec 9, 2025
@zacharycmontoya zacharycmontoya force-pushed the zach.montoya/instrument-static-valuetype branch 2 times, most recently from 5d57d03 to cbfdd00 Compare January 30, 2026 22:40
@zacharycmontoya zacharycmontoya force-pushed the zach.montoya/otel-baggage-sync-APMAPI-1736 branch 2 times, most recently from c9cbcf3 to b258b5b Compare January 30, 2026 23:55
@pr-commenter
Copy link
Copy Markdown

pr-commenter bot commented Jan 31, 2026

Benchmarks

Benchmark execution time: 2026-03-16 16:16:35

Comparing candidate commit f8b5443 in PR branch zach.montoya/otel-baggage-sync-APMAPI-1736 with baseline commit 4972eba in branch master.

Found 10 performance improvements and 9 performance regressions! Performance is the same for 157 metrics, 16 unstable metrics.

scenario:Benchmarks.Trace.AgentWriterBenchmark.WriteAndFlushEnrichedTraces net6.0

  • 🟥 execution_time [+80.052ms; +80.205ms] or [+64.789%; +64.913%]

scenario:Benchmarks.Trace.AgentWriterBenchmark.WriteAndFlushEnrichedTraces netcoreapp3.1

  • 🟩 execution_time [-109.003ms; -108.805ms] or [-49.562%; -49.472%]

scenario:Benchmarks.Trace.Asm.AppSecBodyBenchmark.AllCycleMoreComplexBody net6.0

  • 🟥 execution_time [+13.571ms; +16.562ms] or [+6.640%; +8.103%]

scenario:Benchmarks.Trace.Asm.AppSecBodyBenchmark.AllCycleSimpleBody net472

  • 🟩 throughput [+52979.121op/s; +54523.953op/s] or [+5.507%; +5.667%]

scenario:Benchmarks.Trace.Asm.AppSecBodyBenchmark.AllCycleSimpleBody netcoreapp3.1

  • 🟥 execution_time [+13.098ms; +18.895ms] or [+6.588%; +9.504%]

scenario:Benchmarks.Trace.Asm.AppSecBodyBenchmark.ObjectExtractorMoreComplexBody netcoreapp3.1

  • 🟩 execution_time [-18.541ms; -13.339ms] or [-8.776%; -6.314%]

scenario:Benchmarks.Trace.Asm.AppSecBodyBenchmark.ObjectExtractorSimpleBody netcoreapp3.1

  • 🟥 execution_time [+11.391ms; +17.546ms] or [+5.713%; +8.800%]

scenario:Benchmarks.Trace.AspNetCoreBenchmark.SendRequest net6.0

  • 🟥 execution_time [+97.891ms; +98.781ms] or [+98.299%; +99.192%]

scenario:Benchmarks.Trace.CharSliceBenchmark.OptimizedCharSlice net6.0

  • 🟩 execution_time [-94.087µs; -90.806µs] or [-6.401%; -6.178%]
  • 🟩 throughput [+44.831op/s; +46.492op/s] or [+6.590%; +6.834%]

scenario:Benchmarks.Trace.GraphQLBenchmark.ExecuteAsync net6.0

  • 🟥 execution_time [+15.254ms; +19.287ms] or [+7.731%; +9.775%]
  • 🟥 throughput [-43671.931op/s; -32552.077op/s] or [-8.052%; -6.002%]

scenario:Benchmarks.Trace.ILoggerBenchmark.EnrichedLog net472

  • 🟩 throughput [+13464.088op/s; +15912.481op/s] or [+5.492%; +6.491%]

scenario:Benchmarks.Trace.Iast.StringAspectsBenchmark.StringConcatAspectBenchmark netcoreapp3.1

  • 🟩 throughput [+313.848op/s; +483.498op/s] or [+17.944%; +27.643%]

scenario:Benchmarks.Trace.RedisBenchmark.SendReceive net472

  • 🟩 throughput [+27520.485op/s; +29436.888op/s] or [+7.906%; +8.457%]

scenario:Benchmarks.Trace.SpanBenchmark.StartFinishScope net6.0

  • 🟥 throughput [-72404.014op/s; -63417.105op/s] or [-6.548%; -5.735%]

scenario:Benchmarks.Trace.SpanBenchmark.StartFinishScope netcoreapp3.1

  • 🟩 execution_time [-22.470ms; -18.562ms] or [-10.409%; -8.599%]

scenario:Benchmarks.Trace.SpanBenchmark.StartFinishSpan netcoreapp3.1

  • 🟩 execution_time [-16.981ms; -11.118ms] or [-7.990%; -5.231%]

scenario:Benchmarks.Trace.TraceAnnotationsBenchmark.RunOnMethodBegin netcoreapp3.1

  • 🟥 execution_time [+10.531ms; +16.230ms] or [+5.355%; +8.254%]

@dd-trace-dotnet-ci-bot
Copy link
Copy Markdown

dd-trace-dotnet-ci-bot bot commented Jan 31, 2026

Execution-Time Benchmarks Report ⏱️

Execution-time results for samples comparing This PR (7921) and master.

✅ No regressions detected - check the details below

Full Metrics Comparison

FakeDbCommand

Metric Master (Mean ± 95% CI) Current (Mean ± 95% CI) Change Status
.NET Framework 4.8 - Baseline
duration75.45 ± (75.38 - 75.67) ms76.47 ± (76.46 - 76.79) ms+1.3%✅⬆️
.NET Framework 4.8 - Bailout
duration79.31 ± (79.20 - 79.63) ms82.20 ± (81.82 - 82.24) ms+3.7%✅⬆️
.NET Framework 4.8 - CallTarget+Inlining+NGEN
duration1101.85 ± (1099.74 - 1106.16) ms1109.95 ± (1109.90 - 1118.10) ms+0.7%✅⬆️
.NET Core 3.1 - Baseline
process.internal_duration_ms23.13 ± (23.08 - 23.19) ms23.42 ± (23.37 - 23.47) ms+1.3%✅⬆️
process.time_to_main_ms87.77 ± (87.59 - 87.95) ms89.53 ± (89.32 - 89.73) ms+2.0%✅⬆️
runtime.dotnet.exceptions.count0 ± (0 - 0)0 ± (0 - 0)+0.0%
runtime.dotnet.mem.committed10.89 ± (10.88 - 10.89) MB10.91 ± (10.91 - 10.91) MB+0.2%✅⬆️
runtime.dotnet.threads.count12 ± (12 - 12)12 ± (12 - 12)+0.0%
.NET Core 3.1 - Bailout
process.internal_duration_ms23.07 ± (23.01 - 23.13) ms23.27 ± (23.21 - 23.32) ms+0.8%✅⬆️
process.time_to_main_ms89.26 ± (89.06 - 89.47) ms90.84 ± (90.65 - 91.02) ms+1.8%✅⬆️
runtime.dotnet.exceptions.count0 ± (0 - 0)0 ± (0 - 0)+0.0%
runtime.dotnet.mem.committed10.92 ± (10.92 - 10.93) MB10.95 ± (10.94 - 10.95) MB+0.2%✅⬆️
runtime.dotnet.threads.count13 ± (13 - 13)13 ± (13 - 13)+0.0%
.NET Core 3.1 - CallTarget+Inlining+NGEN
process.internal_duration_ms226.08 ± (224.49 - 227.67) ms227.24 ± (225.93 - 228.55) ms+0.5%✅⬆️
process.time_to_main_ms501.37 ± (500.28 - 502.46) ms507.45 ± (506.60 - 508.29) ms+1.2%✅⬆️
runtime.dotnet.exceptions.count0 ± (0 - 0)0 ± (0 - 0)+0.0%
runtime.dotnet.mem.committed47.21 ± (47.18 - 47.24) MB47.26 ± (47.24 - 47.29) MB+0.1%✅⬆️
runtime.dotnet.threads.count28 ± (28 - 28)28 ± (28 - 28)+0.0%
.NET 6 - Baseline
process.internal_duration_ms21.67 ± (21.63 - 21.72) ms21.89 ± (21.84 - 21.94) ms+1.0%✅⬆️
process.time_to_main_ms75.36 ± (75.19 - 75.53) ms76.58 ± (76.41 - 76.76) ms+1.6%✅⬆️
runtime.dotnet.exceptions.count0 ± (0 - 0)0 ± (0 - 0)+0.0%
runtime.dotnet.mem.committed10.59 ± (10.58 - 10.59) MB10.62 ± (10.62 - 10.62) MB+0.3%✅⬆️
runtime.dotnet.threads.count10 ± (10 - 10)10 ± (10 - 10)+0.0%
.NET 6 - Bailout
process.internal_duration_ms21.74 ± (21.70 - 21.78) ms22.02 ± (21.96 - 22.08) ms+1.3%✅⬆️
process.time_to_main_ms77.20 ± (77.02 - 77.38) ms78.55 ± (78.35 - 78.76) ms+1.8%✅⬆️
runtime.dotnet.exceptions.count0 ± (0 - 0)0 ± (0 - 0)+0.0%
runtime.dotnet.mem.committed10.69 ± (10.69 - 10.70) MB10.73 ± (10.73 - 10.73) MB+0.3%✅⬆️
runtime.dotnet.threads.count11 ± (11 - 11)11 ± (11 - 11)+0.0%
.NET 6 - CallTarget+Inlining+NGEN
process.internal_duration_ms210.30 ± (208.51 - 212.09) ms214.76 ± (213.00 - 216.53) ms+2.1%✅⬆️
process.time_to_main_ms498.70 ± (497.81 - 499.58) ms507.71 ± (506.87 - 508.54) ms+1.8%✅⬆️
runtime.dotnet.exceptions.count0 ± (0 - 0)0 ± (0 - 0)+0.0%
runtime.dotnet.mem.committed49.14 ± (49.11 - 49.18) MB49.27 ± (49.24 - 49.29) MB+0.2%✅⬆️
runtime.dotnet.threads.count29 ± (29 - 29)29 ± (29 - 29)+0.1%✅⬆️
.NET 8 - Baseline
process.internal_duration_ms20.00 ± (19.96 - 20.03) ms20.07 ± (20.02 - 20.11) ms+0.3%✅⬆️
process.time_to_main_ms75.20 ± (75.03 - 75.37) ms76.43 ± (76.26 - 76.59) ms+1.6%✅⬆️
runtime.dotnet.exceptions.count0 ± (0 - 0)0 ± (0 - 0)+0.0%
runtime.dotnet.mem.committed7.69 ± (7.68 - 7.69) MB7.65 ± (7.64 - 7.66) MB-0.4%
runtime.dotnet.threads.count10 ± (10 - 10)10 ± (10 - 10)+0.0%
.NET 8 - Bailout
process.internal_duration_ms20.05 ± (20.01 - 20.09) ms20.24 ± (20.19 - 20.29) ms+0.9%✅⬆️
process.time_to_main_ms76.47 ± (76.36 - 76.58) ms77.58 ± (77.40 - 77.77) ms+1.5%✅⬆️
runtime.dotnet.exceptions.count0 ± (0 - 0)0 ± (0 - 0)+0.0%
runtime.dotnet.mem.committed7.75 ± (7.74 - 7.76) MB7.70 ± (7.69 - 7.71) MB-0.6%
runtime.dotnet.threads.count11 ± (11 - 11)11 ± (11 - 11)+0.0%
.NET 8 - CallTarget+Inlining+NGEN
process.internal_duration_ms169.01 ± (167.98 - 170.03) ms169.97 ± (169.06 - 170.88) ms+0.6%✅⬆️
process.time_to_main_ms482.91 ± (482.14 - 483.68) ms490.22 ± (489.44 - 491.00) ms+1.5%✅⬆️
runtime.dotnet.exceptions.count0 ± (0 - 0)0 ± (0 - 0)+0.0%
runtime.dotnet.mem.committed36.53 ± (36.51 - 36.55) MB36.56 ± (36.54 - 36.58) MB+0.1%✅⬆️
runtime.dotnet.threads.count28 ± (28 - 28)28 ± (28 - 28)+0.0%

HttpMessageHandler

Metric Master (Mean ± 95% CI) Current (Mean ± 95% CI) Change Status
.NET Framework 4.8 - Baseline
duration194.09 ± (193.83 - 194.65) ms195.05 ± (194.78 - 195.44) ms+0.5%✅⬆️
.NET Framework 4.8 - Bailout
duration198.72 ± (198.48 - 199.21) ms198.72 ± (198.54 - 199.31) ms+0.0%✅⬆️
.NET Framework 4.8 - CallTarget+Inlining+NGEN
duration1151.83 ± (1153.69 - 1161.28) ms1161.22 ± (1162.69 - 1170.95) ms+0.8%✅⬆️
.NET Core 3.1 - Baseline
process.internal_duration_ms189.33 ± (188.93 - 189.74) ms189.09 ± (188.67 - 189.50) ms-0.1%
process.time_to_main_ms81.54 ± (81.28 - 81.80) ms81.13 ± (80.94 - 81.31) ms-0.5%
runtime.dotnet.exceptions.count3 ± (3 - 3)3 ± (3 - 3)+0.0%
runtime.dotnet.mem.committed16.11 ± (16.08 - 16.14) MB16.14 ± (16.11 - 16.16) MB+0.1%✅⬆️
runtime.dotnet.threads.count20 ± (20 - 20)20 ± (20 - 20)-0.0%
.NET Core 3.1 - Bailout
process.internal_duration_ms188.78 ± (188.33 - 189.22) ms187.50 ± (187.18 - 187.83) ms-0.7%
process.time_to_main_ms82.75 ± (82.60 - 82.91) ms82.34 ± (82.14 - 82.54) ms-0.5%
runtime.dotnet.exceptions.count3 ± (3 - 3)3 ± (3 - 3)+0.0%
runtime.dotnet.mem.committed16.21 ± (16.18 - 16.25) MB16.14 ± (16.11 - 16.17) MB-0.5%
runtime.dotnet.threads.count21 ± (21 - 21)21 ± (20 - 21)-0.4%
.NET Core 3.1 - CallTarget+Inlining+NGEN
process.internal_duration_ms395.26 ± (393.49 - 397.03) ms396.50 ± (394.86 - 398.15) ms+0.3%✅⬆️
process.time_to_main_ms476.35 ± (475.68 - 477.03) ms476.26 ± (475.53 - 476.99) ms-0.0%
runtime.dotnet.exceptions.count3 ± (3 - 3)3 ± (3 - 3)+0.0%
runtime.dotnet.mem.committed57.83 ± (57.67 - 57.99) MB58.00 ± (57.86 - 58.14) MB+0.3%✅⬆️
runtime.dotnet.threads.count30 ± (29 - 30)30 ± (30 - 30)+0.4%✅⬆️
.NET 6 - Baseline
process.internal_duration_ms192.85 ± (192.48 - 193.21) ms193.06 ± (192.75 - 193.36) ms+0.1%✅⬆️
process.time_to_main_ms70.48 ± (70.28 - 70.67) ms70.73 ± (70.57 - 70.88) ms+0.4%✅⬆️
runtime.dotnet.exceptions.count4 ± (4 - 4)4 ± (4 - 4)+0.0%
runtime.dotnet.mem.committed16.37 ± (16.32 - 16.42) MB16.43 ± (16.37 - 16.48) MB+0.4%✅⬆️
runtime.dotnet.threads.count19 ± (19 - 19)19 ± (19 - 19)+0.6%✅⬆️
.NET 6 - Bailout
process.internal_duration_ms192.90 ± (192.55 - 193.26) ms193.03 ± (192.68 - 193.39) ms+0.1%✅⬆️
process.time_to_main_ms71.34 ± (71.22 - 71.46) ms71.66 ± (71.56 - 71.76) ms+0.4%✅⬆️
runtime.dotnet.exceptions.count4 ± (4 - 4)4 ± (4 - 4)+0.0%
runtime.dotnet.mem.committed16.22 ± (16.09 - 16.35) MB16.42 ± (16.35 - 16.49) MB+1.2%✅⬆️
runtime.dotnet.threads.count19 ± (19 - 19)20 ± (20 - 20)+2.4%✅⬆️
.NET 6 - CallTarget+Inlining+NGEN
process.internal_duration_ms431.44 ± (429.91 - 432.98) ms427.05 ± (425.30 - 428.81) ms-1.0%
process.time_to_main_ms480.93 ± (479.94 - 481.93) ms478.62 ± (477.88 - 479.35) ms-0.5%
runtime.dotnet.exceptions.count4 ± (4 - 4)4 ± (4 - 4)+0.0%
runtime.dotnet.mem.committed60.30 ± (60.24 - 60.36) MB60.35 ± (60.29 - 60.41) MB+0.1%✅⬆️
runtime.dotnet.threads.count30 ± (30 - 31)30 ± (30 - 31)-0.1%
.NET 8 - Baseline
process.internal_duration_ms191.44 ± (191.05 - 191.83) ms190.67 ± (190.30 - 191.05) ms-0.4%
process.time_to_main_ms70.38 ± (70.17 - 70.59) ms70.29 ± (70.06 - 70.53) ms-0.1%
runtime.dotnet.exceptions.count4 ± (4 - 4)4 ± (4 - 4)+0.0%
runtime.dotnet.mem.committed11.77 ± (11.74 - 11.79) MB11.78 ± (11.75 - 11.80) MB+0.1%✅⬆️
runtime.dotnet.threads.count18 ± (18 - 18)18 ± (18 - 18)+0.4%✅⬆️
.NET 8 - Bailout
process.internal_duration_ms192.22 ± (191.75 - 192.68) ms189.92 ± (189.61 - 190.23) ms-1.2%
process.time_to_main_ms71.48 ± (71.33 - 71.64) ms71.06 ± (70.94 - 71.18) ms-0.6%
runtime.dotnet.exceptions.count4 ± (4 - 4)4 ± (4 - 4)+0.0%
runtime.dotnet.mem.committed11.78 ± (11.75 - 11.81) MB11.86 ± (11.83 - 11.90) MB+0.7%✅⬆️
runtime.dotnet.threads.count19 ± (19 - 19)19 ± (19 - 19)-0.5%
.NET 8 - CallTarget+Inlining+NGEN
process.internal_duration_ms359.84 ± (358.19 - 361.49) ms356.37 ± (355.03 - 357.71) ms-1.0%
process.time_to_main_ms460.13 ± (458.99 - 461.26) ms455.33 ± (454.74 - 455.92) ms-1.0%
runtime.dotnet.exceptions.count4 ± (4 - 4)4 ± (4 - 4)+0.0%
runtime.dotnet.mem.committed48.44 ± (48.41 - 48.48) MB48.48 ± (48.44 - 48.51) MB+0.1%✅⬆️
runtime.dotnet.threads.count30 ± (30 - 30)30 ± (30 - 30)+0.1%✅⬆️
Comparison explanation

Execution-time benchmarks measure the whole time it takes to execute a program, and are intended to measure the one-off costs. Cases where the execution time results for the PR are worse than latest master results are highlighted in **red**. The following thresholds were used for comparing the execution times:

  • Welch test with statistical test for significance of 5%
  • Only results indicating a difference greater than 5% and 5 ms are considered.

Note that these results are based on a single point-in-time result for each branch. For full results, see the dashboard.

Graphs show the p99 interval based on the mean and StdDev of the test run, as well as the mean value of the run (shown as a diamond below the graph).

Duration charts
FakeDbCommand (.NET Framework 4.8)
gantt
    title Execution time (ms) FakeDbCommand (.NET Framework 4.8)
    dateFormat  x
    axisFormat %Q
    todayMarker off
    section Baseline
    This PR (7921) - mean (77ms)  : 74, 79
    master - mean (76ms)  : 73, 78

    section Bailout
    This PR (7921) - mean (82ms)  : 80, 84
    master - mean (79ms)  : 77, 82

    section CallTarget+Inlining+NGEN
    This PR (7921) - mean (1,114ms)  : 1052, 1176
    master - mean (1,103ms)  : 1057, 1149

Loading
FakeDbCommand (.NET Core 3.1)
gantt
    title Execution time (ms) FakeDbCommand (.NET Core 3.1)
    dateFormat  x
    axisFormat %Q
    todayMarker off
    section Baseline
    This PR (7921) - mean (120ms)  : 116, 124
    master - mean (118ms)  : 115, 122

    section Bailout
    This PR (7921) - mean (121ms)  : 119, 124
    master - mean (120ms)  : 116, 123

    section CallTarget+Inlining+NGEN
    This PR (7921) - mean (772ms)  : 744, 799
    master - mean (763ms)  : 741, 785

Loading
FakeDbCommand (.NET 6)
gantt
    title Execution time (ms) FakeDbCommand (.NET 6)
    dateFormat  x
    axisFormat %Q
    todayMarker off
    section Baseline
    This PR (7921) - mean (105ms)  : 103, 108
    master - mean (104ms)  : 100, 107

    section Bailout
    This PR (7921) - mean (107ms)  : 104, 110
    master - mean (105ms)  : 102, 109

    section CallTarget+Inlining+NGEN
    This PR (7921) - mean (762ms)  : 718, 806
    master - mean (748ms)  : 709, 786

Loading
FakeDbCommand (.NET 8)
gantt
    title Execution time (ms) FakeDbCommand (.NET 8)
    dateFormat  x
    axisFormat %Q
    todayMarker off
    section Baseline
    This PR (7921) - mean (105ms)  : 102, 108
    master - mean (103ms)  : 100, 106

    section Bailout
    This PR (7921) - mean (106ms)  : 104, 108
    master - mean (104ms)  : 103, 106

    section CallTarget+Inlining+NGEN
    This PR (7921) - mean (701ms)  : 667, 734
    master - mean (693ms)  : 662, 723

Loading
HttpMessageHandler (.NET Framework 4.8)
gantt
    title Execution time (ms) HttpMessageHandler (.NET Framework 4.8)
    dateFormat  x
    axisFormat %Q
    todayMarker off
    section Baseline
    This PR (7921) - mean (195ms)  : 192, 199
    master - mean (194ms)  : 190, 199

    section Bailout
    This PR (7921) - mean (199ms)  : 195, 203
    master - mean (199ms)  : 195, 202

    section CallTarget+Inlining+NGEN
    This PR (7921) - mean (1,167ms)  : 1106, 1227
    master - mean (1,157ms)  : 1102, 1213

Loading
HttpMessageHandler (.NET Core 3.1)
gantt
    title Execution time (ms) HttpMessageHandler (.NET Core 3.1)
    dateFormat  x
    axisFormat %Q
    todayMarker off
    section Baseline
    This PR (7921) - mean (279ms)  : 273, 285
    master - mean (280ms)  : 273, 287

    section Bailout
    This PR (7921) - mean (278ms)  : 274, 283
    master - mean (280ms)  : 274, 286

    section CallTarget+Inlining+NGEN
    This PR (7921) - mean (905ms)  : 865, 945
    master - mean (907ms)  : 881, 933

Loading
HttpMessageHandler (.NET 6)
gantt
    title Execution time (ms) HttpMessageHandler (.NET 6)
    dateFormat  x
    axisFormat %Q
    todayMarker off
    section Baseline
    This PR (7921) - mean (272ms)  : 268, 277
    master - mean (272ms)  : 266, 277

    section Bailout
    This PR (7921) - mean (273ms)  : 269, 278
    master - mean (272ms)  : 268, 277

    section CallTarget+Inlining+NGEN
    This PR (7921) - mean (939ms)  : 905, 973
    master - mean (948ms)  : 908, 987

Loading
HttpMessageHandler (.NET 8)
gantt
    title Execution time (ms) HttpMessageHandler (.NET 8)
    dateFormat  x
    axisFormat %Q
    todayMarker off
    section Baseline
    This PR (7921) - mean (271ms)  : 265, 276
    master - mean (271ms)  : 266, 277

    section Bailout
    This PR (7921) - mean (270ms)  : 266, 274
    master - mean (273ms)  : 267, 280

    section CallTarget+Inlining+NGEN
    This PR (7921) - mean (844ms)  : 814, 873
    master - mean (853ms)  : 811, 896

Loading

@zacharycmontoya zacharycmontoya force-pushed the zach.montoya/otel-baggage-sync-APMAPI-1736 branch 3 times, most recently from a2c8de8 to 0633f47 Compare February 3, 2026 16:12
@zacharycmontoya zacharycmontoya added area:automatic-instrumentation Automatic instrumentation managed C# code (Datadog.Trace.ClrProfiler.Managed) area:opentelemetry OpenTelemetry support labels Feb 3, 2026
@zacharycmontoya zacharycmontoya force-pushed the zach.montoya/otel-baggage-sync-APMAPI-1736 branch 2 times, most recently from 17bb9a3 to 60625e2 Compare February 6, 2026 02:03
SetInstrumentationVerification();

await Fixture.TryStartApp(this);
string[] baggageItems = [
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note for reviewers: All of the test cases start with this baggage as the original contents. However, the OpenTelemetry Baggage API currently treats keys in a case-insensitive manner, so the final baggage output will be missing one of the baggage items below whose keys only differ by case. This is not aligned with our Datadog implementation and more importantly with the OpenTelemetry specification, so I'll submit a PR upstream to fix that. Once that change is shipped, we can then update the OpenTelemetry.Api package and update our snapshots.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've posted a PR upstream to get the OTel API to use case-sensitive baggage names: open-telemetry/opentelemetry-dotnet#6931

@zacharycmontoya zacharycmontoya marked this pull request as ready for review February 6, 2026 02:37
@zacharycmontoya zacharycmontoya requested review from a team as code owners February 6, 2026 02:37
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 60625e2113

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Base automatically changed from zach.montoya/instrument-static-valuetype to master February 10, 2026 15:23
@zacharycmontoya zacharycmontoya requested a review from a team as a code owner February 10, 2026 15:23
…t updates to OpenTelemetry.Baggage.Current are mirrored to Datadog.Trace.Baggage.Current, and vice-versa (though they will only be observed when retrieving the latest OpenTelemetry.Baggage.Current value).
…nTelemetry.Baggage API's without passing a OpenTelemetry.Baggage argument, the underlying OpenTelemetry.Baggage.Current is used as the source for the modifications. This change makes sure that the underlying OpenTelemetry.Baggage.Current store is updated with the contents of Datadog.Trace.Baggage.Current before the modifications are made.
- OpenTelemetry.Baggage.GetBaggage(Baggage)
- OpenTelemetry.Baggage.GetBaggage(string, Baggage)
- OpenTelemetry.Baggage.GetEnumerator(Baggage)
…ntegration. The test cases hit every method and ensures that we correctly update OpenTelemetry.Baggage.Current
…e OpenTelemetry.Baggage.Current when the user has provided the default Baggage struct. Otherwise, the Baggage argument provided by the user will be used as the source for baggage modification so there's no reason to update OpenTelemetry.Baggage.Current.
@zacharycmontoya zacharycmontoya force-pushed the zach.montoya/otel-baggage-sync-APMAPI-1736 branch from 60625e2 to 61edb29 Compare February 10, 2026 20:33
@zacharycmontoya zacharycmontoya requested a review from a team as a code owner February 19, 2026 20:51
Copy link
Copy Markdown
Member

@andrewlock andrewlock left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haven't finished yet, but added some comments/questions to get you started 🙂


internal interface IBaggageHolder
{
[DuckField(Name = "Baggage")]
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OMG the casing inconsistency in this code base 😅

// Additional notes:
// - Since the Datadog Baggage model is mutable (allowing the user to get the Datadog.Trace.Baggage.Current once and continue to mutate that reference),
// we must clear then add the new baggage items.
// - The API can be invoked with an arbitrary OpenTelemetry.Baggage object passed via the parameter, so we must replace all baggage items every time
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To clarify, if the user calls RemoveBaggage("mykey", someBaggage), and passes in a baggage object, that (updated) baggage is set to be the new Baggage.Current value, correct? i.e. all of the SetBaggage and RemoveBaggage() APIs always manipulate the Current baggage, passing in an explicit baggage object just manipulates the "starting" point?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. Haha.

For the static methods, the baggage update will take place on the baggage object passed in or Baggage.Current if nothing is passed in. Then the resulting baggage is set as the new value of Baggage.Current.

// Note: When the user sets OpenTelemetry.Baggage.Current, those changes will override the contents of Datadog.Trace.Baggage.Current,
// so we can always consider Datadog.Trace.Baggage.Current as being up-to-date.
var baggageHolder = apiBaggage.EnsureBaggageHolder();
baggageHolder.Baggage = apiBaggage.Create(Baggage.Current.ToDictionary(kvp => kvp.Key, kvp => kvp.Value));
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given the OTel APIs treat baggage in a case-insensitive manner, what happens if we set the same key here? Are the duplicate keys overwritten, or does it throw?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No exception, the entry just gets overwritten. But I'm resolving this with open-telemetry/opentelemetry-dotnet#6931

…odEnd updates, since the OpenTelemetry.Baggage.Current only happens when no exceptions are thrown.
…nt by utilizing the default value type instance that's passed into the method by our updated bytecode instrumentation.
Copy link
Copy Markdown
Member

@andrewlock andrewlock left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM in general, just one main comment about tweaking Baggage to have an AsDictionary() method + expose the enumerator, but they're non-blocking optimizations (same for most other comments!)

Comment on lines +64 to +65
// Enable OpenTelemetry interop to test that the OTel Baggage API integration works correctly.
SetEnvironmentVariable("DD_TRACE_OTEL_ENABLED", "true");
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably set this in all of our aspnetcore fixtures, regardless, so that we can catch any cases of weird regressions? 🤔 WDYT? We could do it in a separate PR obviously

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep sounds good to me!

@gh-worker-ownership-write-b05516 gh-worker-ownership-write-b05516 bot removed the request for review from a team March 11, 2026 13:00
…' tag on the server span if there's no baggage. This requires updating the ClearBaggage test since it clears all existing baggage from the current request.
…dating Datadog.Trace.Baggage.Current during the OnMethodEnd handler and only if there was no exception thrown in the Setter.
…ing baggage contents in a thread safe manner, which is used to update OpenTelemetry.Baggage.Current. Also make the GetEnumerator() method public
Copy link
Copy Markdown
Collaborator

@bouwkast bouwkast left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM I just did a quick view of the auto-instrumentation changes and a glance at the snapshots

@zacharycmontoya zacharycmontoya merged commit da64a83 into master Mar 19, 2026
140 checks passed
@zacharycmontoya zacharycmontoya deleted the zach.montoya/otel-baggage-sync-APMAPI-1736 branch March 19, 2026 21:21
@github-actions github-actions bot added this to the vNext-v3 milestone Mar 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:automatic-instrumentation Automatic instrumentation managed C# code (Datadog.Trace.ClrProfiler.Managed) area:opentelemetry OpenTelemetry support

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants