Skip to content

feat(FeatureFlags): FFE APM feature-flag span enrichment - #8795

Merged
pavlokhrebto merged 12 commits into
masterfrom
leo.romanovsky/ffe-apm-span-enrichment
Jul 29, 2026
Merged

feat(FeatureFlags): FFE APM feature-flag span enrichment#8795
pavlokhrebto merged 12 commits into
masterfrom
leo.romanovsky/ffe-apm-span-enrichment

Conversation

@leoromanovsky

@leoromanovsky leoromanovsky commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Motivation

  • Add experimental FFE APM span enrichment for OpenFeature evaluations in dd-trace-dotnet.
  • Emit the ffe_* wire format used by dd-trace-js#8343 so backend/Trino decode uses the same tags and encodings.
  • Keep enrichment disabled by default behind DD_EXPERIMENTAL_FLAGGING_PROVIDER_SPAN_ENRICHMENT_ENABLED.

Changes

  • Adds DD_EXPERIMENTAL_FLAGGING_PROVIDER_SPAN_ENRICHMENT_ENABLED in TracerSettings and supported-configurations.yaml.
  • Adds split serial ID support to the UFC Split model.
  • Adds ULeb128Encoder for delta-varint feature-flag serial ID encoding.
  • Adds per-root-span SpanEnrichmentState and SpanEnrichmentStore.
  • Adds SpanEnrichmentHook for the OpenFeature Finally hook path.
  • Adds manual-instrumentation integrations for the OpenFeature hook path.
  • Writes ffe_flags_enc, ffe_subjects_enc, and ffe_runtime_defaults on Span.Finish.
  • Adds synchronization and snapshotting around SpanEnrichmentState mutation and ToSpanTags.
  • Adds a never-throw guard around the Span.Finish enrichment block.
  • Bounds SpanEnrichmentStore growth for root spans that never finish.
  • Unwraps OpenFeature.Model.Value runtime defaults into plain CLR values before JSON serialization.
  • Adds xUnit coverage for required span-enrichment cases, max-200 serial IDs, concurrency, never-throw behavior, and store growth bounds.
  • Updates generated source-generator outputs.

Decisions

  • The feature is opt-in and off by default.
  • No store or accumulator is created when DD_EXPERIMENTAL_FLAGGING_PROVIDER_SPAN_ENRICHMENT_ENABLED=false.
  • No ffe_* tags are emitted when the gate is off.
  • State is keyed by local root span and cleaned up on root-span finish and provider close.
  • Store growth is bounded for root spans that never finish.
  • ffe_* tags are bare span meta tags, not _dd.-prefixed tags.
  • Subject keys are SHA256 hashes and are emitted only when doLog=true.
  • Runtime default values are JSON-serialized as the caller value, not as the OpenFeature wrapper object.

Validation

  • Frozen system-tests parametric suite:
TEST_LIBRARY=dotnet ./run.sh PARAMETRIC -k span_enrichment
Library: dotnet@3.46.0
===================== 18 passed, 2605 deselected in 46.26s =====================
  • FFE dogfooding local trace-intake run:

    • Provider path: DatadogProvider / OpenFeature client.
    • Root span: aspnet_core.request.
    • Service: ffe-dogfooding-dotnet.
    • Gate on aggregation: ffe_flags_enc decoded to [829, 1442, 2311, 2312].
    • Gate on subjects: ffe_subjects_enc contained 2 SHA256 subject keys.
    • Gate off: 3 spans flushed; 0 ffe_* tags.
    • Runtime defaults: unicode string and object defaults serialized as raw UTF-8; long unicode value truncated to 64 codepoints.
    • Codec parity: ZAgUAg== decoded to [100, 108, 128, 130].
  • Staging Datadog APM proof:

@datadog-prod-us1-5

This comment has been minimized.

@dd-trace-dotnet-ci-bot

dd-trace-dotnet-ci-bot Bot commented Jun 16, 2026

Copy link
Copy Markdown

Execution-Time Benchmarks Report ⏱️

Execution-time results for samples comparing This PR (8795) 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
duration71.09 ± (71.22 - 71.56) ms71.22 ± (71.27 - 71.62) ms+0.2%✅⬆️
.NET Framework 4.8 - Bailout
duration78.44 ± (78.23 - 78.65) ms78.77 ± (78.27 - 78.81) ms+0.4%✅⬆️
.NET Framework 4.8 - CallTarget+Inlining+NGEN
duration1087.58 ± (1088.24 - 1096.26) ms1087.47 ± (1086.83 - 1091.83) ms-0.0%
.NET Core 3.1 - Baseline
process.internal_duration_ms22.34 ± (22.30 - 22.38) ms22.18 ± (22.14 - 22.23) ms-0.7%
process.time_to_main_ms83.44 ± (83.15 - 83.73) ms83.39 ± (83.10 - 83.68) ms-0.1%
runtime.dotnet.exceptions.count0 ± (0 - 0)0 ± (0 - 0)+0.0%
runtime.dotnet.mem.committed11.02 ± (11.02 - 11.02) MB11.00 ± (11.00 - 11.01) MB-0.2%
runtime.dotnet.threads.count12 ± (12 - 12)12 ± (12 - 12)+0.0%
.NET Core 3.1 - Bailout
process.internal_duration_ms22.08 ± (22.05 - 22.11) ms22.28 ± (22.24 - 22.32) ms+0.9%✅⬆️
process.time_to_main_ms83.80 ± (83.58 - 84.01) ms84.70 ± (84.46 - 84.93) ms+1.1%✅⬆️
runtime.dotnet.exceptions.count0 ± (0 - 0)0 ± (0 - 0)+0.0%
runtime.dotnet.mem.committed11.05 ± (11.05 - 11.06) MB11.03 ± (11.03 - 11.04) MB-0.2%
runtime.dotnet.threads.count13 ± (13 - 13)13 ± (13 - 13)+0.0%
.NET Core 3.1 - CallTarget+Inlining+NGEN
process.internal_duration_ms210.06 ± (209.04 - 211.08) ms210.03 ± (209.19 - 210.86) ms-0.0%
process.time_to_main_ms535.65 ± (534.43 - 536.87) ms535.00 ± (533.86 - 536.13) ms-0.1%
runtime.dotnet.exceptions.count0 ± (0 - 0)0 ± (0 - 0)+0.0%
runtime.dotnet.mem.committed49.63 ± (49.59 - 49.66) MB49.48 ± (49.44 - 49.52) MB-0.3%
runtime.dotnet.threads.count28 ± (28 - 28)28 ± (28 - 28)+0.3%✅⬆️
.NET 6 - Baseline
process.internal_duration_ms21.01 ± (20.97 - 21.05) ms21.03 ± (20.99 - 21.07) ms+0.1%✅⬆️
process.time_to_main_ms72.48 ± (72.23 - 72.73) ms72.93 ± (72.71 - 73.16) ms+0.6%✅⬆️
runtime.dotnet.exceptions.count0 ± (0 - 0)0 ± (0 - 0)+0.0%
runtime.dotnet.mem.committed10.73 ± (10.73 - 10.73) MB10.73 ± (10.73 - 10.74) MB+0.0%✅⬆️
runtime.dotnet.threads.count10 ± (10 - 10)10 ± (10 - 10)+0.0%
.NET 6 - Bailout
process.internal_duration_ms20.92 ± (20.89 - 20.96) ms20.80 ± (20.76 - 20.84) ms-0.6%
process.time_to_main_ms74.15 ± (73.93 - 74.38) ms72.34 ± (72.18 - 72.49) ms-2.4%
runtime.dotnet.exceptions.count0 ± (0 - 0)0 ± (0 - 0)+0.0%
runtime.dotnet.mem.committed10.85 ± (10.84 - 10.85) MB10.85 ± (10.85 - 10.86) MB+0.1%✅⬆️
runtime.dotnet.threads.count11 ± (11 - 11)11 ± (11 - 11)+0.0%
.NET 6 - CallTarget+Inlining+NGEN
process.internal_duration_ms371.46 ± (369.21 - 373.71) ms372.23 ± (370.11 - 374.34) ms+0.2%✅⬆️
process.time_to_main_ms542.81 ± (541.75 - 543.87) ms544.08 ± (543.06 - 545.11) ms+0.2%✅⬆️
runtime.dotnet.exceptions.count0 ± (0 - 0)0 ± (0 - 0)+0.0%
runtime.dotnet.mem.committed50.63 ± (50.61 - 50.66) MB50.65 ± (50.63 - 50.68) MB+0.0%✅⬆️
runtime.dotnet.threads.count28 ± (28 - 28)28 ± (28 - 28)-0.1%
.NET 8 - Baseline
process.internal_duration_ms18.94 ± (18.92 - 18.97) ms19.04 ± (19.02 - 19.07) ms+0.5%✅⬆️
process.time_to_main_ms69.41 ± (69.28 - 69.54) ms70.09 ± (69.96 - 70.22) ms+1.0%✅⬆️
runtime.dotnet.exceptions.count0 ± (0 - 0)0 ± (0 - 0)+0.0%
runtime.dotnet.mem.committed7.76 ± (7.75 - 7.77) MB7.77 ± (7.77 - 7.77) MB+0.1%✅⬆️
runtime.dotnet.threads.count10 ± (10 - 10)10 ± (10 - 10)+0.0%
.NET 8 - Bailout
process.internal_duration_ms18.93 ± (18.91 - 18.96) ms19.16 ± (19.12 - 19.19) ms+1.2%✅⬆️
process.time_to_main_ms71.12 ± (71.02 - 71.21) ms73.68 ± (73.46 - 73.90) ms+3.6%✅⬆️
runtime.dotnet.exceptions.count0 ± (0 - 0)0 ± (0 - 0)+0.0%
runtime.dotnet.mem.committed7.80 ± (7.80 - 7.81) MB7.84 ± (7.83 - 7.84) MB+0.4%✅⬆️
runtime.dotnet.threads.count11 ± (11 - 11)11 ± (11 - 11)+0.0%
.NET 8 - CallTarget+Inlining+NGEN
process.internal_duration_ms294.12 ± (291.89 - 296.36) ms296.38 ± (293.84 - 298.92) ms+0.8%✅⬆️
process.time_to_main_ms494.95 ± (494.05 - 495.85) ms495.70 ± (494.64 - 496.76) ms+0.2%✅⬆️
runtime.dotnet.exceptions.count0 ± (0 - 0)0 ± (0 - 0)+0.0%
runtime.dotnet.mem.committed38.01 ± (37.98 - 38.04) MB38.15 ± (38.12 - 38.18) MB+0.4%✅⬆️
runtime.dotnet.threads.count27 ± (27 - 27)27 ± (27 - 27)-0.0%

HttpMessageHandler

Metric Master (Mean ± 95% CI) Current (Mean ± 95% CI) Change Status
.NET Framework 4.8 - Baseline
duration190.96 ± (190.79 - 191.36) ms194.88 ± (194.68 - 195.57) ms+2.1%✅⬆️
.NET Framework 4.8 - Bailout
duration195.11 ± (195.00 - 195.40) ms198.16 ± (198.18 - 198.71) ms+1.6%✅⬆️
.NET Framework 4.8 - CallTarget+Inlining+NGEN
duration1167.72 ± (1168.94 - 1176.12) ms1183.59 ± (1185.53 - 1192.54) ms+1.4%✅⬆️
.NET Core 3.1 - Baseline
process.internal_duration_ms184.49 ± (184.27 - 184.72) ms187.04 ± (186.64 - 187.45) ms+1.4%✅⬆️
process.time_to_main_ms80.53 ± (80.37 - 80.69) ms81.55 ± (81.34 - 81.76) ms+1.3%✅⬆️
runtime.dotnet.exceptions.count3 ± (3 - 3)3 ± (3 - 3)+0.0%
runtime.dotnet.mem.committed16.30 ± (16.24 - 16.36) MB16.28 ± (16.25 - 16.31) MB-0.1%
runtime.dotnet.threads.count19 ± (19 - 20)20 ± (20 - 20)+1.3%✅⬆️
.NET Core 3.1 - Bailout
process.internal_duration_ms183.91 ± (183.69 - 184.14) ms186.51 ± (186.21 - 186.81) ms+1.4%✅⬆️
process.time_to_main_ms81.82 ± (81.70 - 81.93) ms83.04 ± (82.88 - 83.20) ms+1.5%✅⬆️
runtime.dotnet.exceptions.count3 ± (3 - 3)3 ± (3 - 3)+0.0%
runtime.dotnet.mem.committed16.39 ± (16.37 - 16.42) MB16.34 ± (16.31 - 16.36) MB-0.4%
runtime.dotnet.threads.count21 ± (20 - 21)20 ± (20 - 21)-0.5%
.NET Core 3.1 - CallTarget+Inlining+NGEN
process.internal_duration_ms375.07 ± (374.00 - 376.14) ms377.98 ± (376.66 - 379.29) ms+0.8%✅⬆️
process.time_to_main_ms524.25 ± (523.42 - 525.08) ms531.36 ± (530.29 - 532.43) ms+1.4%✅⬆️
runtime.dotnet.exceptions.count3 ± (3 - 3)3 ± (3 - 3)+0.0%
runtime.dotnet.mem.committed57.97 ± (57.91 - 58.04) MB57.94 ± (57.86 - 58.01) MB-0.1%
runtime.dotnet.threads.count30 ± (30 - 30)30 ± (30 - 30)-0.3%
.NET 6 - Baseline
process.internal_duration_ms188.38 ± (188.15 - 188.60) ms191.96 ± (191.55 - 192.36) ms+1.9%✅⬆️
process.time_to_main_ms70.36 ± (70.20 - 70.53) ms71.60 ± (71.40 - 71.81) ms+1.8%✅⬆️
runtime.dotnet.exceptions.count4 ± (4 - 4)4 ± (4 - 4)+0.0%
runtime.dotnet.mem.committed16.39 ± (16.24 - 16.54) MB16.48 ± (16.39 - 16.58) MB+0.5%✅⬆️
runtime.dotnet.threads.count19 ± (18 - 19)19 ± (19 - 19)+1.0%✅⬆️
.NET 6 - Bailout
process.internal_duration_ms187.71 ± (187.53 - 187.89) ms190.24 ± (189.95 - 190.52) ms+1.3%✅⬆️
process.time_to_main_ms71.20 ± (71.15 - 71.26) ms72.55 ± (72.41 - 72.69) ms+1.9%✅⬆️
runtime.dotnet.exceptions.count4 ± (4 - 4)4 ± (4 - 4)+0.0%
runtime.dotnet.mem.committed15.91 ± (15.74 - 16.08) MB16.30 ± (16.16 - 16.45) MB+2.5%✅⬆️
runtime.dotnet.threads.count19 ± (18 - 19)19 ± (19 - 19)+3.8%✅⬆️
.NET 6 - CallTarget+Inlining+NGEN
process.internal_duration_ms577.97 ± (574.63 - 581.31) ms587.15 ± (584.52 - 589.77) ms+1.6%✅⬆️
process.time_to_main_ms532.87 ± (531.97 - 533.76) ms539.78 ± (538.76 - 540.81) ms+1.3%✅⬆️
runtime.dotnet.exceptions.count4 ± (4 - 4)4 ± (4 - 4)+0.0%
runtime.dotnet.mem.committed61.48 ± (61.45 - 61.51) MB61.70 ± (61.62 - 61.78) MB+0.4%✅⬆️
runtime.dotnet.threads.count31 ± (31 - 31)31 ± (31 - 31)-0.1%
.NET 8 - Baseline
process.internal_duration_ms186.57 ± (186.34 - 186.79) ms189.03 ± (188.68 - 189.38) ms+1.3%✅⬆️
process.time_to_main_ms69.77 ± (69.58 - 69.96) ms70.83 ± (70.63 - 71.03) ms+1.5%✅⬆️
runtime.dotnet.exceptions.count4 ± (4 - 4)4 ± (4 - 4)+0.0%
runtime.dotnet.mem.committed11.92 ± (11.89 - 11.95) MB11.96 ± (11.94 - 11.99) MB+0.3%✅⬆️
runtime.dotnet.threads.count18 ± (18 - 18)18 ± (18 - 18)+0.1%✅⬆️
.NET 8 - Bailout
process.internal_duration_ms185.79 ± (185.60 - 185.98) ms188.76 ± (188.53 - 189.00) ms+1.6%✅⬆️
process.time_to_main_ms70.64 ± (70.56 - 70.71) ms72.18 ± (72.04 - 72.32) ms+2.2%✅⬆️
runtime.dotnet.exceptions.count4 ± (4 - 4)4 ± (4 - 4)+0.0%
runtime.dotnet.mem.committed11.60 ± (11.47 - 11.72) MB12.01 ± (11.99 - 12.03) MB+3.6%✅⬆️
runtime.dotnet.threads.count18 ± (18 - 18)19 ± (19 - 19)+5.9%✅⬆️
.NET 8 - CallTarget+Inlining+NGEN
process.internal_duration_ms513.20 ± (510.51 - 515.89) ms509.05 ± (506.40 - 511.71) ms-0.8%
process.time_to_main_ms483.41 ± (482.78 - 484.03) ms491.88 ± (491.17 - 492.60) ms+1.8%✅⬆️
runtime.dotnet.exceptions.count4 ± (4 - 4)4 ± (4 - 4)+0.0%
runtime.dotnet.mem.committed51.35 ± (51.31 - 51.38) MB51.49 ± (51.46 - 51.52) MB+0.3%✅⬆️
runtime.dotnet.threads.count29 ± (29 - 29)29 ± (29 - 29)+0.3%✅⬆️
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 (8795) - mean (71ms)  : 69, 74
    master - mean (71ms)  : 69, 74

    section Bailout
    This PR (8795) - mean (79ms)  : 74, 83
    master - mean (78ms)  : 75, 82

    section CallTarget+Inlining+NGEN
    This PR (8795) - mean (1,089ms)  : 1054, 1125
    master - mean (1,092ms)  : 1035, 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 (8795) - mean (113ms)  : 106, 119
    master - mean (113ms)  : 107, 119

    section Bailout
    This PR (8795) - mean (114ms)  : 109, 119
    master - mean (113ms)  : 108, 118

    section CallTarget+Inlining+NGEN
    This PR (8795) - mean (781ms)  : 761, 801
    master - mean (784ms)  : 766, 802

Loading
FakeDbCommand (.NET 6)
gantt
    title Execution time (ms) FakeDbCommand (.NET 6)
    dateFormat  x
    axisFormat %Q
    todayMarker off
    section Baseline
    This PR (8795) - mean (100ms)  : 96, 105
    master - mean (100ms)  : 94, 106

    section Bailout
    This PR (8795) - mean (99ms)  : 97, 101
    master - mean (102ms)  : 96, 107

    section CallTarget+Inlining+NGEN
    This PR (8795) - mean (944ms)  : 911, 978
    master - mean (944ms)  : 908, 980

Loading
FakeDbCommand (.NET 8)
gantt
    title Execution time (ms) FakeDbCommand (.NET 8)
    dateFormat  x
    axisFormat %Q
    todayMarker off
    section Baseline
    This PR (8795) - mean (96ms)  : 92, 100
    master - mean (95ms)  : 93, 97

    section Bailout
    This PR (8795) - mean (100ms)  : 96, 105
    master - mean (97ms)  : 95, 98

    section CallTarget+Inlining+NGEN
    This PR (8795) - mean (822ms)  : 782, 862
    master - mean (818ms)  : 785, 850

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 (8795) - mean (195ms)  : 191, 199
    master - mean (191ms)  : 188, 194

    section Bailout
    This PR (8795) - mean (198ms)  : 196, 201
    master - mean (195ms)  : 193, 197

    section CallTarget+Inlining+NGEN
    This PR (8795) - mean (1,189ms)  : 1138, 1240
    master - mean (1,173ms)  : 1121, 1224

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 (8795) - mean (278ms)  : 272, 284
    master - mean (274ms)  : 271, 277

    section Bailout
    This PR (8795) - mean (279ms)  : 274, 283
    master - mean (274ms)  : 271, 277

    section CallTarget+Inlining+NGEN
    This PR (8795) - mean (949ms)  : 935, 964
    master - mean (942ms)  : 926, 958

Loading
HttpMessageHandler (.NET 6)
gantt
    title Execution time (ms) HttpMessageHandler (.NET 6)
    dateFormat  x
    axisFormat %Q
    todayMarker off
    section Baseline
    This PR (8795) - mean (272ms)  : 266, 278
    master - mean (267ms)  : 264, 271

    section Bailout
    This PR (8795) - mean (272ms)  : 267, 276
    master - mean (267ms)  : 265, 270

    section CallTarget+Inlining+NGEN
    This PR (8795) - mean (1,157ms)  : 1121, 1192
    master - mean (1,140ms)  : 1083, 1196

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

    section Bailout
    This PR (8795) - mean (271ms)  : 267, 274
    master - mean (266ms)  : 263, 269

    section CallTarget+Inlining+NGEN
    This PR (8795) - mean (1,035ms)  : 991, 1078
    master - mean (1,028ms)  : 986, 1070

Loading

@pr-commenter

pr-commenter Bot commented Jun 16, 2026

Copy link
Copy Markdown

Benchmarks

Benchmark execution time: 2026-07-29 09:55:05

Comparing candidate commit d074d7c in PR branch leo.romanovsky/ffe-apm-span-enrichment with baseline commit 4e79e39 in branch master.

📊 Benchmarking dashboard

Found 0 performance improvements and 1 performance regressions! Performance is the same for 71 metrics, 0 unstable metrics, 59 known flaky benchmarks, 67 flaky benchmarks without significant changes.

Explanation

This is an A/B test comparing a candidate commit's performance against that of a baseline commit. Performance changes are noted in the tables below as:

  • 🟩 = significantly better candidate vs. baseline
  • 🟥 = significantly worse candidate vs. baseline

We compute a confidence interval (CI) over the relative difference of means between metrics from the candidate and baseline commits, considering the baseline as the reference.

If the CI is entirely outside the configured SIGNIFICANT_IMPACT_THRESHOLD (or the deprecated UNCONFIDENCE_THRESHOLD), the change is considered significant.

Feel free to reach out to #apm-benchmarking-platform on Slack if you have any questions.

More details about the CI and significant changes

You can imagine this CI as a range of values that is likely to contain the true difference of means between the candidate and baseline commits.

CIs of the difference of means are often centered around 0%, because often changes are not that big:

---------------------------------(------|---^--------)-------------------------------->
                              -0.6%    0%  0.3%     +1.2%
                                 |          |        |
         lower bound of the CI --'          |        |
sample mean (center of the CI) -------------'        |
         upper bound of the CI ----------------------'

As described above, a change is considered significant if the CI is entirely outside the configured SIGNIFICANT_IMPACT_THRESHOLD (or the deprecated UNCONFIDENCE_THRESHOLD).

For instance, for an execution time metric, this confidence interval indicates a significantly worse performance:

----------------------------------------|---------|---(---------^---------)---------->
                                       0%        1%  1.3%      2.2%      3.1%
                                                  |   |         |         |
       significant impact threshold --------------'   |         |         |
                      lower bound of CI --------------'         |         |
       sample mean (center of the CI) --------------------------'         |
                      upper bound of CI ----------------------------------'

scenario:Benchmarks.Trace.HttpClientBenchmark.SendAsync net472

  • 🟥 throughput [-4799.202op/s; -4612.226op/s] or [-5.479%; -5.265%]

Known flaky benchmarks

These benchmarks are marked as flaky and will not trigger a failure. Modify FLAKY_BENCHMARKS_REGEX to control which benchmarks are marked as flaky.

scenario:Benchmarks.Trace.ActivityBenchmark.StartStopWithChild net472

  • 🟥 throughput [-10346.175op/s; -9924.630op/s] or [-12.267%; -11.768%]

scenario:Benchmarks.Trace.ActivityBenchmark.StartStopWithChild netcoreapp3.1

  • 🟥 throughput [-10697.543op/s; -9369.596op/s] or [-10.877%; -9.527%]

scenario:Benchmarks.Trace.AgentWriterBenchmark.WriteAndFlushEnrichedTraces net472

  • 🟥 execution_time [+299.766ms; +301.365ms] or [+148.754%; +149.548%]
  • 🟥 throughput [-47.733op/s; -44.109op/s] or [-8.588%; -7.936%]

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

  • 🟥 execution_time [+380.080ms; +382.003ms] or [+300.286%; +301.805%]
  • 🟩 throughput [+95.430op/s; +102.376op/s] or [+12.582%; +13.498%]

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

  • 🟥 execution_time [+390.236ms; +393.271ms] or [+345.344%; +348.030%]

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

  • 🟥 allocated_mem [+4.692KB; +4.693KB] or [+98.785%; +98.801%]
  • 🟥 throughput [-59866.181op/s; -59492.146op/s] or [-46.579%; -46.288%]

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

  • 🟥 allocated_mem [+3.816KB; +3.816KB] or [+80.699%; +80.711%]
  • 🟩 execution_time [-15.333ms; -11.131ms] or [-7.161%; -5.198%]
  • 🟥 throughput [-59842.493op/s; -57083.199op/s] or [-43.682%; -41.668%]

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

  • 🟥 allocated_mem [+4.544KB; +4.544KB] or [+98.261%; +98.274%]
  • 🟥 throughput [-48567.880op/s; -46323.454op/s] or [-43.911%; -41.882%]

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

  • 🟥 allocated_mem [+1.315KB; +1.315KB] or [+106.388%; +106.404%]
  • 🟥 throughput [-258380.774op/s; -255370.416op/s] or [-26.382%; -26.075%]

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

  • 🟥 allocated_mem [+479 bytes; +480 bytes] or [+39.212%; +39.221%]
  • 🟩 execution_time [-24.430ms; -19.518ms] or [-10.895%; -8.704%]
  • 🟥 throughput [-80037.947op/s; -56786.683op/s] or [-8.551%; -6.067%]

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

  • 🟥 allocated_mem [+1.280KB; +1.280KB] or [+105.947%; +105.963%]
  • 🟥 throughput [-152716.480op/s; -136503.999op/s] or [-21.942%; -19.613%]

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

  • 🟥 allocated_mem [+3.378KB; +3.378KB] or [+89.003%; +89.017%]
  • 🟥 throughput [-72562.527op/s; -71787.233op/s] or [-48.834%; -48.312%]

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

  • 🟥 allocated_mem [+3.336KB; +3.336KB] or [+88.150%; +88.161%]
  • 🟥 throughput [-72664.719op/s; -69751.955op/s] or [-46.235%; -44.382%]

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

  • 🟥 allocated_mem [+3.264KB; +3.264KB] or [+88.493%; +88.506%]
  • 🟥 throughput [-56286.768op/s; -53658.474op/s] or [-44.840%; -42.746%]

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

  • 🟩 throughput [+292164.597op/s; +314169.915op/s] or [+9.742%; +10.476%]

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

  • 🟩 execution_time [-18.032ms; -13.157ms] or [-8.312%; -6.065%]

scenario:Benchmarks.Trace.Asm.AppSecEncoderBenchmark.EncodeArgs net472

  • 🟥 execution_time [+299.002ms; +299.828ms] or [+149.401%; +149.814%]

scenario:Benchmarks.Trace.Asm.AppSecEncoderBenchmark.EncodeArgs net6.0

  • 🟥 execution_time [+300.297ms; +303.831ms] or [+151.441%; +153.223%]

scenario:Benchmarks.Trace.Asm.AppSecEncoderBenchmark.EncodeArgs netcoreapp3.1

  • 🟥 execution_time [+299.478ms; +301.922ms] or [+150.854%; +152.085%]

scenario:Benchmarks.Trace.Asm.AppSecEncoderBenchmark.EncodeLegacyArgs net472

  • 🟥 execution_time [+296.653ms; +297.582ms] or [+145.704%; +146.161%]

scenario:Benchmarks.Trace.Asm.AppSecEncoderBenchmark.EncodeLegacyArgs net6.0

  • 🟥 execution_time [+295.895ms; +299.465ms] or [+144.652%; +146.398%]

scenario:Benchmarks.Trace.Asm.AppSecEncoderBenchmark.EncodeLegacyArgs netcoreapp3.1

  • 🟥 execution_time [+298.174ms; +299.491ms] or [+149.027%; +149.685%]

scenario:Benchmarks.Trace.Asm.AppSecWafBenchmark.RunWafRealisticBenchmarkWithAttack net6.0

  • 🟥 execution_time [+24.131µs; +47.751µs] or [+7.704%; +15.244%]
  • 🟥 throughput [-443.026op/s; -244.277op/s] or [-13.810%; -7.615%]

scenario:Benchmarks.Trace.AspNetCoreBenchmark.SendRequest net472

  • 🟥 execution_time [+299.559ms; +300.360ms] or [+149.511%; +149.910%]

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

  • unstable execution_time [+315.061ms; +389.793ms] or [+342.326%; +423.526%]
  • 🟩 throughput [+832.383op/s; +1049.570op/s] or [+6.840%; +8.624%]

scenario:Benchmarks.Trace.AspNetCoreBenchmark.SendRequest netcoreapp3.1

  • 🟥 execution_time [+365.210ms; +368.664ms] or [+277.301%; +279.923%]

scenario:Benchmarks.Trace.CIVisibilityProtocolWriterBenchmark.WriteAndFlushEnrichedTraces net472

  • unstable execution_time [+331.113ms; +381.919ms] or [+152.243%; +175.603%]
  • 🟥 throughput [-524.094op/s; -481.289op/s] or [-47.488%; -43.609%]

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

  • unstable execution_time [+206.440ms; +339.691ms] or [+87.976%; +144.762%]
  • 🟥 throughput [-671.235op/s; -587.795op/s] or [-44.772%; -39.206%]

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

  • 🟥 execution_time [+327.003ms; +334.834ms] or [+195.586%; +200.270%]
  • 🟥 throughput [-387.556op/s; -351.667op/s] or [-26.985%; -24.486%]

scenario:Benchmarks.Trace.CharSliceBenchmark.OptimizedCharSliceWithPool netcoreapp3.1

  • unstable execution_time [-265.876µs; -32.127µs] or [-14.244%; -1.721%]
  • unstable throughput [+36.615op/s; +181.835op/s] or [+6.834%; +33.941%]

scenario:Benchmarks.Trace.ElasticsearchBenchmark.CallElasticsearch net472

  • 🟥 execution_time [+302.561ms; +304.316ms] or [+152.364%; +153.248%]

scenario:Benchmarks.Trace.ElasticsearchBenchmark.CallElasticsearch net6.0

  • 🟥 execution_time [+300.884ms; +302.417ms] or [+150.774%; +151.542%]

scenario:Benchmarks.Trace.ElasticsearchBenchmark.CallElasticsearch netcoreapp3.1

  • 🟥 execution_time [+300.645ms; +304.493ms] or [+151.031%; +152.964%]

scenario:Benchmarks.Trace.ElasticsearchBenchmark.CallElasticsearchAsync net472

  • 🟥 execution_time [+300.326ms; +302.520ms] or [+150.814%; +151.915%]

scenario:Benchmarks.Trace.ElasticsearchBenchmark.CallElasticsearchAsync net6.0

  • 🟥 execution_time [+300.216ms; +302.823ms] or [+148.444%; +149.732%]

scenario:Benchmarks.Trace.ElasticsearchBenchmark.CallElasticsearchAsync netcoreapp3.1

  • 🟥 execution_time [+301.381ms; +305.011ms] or [+152.753%; +154.593%]

scenario:Benchmarks.Trace.GraphQLBenchmark.ExecuteAsync net472

  • 🟥 execution_time [+300.817ms; +302.411ms] or [+150.983%; +151.783%]

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

  • 🟥 execution_time [+296.298ms; +300.114ms] or [+147.677%; +149.579%]
  • 🟩 throughput [+42465.013op/s; +48770.887op/s] or [+8.432%; +9.684%]

scenario:Benchmarks.Trace.GraphQLBenchmark.ExecuteAsync netcoreapp3.1

  • 🟥 execution_time [+299.860ms; +302.681ms] or [+149.178%; +150.581%]

scenario:Benchmarks.Trace.ILoggerBenchmark.EnrichedLog net6.0

  • 🟩 execution_time [-16.235ms; -12.588ms] or [-7.549%; -5.853%]

scenario:Benchmarks.Trace.Iast.StringAspectsBenchmark.StringConcatAspectBenchmark net472

  • unstable execution_time [+14.942µs; +58.684µs] or [+3.691%; +14.495%]

scenario:Benchmarks.Trace.Iast.StringAspectsBenchmark.StringConcatAspectBenchmark net6.0

  • 🟩 allocated_mem [-21.226KB; -21.204KB] or [-7.743%; -7.735%]
  • unstable execution_time [-43.962µs; +12.621µs] or [-8.689%; +2.494%]
  • unstable throughput [-40.889op/s; +159.534op/s] or [-2.040%; +7.961%]

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

  • 🟩 allocated_mem [-16.820KB; -16.799KB] or [-6.132%; -6.124%]
  • unstable execution_time [-47.581µs; +15.676µs] or [-8.245%; +2.717%]
  • unstable throughput [-33.220op/s; +143.132op/s] or [-1.898%; +8.177%]

scenario:Benchmarks.Trace.Iast.StringAspectsBenchmark.StringConcatBenchmark net6.0

  • unstable execution_time [+10.019µs; +17.100µs] or [+23.681%; +40.419%]
  • 🟥 throughput [-6433.243op/s; -4065.566op/s] or [-27.082%; -17.115%]

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

  • unstable execution_time [-14.379µs; -6.163µs] or [-22.308%; -9.562%]
  • unstable throughput [+1632.658op/s; +3434.934op/s] or [+10.017%; +21.074%]

scenario:Benchmarks.Trace.Log4netBenchmark.EnrichedLog net472

  • 🟥 execution_time [+303.319ms; +304.662ms] or [+153.314%; +153.993%]

scenario:Benchmarks.Trace.Log4netBenchmark.EnrichedLog net6.0

  • 🟥 execution_time [+302.254ms; +305.994ms] or [+153.846%; +155.750%]

scenario:Benchmarks.Trace.Log4netBenchmark.EnrichedLog netcoreapp3.1

  • 🟥 execution_time [+299.351ms; +302.582ms] or [+149.862%; +151.480%]

scenario:Benchmarks.Trace.SerilogBenchmark.EnrichedLog net472

  • 🟥 execution_time [+296.934ms; +299.441ms] or [+147.995%; +149.244%]

scenario:Benchmarks.Trace.SerilogBenchmark.EnrichedLog net6.0

  • 🟥 execution_time [+302.379ms; +304.518ms] or [+151.841%; +152.914%]

scenario:Benchmarks.Trace.SerilogBenchmark.EnrichedLog netcoreapp3.1

  • 🟥 execution_time [+304.030ms; +307.174ms] or [+154.185%; +155.779%]

scenario:Benchmarks.Trace.SingleSpanAspNetCoreBenchmark.SingleSpanAspNetCore net472

  • 🟥 execution_time [+299.261ms; +300.278ms] or [+149.273%; +149.780%]
  • 🟩 throughput [+66118049.933op/s; +66518263.599op/s] or [+48.151%; +48.443%]

scenario:Benchmarks.Trace.SingleSpanAspNetCoreBenchmark.SingleSpanAspNetCore net6.0

  • 🟥 execution_time [+428.070ms; +433.968ms] or [+532.381%; +539.716%]

scenario:Benchmarks.Trace.SingleSpanAspNetCoreBenchmark.SingleSpanAspNetCore netcoreapp3.1

  • 🟥 execution_time [+298.686ms; +299.824ms] or [+148.978%; +149.545%]

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

  • 🟩 throughput [+92744.922op/s; +100944.695op/s] or [+8.659%; +9.425%]

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

  • 🟩 throughput [+67167.652op/s; +97278.085op/s] or [+5.199%; +7.529%]

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

  • 🟩 throughput [+55187.074op/s; +67669.648op/s] or [+5.481%; +6.721%]

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

  • 🟩 throughput [+43427.815op/s; +48100.078op/s] or [+7.886%; +8.734%]

scenario:Benchmarks.Trace.TraceAnnotationsBenchmark.RunOnMethodBegin net6.0

  • 🟩 throughput [+61295.273op/s; +78667.142op/s] or [+6.848%; +8.789%]

Known flaky benchmarks without significant changes:

  • scenario:Benchmarks.OpenTelemetry.InstrumentedApi.Trace.ActivityBenchmark.StartSpan net472
  • scenario:Benchmarks.OpenTelemetry.InstrumentedApi.Trace.ActivityBenchmark.StartSpan net6.0
  • scenario:Benchmarks.OpenTelemetry.InstrumentedApi.Trace.ActivityBenchmark.StartSpan netcoreapp3.1
  • scenario:Benchmarks.OpenTelemetry.InstrumentedApi.Trace.ActivityBenchmark.StartSpan_AddEvent_Sampled net472
  • scenario:Benchmarks.OpenTelemetry.InstrumentedApi.Trace.ActivityBenchmark.StartSpan_AddEvent_Sampled net6.0
  • scenario:Benchmarks.OpenTelemetry.InstrumentedApi.Trace.ActivityBenchmark.StartSpan_AddEvent_Sampled netcoreapp3.1
  • scenario:Benchmarks.OpenTelemetry.InstrumentedApi.Trace.ActivityBenchmark.StartSpan_GetContext_Sampled net472
  • scenario:Benchmarks.OpenTelemetry.InstrumentedApi.Trace.ActivityBenchmark.StartSpan_GetContext_Sampled net6.0
  • scenario:Benchmarks.OpenTelemetry.InstrumentedApi.Trace.ActivityBenchmark.StartSpan_GetContext_Sampled netcoreapp3.1
  • scenario:Benchmarks.OpenTelemetry.InstrumentedApi.Trace.ActivityBenchmark.StartSpan_SetAttributes_Sampled net472
  • scenario:Benchmarks.OpenTelemetry.InstrumentedApi.Trace.ActivityBenchmark.StartSpan_SetAttributes_Sampled net6.0
  • scenario:Benchmarks.OpenTelemetry.InstrumentedApi.Trace.ActivityBenchmark.StartSpan_SetAttributes_Sampled netcoreapp3.1
  • scenario:Benchmarks.OpenTelemetry.InstrumentedApi.Trace.ActivityBenchmark.StartSpan_SetStatus_Sampled net472
  • scenario:Benchmarks.OpenTelemetry.InstrumentedApi.Trace.ActivityBenchmark.StartSpan_SetStatus_Sampled net6.0
  • scenario:Benchmarks.OpenTelemetry.InstrumentedApi.Trace.ActivityBenchmark.StartSpan_SetStatus_Sampled netcoreapp3.1
  • scenario:Benchmarks.OpenTelemetry.InstrumentedApi.Trace.ActivityBenchmark.StartSpan_UpdateName_Sampled net472
  • scenario:Benchmarks.OpenTelemetry.InstrumentedApi.Trace.ActivityBenchmark.StartSpan_UpdateName_Sampled net6.0
  • scenario:Benchmarks.OpenTelemetry.InstrumentedApi.Trace.ActivityBenchmark.StartSpan_UpdateName_Sampled netcoreapp3.1
  • scenario:Benchmarks.OpenTelemetry.InstrumentedApi.Trace.TelemetrySpanBenchmark.StartSpan net472
  • scenario:Benchmarks.OpenTelemetry.InstrumentedApi.Trace.TelemetrySpanBenchmark.StartSpan net6.0
  • scenario:Benchmarks.OpenTelemetry.InstrumentedApi.Trace.TelemetrySpanBenchmark.StartSpan netcoreapp3.1
  • scenario:Benchmarks.OpenTelemetry.InstrumentedApi.Trace.TelemetrySpanBenchmark.StartSpan_AddEvent_Sampled net472
  • scenario:Benchmarks.OpenTelemetry.InstrumentedApi.Trace.TelemetrySpanBenchmark.StartSpan_AddEvent_Sampled net6.0
  • scenario:Benchmarks.OpenTelemetry.InstrumentedApi.Trace.TelemetrySpanBenchmark.StartSpan_AddEvent_Sampled netcoreapp3.1
  • scenario:Benchmarks.OpenTelemetry.InstrumentedApi.Trace.TelemetrySpanBenchmark.StartSpan_GetContext_Sampled net472
  • scenario:Benchmarks.OpenTelemetry.InstrumentedApi.Trace.TelemetrySpanBenchmark.StartSpan_GetContext_Sampled net6.0
  • scenario:Benchmarks.OpenTelemetry.InstrumentedApi.Trace.TelemetrySpanBenchmark.StartSpan_GetContext_Sampled netcoreapp3.1
  • scenario:Benchmarks.OpenTelemetry.InstrumentedApi.Trace.TelemetrySpanBenchmark.StartSpan_RecordException_Sampled net472
  • scenario:Benchmarks.OpenTelemetry.InstrumentedApi.Trace.TelemetrySpanBenchmark.StartSpan_RecordException_Sampled net6.0
  • scenario:Benchmarks.OpenTelemetry.InstrumentedApi.Trace.TelemetrySpanBenchmark.StartSpan_RecordException_Sampled netcoreapp3.1
  • scenario:Benchmarks.OpenTelemetry.InstrumentedApi.Trace.TelemetrySpanBenchmark.StartSpan_SetAttributes_Sampled net472
  • scenario:Benchmarks.OpenTelemetry.InstrumentedApi.Trace.TelemetrySpanBenchmark.StartSpan_SetAttributes_Sampled net6.0
  • scenario:Benchmarks.OpenTelemetry.InstrumentedApi.Trace.TelemetrySpanBenchmark.StartSpan_SetAttributes_Sampled netcoreapp3.1
  • scenario:Benchmarks.OpenTelemetry.InstrumentedApi.Trace.TelemetrySpanBenchmark.StartSpan_SetStatus_Sampled net472
  • scenario:Benchmarks.OpenTelemetry.InstrumentedApi.Trace.TelemetrySpanBenchmark.StartSpan_SetStatus_Sampled net6.0
  • scenario:Benchmarks.OpenTelemetry.InstrumentedApi.Trace.TelemetrySpanBenchmark.StartSpan_SetStatus_Sampled netcoreapp3.1
  • scenario:Benchmarks.OpenTelemetry.InstrumentedApi.Trace.TelemetrySpanBenchmark.StartSpan_UpdateName_Sampled net472
  • scenario:Benchmarks.OpenTelemetry.InstrumentedApi.Trace.TelemetrySpanBenchmark.StartSpan_UpdateName_Sampled net6.0
  • scenario:Benchmarks.OpenTelemetry.InstrumentedApi.Trace.TelemetrySpanBenchmark.StartSpan_UpdateName_Sampled netcoreapp3.1
  • scenario:Benchmarks.Trace.ActivityBenchmark.StartStopWithChild net6.0
  • scenario:Benchmarks.Trace.Asm.AppSecBodyBenchmark.ObjectExtractorSimpleBody net472
  • scenario:Benchmarks.Trace.Asm.AppSecWafBenchmark.RunWafRealisticBenchmark net472
  • scenario:Benchmarks.Trace.Asm.AppSecWafBenchmark.RunWafRealisticBenchmark net6.0
  • scenario:Benchmarks.Trace.Asm.AppSecWafBenchmark.RunWafRealisticBenchmark netcoreapp3.1
  • scenario:Benchmarks.Trace.Asm.AppSecWafBenchmark.RunWafRealisticBenchmarkWithAttack net472
  • scenario:Benchmarks.Trace.Asm.AppSecWafBenchmark.RunWafRealisticBenchmarkWithAttack netcoreapp3.1
  • scenario:Benchmarks.Trace.CharSliceBenchmark.OptimizedCharSlice net472
  • scenario:Benchmarks.Trace.CharSliceBenchmark.OptimizedCharSlice net6.0
  • scenario:Benchmarks.Trace.CharSliceBenchmark.OptimizedCharSlice netcoreapp3.1
  • scenario:Benchmarks.Trace.CharSliceBenchmark.OptimizedCharSliceWithPool net472
  • scenario:Benchmarks.Trace.CharSliceBenchmark.OptimizedCharSliceWithPool net6.0
  • scenario:Benchmarks.Trace.CharSliceBenchmark.OriginalCharSlice net472
  • scenario:Benchmarks.Trace.CharSliceBenchmark.OriginalCharSlice net6.0
  • scenario:Benchmarks.Trace.CharSliceBenchmark.OriginalCharSlice netcoreapp3.1
  • scenario:Benchmarks.Trace.ILoggerBenchmark.EnrichedLog net472
  • scenario:Benchmarks.Trace.ILoggerBenchmark.EnrichedLog netcoreapp3.1
  • scenario:Benchmarks.Trace.Iast.StringAspectsBenchmark.StringConcatBenchmark net472
  • scenario:Benchmarks.Trace.RedisBenchmark.SendReceive net472
  • scenario:Benchmarks.Trace.RedisBenchmark.SendReceive net6.0
  • scenario:Benchmarks.Trace.RedisBenchmark.SendReceive netcoreapp3.1
  • scenario:Benchmarks.Trace.SpanBenchmark.StartFinishScope net472
  • scenario:Benchmarks.Trace.SpanBenchmark.StartFinishScope netcoreapp3.1
  • scenario:Benchmarks.Trace.SpanBenchmark.StartFinishSpan net472
  • scenario:Benchmarks.Trace.SpanBenchmark.StartFinishTwoScopes net472
  • scenario:Benchmarks.Trace.SpanBenchmark.StartFinishTwoScopes netcoreapp3.1
  • scenario:Benchmarks.Trace.TraceAnnotationsBenchmark.RunOnMethodBegin net472
  • scenario:Benchmarks.Trace.TraceAnnotationsBenchmark.RunOnMethodBegin netcoreapp3.1

@leoromanovsky leoromanovsky added type:new-feature area:tracer The core tracer library (Datadog.Trace, does not include OpenTracing, native code, or integrations) feature_flags labels Jun 17, 2026
@leoromanovsky
leoromanovsky marked this pull request as ready for review June 22, 2026 14:18
@leoromanovsky
leoromanovsky requested review from a team as code owners June 22, 2026 14:18
@leoromanovsky
leoromanovsky requested review from dd-oleksii, typotter and vandonr and removed request for a team June 22, 2026 14:18
@leoromanovsky leoromanovsky changed the title feat(FeatureFlags): FFE APM feature-flag span enrichment (experimental, gated) feat(FeatureFlags): FFE APM feature-flag span enrichment Jun 22, 2026
@leoromanovsky
leoromanovsky requested review from dromanol and removed request for vandonr June 22, 2026 14:18

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

Copy link
Copy Markdown

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: b294dd157d

ℹ️ 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".

Comment thread tracer/src/Datadog.Trace/FeatureFlags/SpanEnrichmentStore.cs Outdated

@andrewlock andrewlock left a comment

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.

I haven't finished, but figured there's a lot to be going on with, so would give some initial feedback!

The bulk of it is:

  • Do we need this to be a public API, if not, it's preferable to not
  • Not a fan of the AI comments, they add a lot of noise but not much value IMO😅
  • I would really rather avoid the global static accesses, these are a nightmare for testing, and a source of ongoing flakiness in our tests

Comment thread tracer/src/Datadog.FeatureFlags.OpenFeature/Sdk/FeatureFlagsSdk.cs Outdated
Comment thread tracer/src/Datadog.FeatureFlags.OpenFeature/Sdk/FeatureFlagsSdk.cs Outdated
Comment thread tracer/src/Datadog.FeatureFlags.OpenFeature/Sdk/FeatureFlagsSdk.cs Outdated
Comment thread tracer/src/Datadog.FeatureFlags.OpenFeature/Sdk/FeatureFlagsSdk.cs Outdated
Comment thread tracer/src/Datadog.FeatureFlags.OpenFeature/Sdk/FeatureFlagsSdk.cs Outdated
Comment thread tracer/src/Datadog.Trace/FeatureFlags/SpanEnrichmentState.cs Outdated
Comment thread tracer/src/Datadog.Trace/FeatureFlags/SpanEnrichmentState.cs Outdated
Comment thread tracer/src/Datadog.Trace/FeatureFlags/SpanEnrichmentState.cs Outdated
Comment thread tracer/src/Datadog.Trace/FeatureFlags/SpanEnrichmentState.cs Outdated
Comment thread tracer/src/Datadog.Trace/FeatureFlags/SpanEnrichmentState.cs Outdated
@leoromanovsky
leoromanovsky requested a review from andrewlock June 23, 2026 12:23
Comment thread tracer/src/Datadog.Trace/TracerManager.cs Outdated

@andrewlock andrewlock left a comment

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.

I started looking through the detailed changes, and they look better thanks 👍

However, reading again, I have some fundamental concerns about the overall design. This seems to be adding a "central" singleton store which is then hit whenever a trace is closed. That requires a bunch of synchronization and other work to make it thread safe, but it also forces you to do a whole bunch of work in the hot path.

What's more, I'm not sure that this actually flows async context correctly? Won't you end up with random spans being tagged with feature flags from completely different traces (which were running concurrently)?

Can/should we take a different design, where you have a per-trace-context span enrichment object. That way you get proper async-context flow, eliminate most of the lock contention, and can delay serialization yo background threads?

Comment thread tracer/src/Datadog.FeatureFlags.OpenFeature/SpanEnrichmentHook.cs Outdated
Comment thread tracer/src/Datadog.Trace/TracerManager.cs Outdated
// Serial id of the experiment split, used for APM span enrichment.
// Nullable: absent in UFC shapes that predate span enrichment. Deserialized
// from the UFC "serialId" field (Newtonsoft case-insensitive matching).
[JsonProperty("serialId")]

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.

Is this property really necessary? 🤔 It seems like all the other properties would need it if so. Case-insensitive matching seems to suggest that this property is not required?

Comment thread tracer/src/Datadog.Trace/FeatureFlags/SpanEnrichmentState.cs Outdated
Comment thread tracer/src/Datadog.Trace/FeatureFlags/SpanEnrichmentState.cs Outdated
Comment thread tracer/src/Datadog.Trace/Span.cs Outdated
Comment thread tracer/src/Datadog.Trace/FeatureFlags/SpanEnrichmentState.cs Outdated
@pavlokhrebto

pavlokhrebto commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Hey @andrewlock ! Thanks a lot for the detailed review — really helpful. Went through all of it, here's where I landed.

Most of your feedback centers on the central SpanEnrichmentStore. The direction I think we should take: move the state onto the trace itself (a lazy field on TraceContext) instead of a shared store. That kills the store, the 10k cap, the eviction/leak machinery and the lock contention, makes the "wrong trace gets tagged" case impossible (no shared map to look up wrong), and removes the global state behind the testing/statics comment.

The one part I think we'd rather not touch is the serializer (SpanMessagePackFormatter) — i.e. moving the tag encoding off Span.Finish() onto the background write path. It looks like a core and byte-level with a big blast radius, so I'd prefer not to change it blind. Proposal: we do the state-on-trace redesign + all the smaller fixes now (which already gets the shared-store lookup + locking out of the hot path), and leave the serializer deferral to the team, or do with your guidance as a follow-up. Whatever you prefer.

Rundown of your comments and what I'm proposing:

Comment Proposed fix
Central store, hot-path work, lock contention State on TraceContext; delete the store
Spans tagged from other concurrent traces? Impossible once state is per-trace; I'll add a test to confirm
Defer serialization to the background path The serializer bit above — leave to you or do with guidance
Static/global access bad for testing (+ host in the FF module, not TracerManager/TracerSettings) Global store gone; wiring out of TracerManager; only stateless instrumentation statics remain
SpanId — what about 128-bit ids? Moot — no id keying anymore, state lives on the trace
Clear() vs Dispose() Cleanup path deleted with the store
Truncation "isn't really UTF-8 safe" Keep the plain substring, drop the misleading comment
AsDictionary() allocation Applying your foreach + presized-dict suggestion
Boxing on the runtime-default path Fixed (is Value)
Hand-rolled JSON / built-in helpers Using the built-in JSON helper, dropping bespoke code
Duplicated metadata-key constants Move to a shared constants file
AI/Node comments + doc wording Pruning them; applying your doc suggestions
public vs internal Tightening to internal where possible
Split back-compat / is the field needed Confirm it's optional + version-tolerant, document, drop if unused
Collection expressions, HashSet vs SortedSet, blank lines, lock comment Applying the nits

A few questions for you:

  1. State home: I lean TraceContext (it owns RootSpan) over a field on Span — any objection?
  2. Cleanup ABI: OK to delete ClearSpanEnrichment + its integration, or keep a no-op stub for manual-instrumentation version compat?
  3. Cap: no global store to bound anymore; per-trace limits (200/10/20/5/64) stay. Any case that still needs an extra ceiling? (I don't think so)

We'll get the state-on-trace redesign + fixes onto a stacked branch.

Two clarifications on the state-on-trace plan so I'm not overselling it:

  • Per-trace state still needs light synchronization — a single trace can accumulate from concurrent async evaluations, so there's still a small per-trace lock (or a lock-free structure). What goes away is the cross-trace contention on the shared map, not locking entirely.
  • Without the serializer change, the encoding + tag-write still happen at Span.Finish() (just reading the trace's own state instead of the shared store). So this removes the shared-store lookup and cross-trace locking from the hot path, but the encode + SetTag stay at finish — moving those off is exactly what the serializer deferral would do. Flagging so the split is clear.

Thanks again 🙏

catch (Exception ex)
{
// Enrichment must never break flag evaluation.
System.Diagnostics.Debug.WriteLine($"[Datadog] SpanEnrichmentHook.FinallyAsync failed: {ex}");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This log will only be visible in dev/staging.
DatadogLogging.GetLoggerFor + Log.Debug would surface this error in prod logging aiui

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Good instinct, but DatadogLogging isn't reachable from here: Datadog.FeatureFlags.OpenFeature is a standalone shim with no reference to Datadog.Trace — it only source-links a few contract files (IEvaluation, FeatureFlagMetadataKeys, etc.), and DatadogLogging is internal. Wiring it up would mean taking a dependency the package intentionally avoids.

The accumulation that matters actually runs in the tracer's CallTarget integration and SpanEnrichmentState.Accumulate, both of which already log at Debug inside Datadog.Trace. This catch only guards local arg-building in the shim, so Debug.WriteLine is really just a dev aid — happy to drop it entirely if you'd prefer nothing here.

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.

The https://www.nuget.org/packages/OpenFeature package depends on the .NET logging packages, which suggests that the "correct" place to log this is using the .NET ILogger abstraction. That said, given we don't use it anywhere here currently, and plumbing it through is prob a bit of a pain, my preference is to just remove the System.Diagnostics.Debug.WriteLine anyway. It literally isn't in the compiled code we ship, so it mostly ends up just being noise

// lock and performs encoding/JSON serialization after releasing it.
private readonly object _gate = new();

private readonly HashSet<long> _serialIds = new();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Would using a SortedSet here and moving the sorting logic to AddSerialId instead of sorting the whole list in ToSpanTag be a useful shift of work? Wanting to optimize for the flush pushes the sort work to AddSerialId which is probably called a lot more often? Seems cleaner to pass the sorted set around instead of hash set to array/collection back to sorted set.
not blocking, but something to consider.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Considered it, but I think HashSet + sort-at-flush is the better fit here:

  • The ULEB128 encoder is the frozen cross-SDK codec and already dedupes + sorts internally, so a pre-sorted set wouldn't remove the flush-time sort unless we changed the encoder (which has to stay byte-identical).
  • AddSerialId is the hot accumulate path — HashSet keeps inserts O(1) and we sort once at flush. SortedSet would push O(log n) onto every insert, i.e. onto the path that's called most often.

So I'd keep HashSet.

Thanks though — good to sanity-check the work placement.

pavlokhrebto added a commit that referenced this pull request Jul 7, 2026
…d of a central store (#8869)

## Summary of changes

Stacked on top of #8795. Replaces the central `SpanEnrichmentStore` with
per-trace FFE span-enrichment state stored on `TraceContext`, and folds
in the review nits from that PR. The `ffe_*` tags are still written at
`Span.Finish()`, now reading the trace's own state. Wire output is
unchanged (byte-identical).

## Reason for change

The review on #8795 flagged that a process-wide
`ConcurrentDictionary<rootSpanId, state>` is suboptimal: it forces
synchronization and hot-path lookups for every root span, needs a
cap/eviction/warn to avoid leaks, and — because it keys by span id —
risks tagging spans with feature flags from a different concurrent
trace. Storing the state on the trace itself removes all of that.

## Implementation details

- **State on `TraceContext`** — a lazily-created `SpanEnrichmentState?`,
exposed via `FeatureFlagEnrichment` /
`GetOrCreateFeatureFlagEnrichment()`. It lives and dies with the trace,
so there is no central map, no `SpanId` key, no cap, and no eviction.
- **Accumulate path** — the OpenFeature and native evaluate integrations
write directly to the active trace's state, gated on
`Settings.IsSpanEnrichmentEnabled`. Because an evaluation can only ever
touch its own trace's state, cross-trace contamination is structurally
impossible.
- **Write path** — `Span.Finish()` reads
`Context.TraceContext?.FeatureFlagEnrichment` for the local root span;
no store lookup, no gate. Errors are logged at `Debug` to avoid per-span
log flooding.
- **Deletions** — `SpanEnrichmentStore`, its `TracerManager` wiring, the
`ClearSpanEnrichment` seam/integration, and the provider-close bridge
are all removed. `IsSpanEnrichmentEnabled` reads the setting directly.
- **Concurrency** — `SpanEnrichmentState` keeps its per-instance lock
(concurrent evals under one trace, e.g. `Task.WhenAll`); there is no
shared-map contention.
- **Review nits** — tightened member visibility, pre-sized
`AsDictionary()` in the OpenFeature structure conversion, documented
`Split.SerialId` as optional for config skew, and removed cross-SDK
comment references.

The frozen wire contract (tag names, ULEB128 encoding, SHA256 hashing,
per-eval limits, golden vector) is unchanged.

## Test coverage

- Existing codec golden-vector / round-trip and accumulator cap/dedupe
tests, retargeted off the deleted store onto `SpanEnrichmentState`.
- New: lazy state creation with a stable instance; two separate traces
never cross-contaminate; `ffe_*` tags land on the local root only, never
on child spans; gate-off allocates no state and emits nothing.

## Other details

The native `generated_calltargets.g.cpp` entry for the removed
`ClearSpanEnrichment` call target was removed by hand (the Nuke
generator can't run on macOS); CI regeneration on Windows/Linux should
confirm it's byte-identical. The store-era `SpanFinish_EnrichmentThrows`
test was dropped because it depended on a test-only fault hook on the
store; the never-throw guard remains in `Span.Finish()`.
@pavlokhrebto

Copy link
Copy Markdown
Contributor

@andrewlock comments addressed in #8869 and merged in this branch

@pavlokhrebto
pavlokhrebto requested a review from andrewlock July 7, 2026 11:24
@pavlokhrebto

pavlokhrebto commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

@andrewlock — ready for another look. The redesign from your review is in: enrichment state now lives on TraceContext (created lazily + gated), the central SpanEnrichmentStore is gone, and the write reads the trace's own state. Quick map of your comments → what changed:

  • Central store / hot-path work / async-context — replaced by per-trace state on TraceContext; SpanEnrichmentStore and its cap/eviction/cleanup are deleted. No shared map, so no cross-trace mixing and no cross-request lock contention. (Encoding still happens at Span.Finish() — moving it into the serializer is the one thing I deliberately left out, since it's the only piece touching SpanMessagePackFormatter; happy to do it as a follow-up or leave it to your team.)
  • Static/global access + "host it in the FeatureFlags module" — the global store is gone, nothing static holds state anymore.
  • Clear() vs Dispose() — moot: the store and its cleanup are deleted; state is GC'd with the trace.
  • AsDictionary() allocation — applied your suggestion (presized dict + foreach).
  • Collection expressions — applied.
  • Split serialId — is it necessary? — yes: serialId is the integer that gets encoded into ffe_flags_enc; without it there's nothing to encode for the flags tag, so it's kept.

No cap and no serializer deferral, which also matches what the other SDKs (JS/Python/Ruby) do. I've resolved the threads that are done and left this so the mapping is in one place

@pavlokhrebto
pavlokhrebto dismissed andrewlock’s stale review July 7, 2026 12:24

Dismissing to unblock the PR - @andrewlock is PTO for the next 2 weeks and his comments were addressed.
Will ask someone else to check

@aarsilv
aarsilv requested a review from Copilot July 13, 2026 22:09

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds an opt-in, experimental Feature Flags (FFE) APM span-enrichment pipeline for OpenFeature evaluations in dd-trace-dotnet, emitting the cross-SDK ffe_* tag wire format and integrating enrichment into the root-span finish path.

Changes:

  • Introduces span-enrichment state/codec (SpanEnrichmentState, ULeb128Encoder, SHA256 subject hashing) and writes ffe_* tags on root Span.Finish.
  • Extends feature-flag evaluation metadata (split serial id + do_log) and adds OpenFeature hook + manual-instrumentation calltargets to accumulate enrichment.
  • Adds unit + integration tests and updates generated calltarget/config outputs for the new env var gate (DD_EXPERIMENTAL_FLAGGING_PROVIDER_SPAN_ENRICHMENT_ENABLED).

Reviewed changes

Copilot reviewed 24 out of 33 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tracer/test/test-applications/integrations/Samples.FeatureFlags/Program.cs Adds an enrich mode to wrap evaluations in a root+child span for integration testing.
tracer/test/Datadog.Trace.Tests/FeatureFlags/SpanEnrichmentTests.cs New unit tests for codec, accumulation limits, concurrency, and Span.Finish “never throw” behavior.
tracer/test/Datadog.Trace.Tests/FeatureFlags/FeatureFlagsEvaluatorTests.cs Adds Split JSON deserialization tests for optional serialId.
tracer/test/Datadog.Trace.TestHelpers/FeatureFlagsHelpers.cs Adds deterministic serial ids to test flag models for span-enrichment assertions.
tracer/test/Datadog.Trace.ClrProfiler.IntegrationTests/FeatureFlags/SpanEnrichmentIntegrationTests.cs New integration tests validating ffe_* tags land on root only and are gated off by default.
tracer/src/Datadog.Tracer.Native/Generated/generated_calltargets.g.cpp Updates native calltarget definitions (generated) for new OpenFeature methods/integrations.
tracer/src/Datadog.Trace/Util/Sha256Helper.cs Adds SHA256 helper for subject-key hashing used by span enrichment.
tracer/src/Datadog.Trace/TraceContext.cs Adds per-trace lazy SpanEnrichmentState storage and accessor.
tracer/src/Datadog.Trace/Span.cs Writes ffe_* tags during root-span Finish, guarded and never-throw.
tracer/src/Datadog.Trace/Generated/netstandard2.0/Datadog.Trace.SourceGenerators/InstrumentationDefinitionsGenerator/InstrumentationDefinitions.g.cs Registers new OpenFeature manual instrumentation integrations (generated).
tracer/src/Datadog.Trace/Generated/netstandard2.0/Datadog.Trace.SourceGenerators/ConfigurationKeysGenerator/ConfigurationKeys.FeatureFlags.g.cs Adds generated config key constant for span-enrichment gate.
tracer/src/Datadog.Trace/Generated/netcoreapp3.1/Datadog.Trace.SourceGenerators/InstrumentationDefinitionsGenerator/InstrumentationDefinitions.g.cs Registers new OpenFeature manual instrumentation integrations (generated).
tracer/src/Datadog.Trace/Generated/netcoreapp3.1/Datadog.Trace.SourceGenerators/ConfigurationKeysGenerator/ConfigurationKeys.FeatureFlags.g.cs Adds generated config key constant for span-enrichment gate.
tracer/src/Datadog.Trace/Generated/net6.0/Datadog.Trace.SourceGenerators/InstrumentationDefinitionsGenerator/InstrumentationDefinitions.g.cs Registers new OpenFeature manual instrumentation integrations (generated).
tracer/src/Datadog.Trace/Generated/net6.0/Datadog.Trace.SourceGenerators/ConfigurationKeysGenerator/ConfigurationKeys.FeatureFlags.g.cs Adds generated config key constant for span-enrichment gate.
tracer/src/Datadog.Trace/Generated/net461/Datadog.Trace.SourceGenerators/InstrumentationDefinitionsGenerator/InstrumentationDefinitions.g.cs Registers new OpenFeature manual instrumentation integrations (generated).
tracer/src/Datadog.Trace/Generated/net461/Datadog.Trace.SourceGenerators/ConfigurationKeysGenerator/ConfigurationKeys.FeatureFlags.g.cs Adds generated config key constant for span-enrichment gate.
tracer/src/Datadog.Trace/FeatureFlags/ULeb128Encoder.cs Implements base64 ULEB128 delta-varint encoding used by ffe_flags_enc / subjects values.
tracer/src/Datadog.Trace/FeatureFlags/SpanEnrichmentState.cs Implements bounded per-trace enrichment accumulation + snapshot-to-tags logic.
tracer/src/Datadog.Trace/FeatureFlags/Rcm/Model/Split.cs Adds optional SerialId to UFC Split model for enrichment metadata.
tracer/src/Datadog.Trace/FeatureFlags/FeatureFlagsEvaluator.cs Surfaces split serial id + do_log into evaluation metadata for downstream enrichment.
tracer/src/Datadog.Trace/FeatureFlags/FeatureFlagMetadataKeys.cs Defines shared metadata keys for split serial id + do_log.
tracer/src/Datadog.Trace/Configuration/TracerSettings.cs Adds IsSpanEnrichmentEnabled setting from env/config.
tracer/src/Datadog.Trace/Configuration/supported-configurations.yaml Documents new env var gate for span enrichment.
tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/ManualInstrumentation/OpenFeature/OpenFeatureSdkIsSpanEnrichmentEnabledIntegration.cs Instruments OpenFeature SDK to read the tracer’s gate setting.
tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/ManualInstrumentation/OpenFeature/OpenFeatureSdkAccumulateSpanEnrichmentIntegration.cs Instruments OpenFeature SDK to accumulate enrichment on the active trace context.
tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/ManualInstrumentation/FeatureFlags/FeatureFlagsSdkEvaluateIntegration.cs Accumulates enrichment for native FeatureFlags SDK evaluations into the trace context.
tracer/src/Datadog.Trace.Trimming/build/Datadog.Trace.Trimming.xml Adds trimming hint for SortedSet<T>.Enumerator.
tracer/src/Datadog.FeatureFlags.OpenFeature/SpanEnrichmentHook.cs Adds OpenFeature FinallyAsync hook to feed evaluation metadata into enrichment.
tracer/src/Datadog.FeatureFlags.OpenFeature/Sdk/FeatureFlagsSdk.cs Adds new SDK surface for enrichment accumulation + gate check (instrumented).
tracer/src/Datadog.FeatureFlags.OpenFeature/DatadogProvider.cs Conditionally registers the new hook only when the gate is enabled.
tracer/src/Datadog.FeatureFlags.OpenFeature/Datadog.FeatureFlags.OpenFeature.csproj Links shared metadata keys into the OpenFeature package build.
tracer/build/supported_calltargets.g.json Updates supported calltargets manifest (generated).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +59 to +62
// The sample wraps flag evaluation in "ffe.root" and a child "ffe.child" (including an eval
// after an await). Wait for both spans.
var spans = await agent.WaitForSpansAsync(2, operationName: "ffe.root", returnAllOperations: true);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Correct — WaitForSpansAsync's loop only counts spans whose name matches operationName, so count: 2 + operationName: "ffe.root" never breaks early (the sample emits one ffe.root + one ffe.child) and burned the full timeout. Fixed by dropping the operationName filter so it waits for 2 total spans.

Comment on lines +116 to +119
Assert.Contains("Exit. OK", output);

var spans = await agent.WaitForSpansAsync(2, operationName: "ffe.root", returnAllOperations: true);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same fix applied here — removed the operationName: "ffe.root" filter so the negative-control test waits for both spans (root + child) instead of two root spans.

Comment thread tracer/src/Datadog.Trace/TraceContext.cs
@tylfin
tylfin requested a review from dudikeleti July 14, 2026 17:39
- Add experimental FFE APM span enrichment for OpenFeature evaluations in `dd-trace-dotnet`.
- Emit the `ffe_*` wire format used by `dd-trace-js#8343` so backend/Trino decode uses the same tags and encodings.
- Keep enrichment disabled by default behind `DD_EXPERIMENTAL_FLAGGING_PROVIDER_SPAN_ENRICHMENT_ENABLED`.

- Adds `DD_EXPERIMENTAL_FLAGGING_PROVIDER_SPAN_ENRICHMENT_ENABLED` in `TracerSettings` and `supported-configurations.yaml`.
- Adds split serial ID support to the UFC `Split` model.
- Adds `ULeb128Encoder` for delta-varint feature-flag serial ID encoding.
- Adds per-root-span `SpanEnrichmentState` and `SpanEnrichmentStore`.
- Adds `SpanEnrichmentHook` for the OpenFeature `Finally` hook path.
- Adds manual-instrumentation integrations for the OpenFeature hook path.
- Writes `ffe_flags_enc`, `ffe_subjects_enc`, and `ffe_runtime_defaults` on `Span.Finish`.
- Adds synchronization and snapshotting around `SpanEnrichmentState` mutation and `ToSpanTags`.
- Adds a never-throw guard around the `Span.Finish` enrichment block.
- Bounds `SpanEnrichmentStore` growth for root spans that never finish.
- Unwraps `OpenFeature.Model.Value` runtime defaults into plain CLR values before JSON serialization.
- Adds xUnit coverage for required span-enrichment cases, max-200 serial IDs, concurrency, never-throw behavior, and store growth bounds.
- Updates generated source-generator outputs.

- The feature is opt-in and off by default.
- No store or accumulator is created when `DD_EXPERIMENTAL_FLAGGING_PROVIDER_SPAN_ENRICHMENT_ENABLED=false`.
- No `ffe_*` tags are emitted when the gate is off.
- State is keyed by local root span and cleaned up on root-span finish and provider close.
- Store growth is bounded for root spans that never finish.
- `ffe_*` tags are bare span `meta` tags, not `_dd.`-prefixed tags.
- Subject keys are SHA256 hashes and are emitted only when `doLog=true`.
- Runtime default values are JSON-serialized as the caller value, not as the OpenFeature wrapper object.

- Frozen `system-tests` parametric suite:

```bash
TEST_LIBRARY=dotnet ./run.sh PARAMETRIC -k span_enrichment
Library: dotnet@3.46.0
===================== 18 passed, 2605 deselected in 46.26s =====================
```

- FFE dogfooding local trace-intake run:
  - Provider path: `DatadogProvider` / OpenFeature client.
  - Root span: `aspnet_core.request`.
  - Service: `ffe-dogfooding-dotnet`.
  - Gate on aggregation: `ffe_flags_enc` decoded to `[829, 1442, 2311, 2312]`.
  - Gate on subjects: `ffe_subjects_enc` contained 2 SHA256 subject keys.
  - Gate off: 3 spans flushed; 0 `ffe_*` tags.
  - Runtime defaults: unicode string and object defaults serialized as raw UTF-8; long unicode value truncated to 64 codepoints.
  - Codec parity: `ZAgUAg==` decoded to `[100, 108, 128, 130]`.

- Staging Datadog APM proof:
  - Runtime: dogfooding staging APM proof against locally built SDK sha `b294dd1` and the staging Agent with `DD_SITE=datad0g.com`.
  - Validation id: `ffe-span-dotnet-20260619121831-b294dd1`.
  - Service: `ffe-dogfooding-dotnet`.
  - Env: `staging`.
  - Scenario: `span_enrichment.aggregate`.
  - Query site: `https://dd.datad0g.com`.
  - Query: `service:ffe-dogfooding-dotnet env:staging @validation_id:ffe-span-dotnet-20260619121831-b294dd1`.
  - Query result: 8 matching spans.
  - Trace Explorer: https://dd.datad0g.com/apm/traces?end=1781904227862&historicalData=true&paused=true&query=service%3Affe-dogfooding-dotnet+env%3Astaging+%40validation_id%3Affe-span-dotnet-20260619121831-b294dd1&start=1781817827862
  - Indexed span: `trace_id=6a35358d0000000091168b7fb4b815a2`, `span_id=15600877720192993068`, `resource=POST /scenario`, `ffe_flags_enc=vQblBKMP`.
  - Indexed span: `trace_id=6a3534fd000000002e93ea8b4f41ec5d`, `span_id=12009548985036661845`, `resource=POST /scenario`, `ffe_flags_enc=vQblBKMP`.

Original commits:
* cd01e22 feat(02-03): add split serial id to UFC model + surface FFE metadata + span-enrichment gate
* dccfd4e feat(02-03): ULeb128 codec + per-root-span enrichment state/store + Finally hook + Span.Finish write
* b30c08d test(02-03): xUnit L0 span-enrichment suite (7 required cases + max-200) + finish-time SetTag fix
* 56a5789 fix(02-03): synchronize SpanEnrichmentState mutation + snapshot ToSpanTags (CR-01)
* 7022962 fix(02-03): wrap FFE enrichment block in Span.Finish in never-throw guard (WR-02)
* c7ad723 fix(02-03): bound SpanEnrichmentStore growth when a root never finishes (WR-03)
* 444cf7a test(02-03): concurrency + never-throw + growth-bound regression tests (CR-01/WR-02/WR-03)
* f763d31 chore: sync generated calltarget + trimming descriptor files for span-enrichment integrations
* d694d56 [FeatureFlags] Make span-enrichment gate-off inert and store lifecycle atomic
* e5fc41e [FeatureFlags] Add OpenFeature span-enrichment integration test
* f97218f [FeatureFlags] Strip internal planning labels from span-enrichment comments
* 1ea7d22 [FeatureFlags] Unwrap OpenFeature Value for object runtime-default span enrichment
* 21c394e Fix span enrichment test member ordering
* b340190 Fix native feature flag span enrichment
* b294dd1 Enable OpenFeature span enrichment for netstandard
* 0795f28 Tighten FFE span enrichment shim
* 7544055 Move FFE span enrichment state to tracer manager
* 7c55c34 Address FFE span enrichment review comments
* 0bb0c30 Merge branch 'master' of github.com:DataDog/dd-trace-dotnet into leo.romanovsky/ffe-apm-span-enrichment
* d43b132 Regenerate generated files after merging master
* bcabc1c [FeatureFlags] Store FFE span enrichment state on TraceContext instead of a central store (#8869)
* 64c10ac Nits fix
* 88c2127 Merge branch 'master' into leo.romanovsky/ffe-apm-span-enrichment
* b9ae537 Merge branch 'master' into leo.romanovsky/ffe-apm-span-enrichment
* 658c197 Address Copilot feedback: Volatile.Read FFE state getter, fix integration-test span wait
* e307716 Merge master
* 2d45803 Merge branch 'leo.romanovsky/ffe-apm-span-enrichment' of github.com:DataDog/dd-trace-dotnet into leo.romanovsky/ffe-apm-span-enrichment

Signed-off-by: Pavel <pavlo.khrebto@datadoghq.com>
@pavlokhrebto
pavlokhrebto force-pushed the leo.romanovsky/ffe-apm-span-enrichment branch from 2d45803 to 401a696 Compare July 15, 2026 12:22

@dudikeleti dudikeleti left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for working on this.
Overall, the changes look good to me from the areas I’m familiar with. I left a few non-blocking comments. PTAL.

Comment thread tracer/src/Datadog.FeatureFlags.OpenFeature/SpanEnrichmentHook.cs Outdated
var manager = tracer.TracerManager;
var res = manager.FeatureFlags?.Evaluate(parameters.FlagKey, parameters.TargetType, parameters.DefaultValue, parameters.TargetingKey ?? string.Empty, parameters.Attributes);
var traceContext = tracer.InternalActiveScope?.Span?.Context.TraceContext;
traceContext?.GetOrCreateFeatureFlagEnrichment()?.AccumulateEvaluation(res, parameters.TargetingKey);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Consider (if its not too large refactor) to check first if we really need to accumulate before creating the state, currently we allocate a bunch of objects when we might drop everything later. checking eligibility first would keep these no-op evaluations allocation-free. (same in OpenFeatureSdkAccumulateSpanEnrichmentIntegration.cs)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The gate already lives in GetOrCreateFeatureFlagEnrichment(), which returns null when enrichment is disabled — so with the feature off these callsites (?.AccumulateEvaluation(...) / ?.Accumulate(...)) are fully no-op and allocate nothing. When it's on, the state is created lazily once per trace (not per evaluation) and reused, so a non-recording eval doesn't allocate a new state; the only cost is the one-time state on the first eligible eval. Given that, an extra eligibility pre-check would add branching for little gain — happy to revisit if profiling shows otherwise.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The gate already lives in GetOrCreateFeatureFlagEnrichment(), which returns null when enrichment is disabled

The disabled case is indeed allocation-free. My concern is when enrichment is enabled but none of the feature-flag evaluations in a trace are eligible for recording. The first evaluation still creates SpanEnrichmentState and its collections, which remain empty and unused for the entire trace. This may be an edge case, so it's up to you whether it's worth changing, but determining eligibility before creating the state would avoid those allocations.

Comment thread tracer/src/Datadog.Trace/FeatureFlags/FeatureFlagsEvaluator.cs Outdated
Comment thread tracer/src/Datadog.Trace/FeatureFlags/ULeb128Encoder.cs Outdated
Comment thread tracer/src/Datadog.Trace/FeatureFlags/ULeb128Encoder.cs Outdated
Sort+dedupe serial ids via Array.Sort instead of a SortedSet (drop the
unused overload) and write varint bytes directly into the payload buffer;
skip the runtime-default value conversion in the OpenFeature hook unless
the eval records a default; and only emit do_log/split-serial-id metadata
(and its ToString) when span enrichment is enabled.
…ataDog/dd-trace-dotnet into leo.romanovsky/ffe-apm-span-enrichment

@andrewlock andrewlock left a comment

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.

Thanks for the tracecontext refactor, It's looking much better! Most of my suggestions are about ways to improve perf in general - one of the concerns is that basically all of the production code runs on the hot path currently, so we need to be careful, especially with allocations.

Obviously a good way to mitigate that would be to move the serialization to the background thread, but I'm fine with deferring that to a follow up PR obviously!

catch (Exception ex)
{
// Enrichment must never break flag evaluation.
System.Diagnostics.Debug.WriteLine($"[Datadog] SpanEnrichmentHook.FinallyAsync failed: {ex}");

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.

The https://www.nuget.org/packages/OpenFeature package depends on the .NET logging packages, which suggests that the "correct" place to log this is using the .NET ILogger abstraction. That said, given we don't use it anywhere here currently, and plumbing it through is prob a bit of a pain, my preference is to just remove the System.Diagnostics.Debug.WriteLine anyway. It literally isn't in the compiled code we ship, so it mostly ends up just being noise

Comment thread tracer/src/Datadog.FeatureFlags.OpenFeature/SpanEnrichmentHook.cs Outdated
Comment thread tracer/src/Datadog.FeatureFlags.OpenFeature/SpanEnrichmentHook.cs Outdated
Comment thread tracer/src/Datadog.FeatureFlags.OpenFeature/SpanEnrichmentHook.cs Outdated
Comment thread tracer/src/Datadog.Trace/Util/Sha256Helper.cs
Comment thread tracer/src/Datadog.Trace/FeatureFlags/ULeb128Encoder.cs
Comment thread tracer/test/Datadog.Trace.Tests/FeatureFlags/SpanEnrichmentTests.cs Outdated
Comment thread tracer/test/Datadog.Trace.Tests/FeatureFlags/SpanEnrichmentTests.cs Outdated
Comment thread tracer/test/Datadog.Trace.Tests/FeatureFlags/SpanEnrichmentTests.cs Outdated
Early-return when feature flags are disabled; drop IDisposable and the
Debug.WriteLine from the OpenFeature hook and pattern-match Value access;
return a concrete List and inline the default truncation; use fluent
assertions and move codec/Sha256 tests into dedicated test classes.
@pavlokhrebto

Copy link
Copy Markdown
Contributor

Thanks @andrewlock! Agreed across the board. I pushed the quick wins and will take the bigger allocation/serialization work as the follow-up PR you offered.

Addressed in this PR:

  • Evaluate integration early-returns when feature flags are disabled (skips the async-local scope lookup for a no-op).
  • OpenFeature hook: dropped IDisposable, removed the Debug.WriteLine, pattern-match AsStructure/AsList (no !), presized the list.
  • SpanEnrichmentState.ToSpanTags returns a concrete List<>; inlined the default truncation.
  • Skip creating per-trace state for evaluations that record nothing (new IsRecordable predicate, gated at both callsites).
  • Tests: fluent assertions instead of Assert.Contains; moved the codec tests into a dedicated ULeb128EncoderTests and added Sha256HelperTests; removed the SetTracer + redundant Finish from the accumulate test.

Follow-up PR (the hot-path/allocation work, since it's all tied to the serializer move):

  • Move ffe_* encoding/tagging off Span.Finish() to the serialization thread — off the hot path, drops the Tags.SetTag allocations, and lets me delete the ToSpanTags test hook.
  • Encoder allocations: ReadOnlySpan<long>/stackalloc for small counts, array-pool otherwise, reuse VarEncodingHelper, split modern/old .NET impls.
  • Delay-hash subjects (store the raw targeting key, hash at serialization) — removes the per-AddSubject hash and the HashTargetingKey helper.
  • readonly struct return from ToSpanTags, and snapshot-testing the integration output.

Re TracerSettings (forcing IsSpanEnrichmentEnabled false when the provider is off): agree it's not necessary — with the provider disabled there are no evaluations to enrich, so the gate is moot either way. Left it independent for clarity.

## Summary of changes

Stacked on top of #8795 (FFE APM span enrichment). Moves the `ffe_*` tag
encoding/serialization off the customer's `Span.Finish()` path and onto
the
serializer thread, plus two follow-ups Andrew agreed to defer from that
PR:
`ULeb128Encoder` allocation reductions and a wire-format snapshot test.

## Reason for change

In #8795, encoding the feature-flag serial ids (ULEB128 + base64),
serializing
the subjects/runtime-defaults JSON, and hashing the targeting keys all
ran
synchronously inside `Span.Finish()` — i.e. on the customer's thread.
This work
is only needed at serialization time, so it belongs on the background
serializer
thread where it stays off the hot path.

## Implementation details

- **Serializer-thread write path.** Removed the `ffe_*` write block from
`Span.Finish()`. `SpanMessagePackFormatter.WriteTags` now writes the
tags for
the local-root span only, reading
`Context.TraceContext.FeatureFlagEnrichment`.
Because the formatter back-patches the meta-map count, conditionally
injecting
  tags is safe.
- **`SpanEnrichmentState.BuildSpanTags()`** replaces `ToSpanTags()`,
returning a
new `readonly struct FeatureFlagSpanTags` (no `List`/`Dictionary` for
the tag
set). It snapshots the bounded state under the lock, then
encodes/serializes
after releasing it, and never throws — on failure it returns `default`
so
  enrichment can never break span serialization.
- **Delay-hashed subjects.** Targeting keys are now stored raw and
SHA256-hashed
in `BuildSpanTags()` (serializer thread) instead of at accumulation
time, so
  the hash no longer runs on the evaluation path.
- **`ULeb128Encoder` allocations.** Small id sets encode entirely on the
stack
on modern runtimes (`stackalloc` + `Span.Sort` + `ReadOnlySpan` base64);
larger
sets and `net461`/`netstandard2.0` use `ArrayPool` for both buffers.
Output is
  byte-identical (golden vector `ZAgUAg==` unchanged).
- CI-visibility formatter intentionally untouched — FFE enrichment is
APM-only.

## Test coverage

- Guardrail tests rewritten to assert `ffe_*` tags via the real
serialize → deserialize (`MockSpan`) path rather than off
`Span.Finish()`:
gate-on/gate-off controls, no-data, per-trace isolation, root-only
placement,
a "enrichment throws → serialization still succeeds" test, and a
concurrent
  Add-racing-`BuildSpanTags` regression.
- New `Serialize_FfeWireFormat_MatchesFrozenSnapshot`: byte-exact
snapshot of the
three `ffe_*` values through the formatter, locking the frozen cross-SDK
  contract (tag names + codec output + compact JSON).
- `ULeb128EncoderTests` unchanged and still green (output contract
preserved).

## Other details

Stacked PR — base is `leo.romanovsky/ffe-apm-span-enrichment`;
review/merge #8795 first.
@pavlokhrebto
pavlokhrebto merged commit 2302d41 into master Jul 29, 2026
141 checks passed
@pavlokhrebto
pavlokhrebto deleted the leo.romanovsky/ffe-apm-span-enrichment branch July 29, 2026 10:39
@github-actions github-actions Bot added this to the vNext-v3 milestone Jul 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:tracer The core tracer library (Datadog.Trace, does not include OpenTracing, native code, or integrations) feature_flags type:new-feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants