Skip to content

feat(otel): Emit OpenTelemetry HTTP semantic conventions on HTTP server spans (ASP.NET) - #9027

Open
zacharycmontoya wants to merge 20 commits into
masterfrom
otel-aspnet-rebased
Open

zacharycmontoya wants to merge 20 commits into
masterfrom
otel-aspnet-rebased

Conversation

@zacharycmontoya

@zacharycmontoya zacharycmontoya commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Summary of changes

Updates the server spans produced by the ASP.NET, ASP.NET MVC 5, and ASP.NET Web API 2 (both IIS-hosted and OWIN self-hosted) integrations so that their span name and request attributes match the OpenTelemetry HTTP server span specification when DD_TRACE_OTEL_SEMANTICS_ENABLED=true. This is the ASP.NET follow-up to the ASP.NET Core work in #8995, and reuses the WebTags / HttpSemanticConventions foundation introduced there. Also extends the OTLP test harness from #8995 to .NET Framework so the new attribute set can be snapshot-tested end-to-end against IIS Express and OWIN-hosted samples.

Reason for change

When DD_TRACE_OTEL_SEMANTICS_ENABLED=true, the SDK should produce spans that align with the OpenTelemetry semantic conventions across every span kind. Today the ASP.NET server integrations emit spans with the Datadog attribute set (http.method, http.url, http.request.headers.host) and a Datadog resource name built from various algorithms. After these changes, a user who has opted into OpenTelemetry semantics will get a server span whose name and attributes match what the OpenTelemetry ASP.NET instrumentation itself would produce, so they can switch freely between the two without needing to update dashboards, monitors, or downstream tooling built for OpenTelemetry HTTP server spans.

Implementation details

In general, when OpenTelemetry semantics is enabled, the ASP.NET integrations calculate the span resource name and the set of span attributes as prescribed by the OpenTelemetry specification. This is done by creating a branch in each integration to call either HttpSemanticConventions.SetHttpServerRequestValues when using OpenTelemetry semantics or SpanExtensions.DecorateWebServerSpan when using Datadog semantics, but not both.

Also, ASP.NET required some additional logic due to the following factors:

  1. A single request may be handled by more than one instrumented framework. An IIS-hosted Web API request produces an aspnet.request span from the TracingHttpModule and an aspnet-webapi.request span from the Web API integration nested inside it. The conventions describe exactly one HTTP server span per request, so this PR unifies the spans into one.
  2. The route isn't known when the server span is created. The TracingHttpModule starts its span at BeginRequest, long before MVC or Web API has matched a route, but the span name must be {method} {http.route} when route information is available, so this PR updates the span when instrumentation has access to more detailed route information.

TracingHttpModule (ASP.NET)

Aside from generating a span that aligns with OpenTelemetry semantic conventions, the only update to this instrumentation is specifically handling the Server.TransferRequest() scenario that re-runs the pipeline with a fresh HttpContext but the same ExecutionContext. In this scenario, BeginRequest will avoid creating a new span, report the active span to Security, and mark the scope to not be disposed when EndRequest runs.

AspNetWebApi2Integration (Web API) and AspNetMvcIntegration (MVC)

This either generates a new span that aligns with OpenTelemetry semantic conventions when no active server span is present or it updates the active server span with an updated http.route span attribute and an updated resource name.

When there is an active server span, no new child span is generated to report to AppSec, so the active scope is found using SharedItems.TryPeekScopeOrServerScope(context, key) and reported against.

Note: The tags aspnet.controller, aspnet.action, aspnet.route were only carried on MVC / Web API child spans, so these keys may no longer be reported under OpenTelemetry semantics. Instead, their information is carried in the http.route span attribute.

HttpSemanticConventions

  • SetHttpServerRequestValues — a new Uri-based overload is introduced since System.Web exposes the request as an absolute URI rather than as separate scheme/host/path/query components.
  • SetServerAddressAndPort — The server.address / server.port span attributes are derived from the Host header as the spec requires, falling back to the request URI when there is no Host header.
  • WebTags.NetworkProtocolVersion/network.protocol.version is now populated on ASP.NET server spans, which uses the new GetNetworkProtocolVersion method to parse the protocol string.

Test coverage

Unit tests

  • HttpSemanticConventionsTests — provides coverage for the new HttpSemanticConventions methods

Integration tests — OTLP harness extended to .NET Framework

  • OtlpServerTestBase — provides a shared harness for either IIS-hosted or self-hosted test applications. This mirrors OtlpAspNetCoreTestBase.
  • OtlpAspNetTestBase — provides the IIS Express (IisFixture) harness for running ASP.NET applications
  • OwinFixture — moved out of OwinWebApi2Tests so the OTLP suite can share the self-hosted OWIN server.
  • OtlpAspNetMvc5Tests/OtlpAspNetWebApi2Tests/OtlpOwinWebApi2Tests — provides OTLP snapshot testing that exercises for their corresponding test applications

For the new end-to-end integration tests, each suite runs only under two scenarios:

  • Datadog semantics
  • OpenTelemetry semantics

Understanding the snapshots

  • "GET /delay/0" vs "GET /delay/0?id=1" vs "GET /delay/0?token=SUPER-SECRET-TOKEN-VALUE" — identifies the baseline attribute set, and how the query string is reported and obfuscated
  • "GET /not-a-registered-route/1" vs "FOO /not-a-registered-route/1" — how the span name falls back to {method} (and to HTTP) with no route, and how http.request.method_original is reported
  • "GET /statuscode/302" vs "GET /statuscode/400" vs "GET /statuscode/500" vs "GET /badrequest" — status-to-error mapping for server spans (4xx is not an error, unlike on client spans) and exceptions recorded as span events
  • "GET /badrequest?TransferRequest=true" vs "GET /BadRequestWithStatusCode/401?TransferRequest=true", "GET /api/TransferRequest/500" vs "GET /api/TransferRequest/401" — shows that the status code is accurate in the reported span
  • Any Datadog vs OpenTelemetry pair — shows the two nested spans collapsing into one

Other details

📝 This PR is Stacked on top of #8995, which introduces several additional changes like updates to the WebTags implementation and extension methods to standardize the access to the HttpMethod/HttpClientIp/NetworkClientIp tags.

⚠️ I recommend viewing the commits one-by-one, as they were curated to make reviewing easier.

JIRA: APMAPI-2053

Follow-up work

  1. The ASP.NET snapshot tests do not run in CI currently because they emit OTLP traces, which requires the dd-apm-test-agent container to be run (and we can't run docker-in-docker in the Azure DevOps managed pool). We will need to update our in-process test agent to supports OTLP traces in order to run these in CI.
  2. An additional tag network.protocol.name is still unset on server spans
  3. Follow up to ensure that code origin is reported on ASP.NET spans in OTel semantics mode (it currently only reports when using Datadog semantics)

@zacharycmontoya zacharycmontoya changed the title Otel aspnet rebased feat(otel): Emit OpenTelemetry HTTP semantic conventions on HTTP server spans (ASP.NET) Aug 11, 2026
@dd-trace-dotnet-ci-bot

dd-trace-dotnet-ci-bot Bot commented Aug 12, 2026

Copy link
Copy Markdown

Execution-Time Benchmarks Report ⏱️

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

✅ No regressions detected

📄 View the full report (charts + all metrics) →

@pr-commenter

pr-commenter Bot commented Aug 12, 2026

Copy link
Copy Markdown

Benchmarks

Benchmark execution time: 2026-09-09 23:47:55

Comparing candidate commit ff36979 in PR branch otel-aspnet-rebased with baseline commit b8ebe04 in branch master.

📊 Benchmarking dashboard

Found 2 performance improvements and 1 performance regressions! Performance is the same for 69 metrics, 0 unstable metrics, 65 known flaky benchmarks, 61 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.OpenTelemetry.InstrumentedApi.Trace.TracerBenchmark.StartActiveSpan net472

  • 🟩 throughput [+10744.846op/s; +12381.222op/s] or [+5.789%; +6.671%]

scenario:Benchmarks.Trace.HttpClientBenchmark.SendAsync net472

  • 🟥 throughput [-5196.881op/s; -4831.352op/s] or [-5.933%; -5.515%]

scenario:Benchmarks.Trace.HttpClientBenchmark.SendAsync net6.0

  • 🟩 throughput [+7504.597op/s; +8591.588op/s] or [+5.098%; +5.836%]

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 [-6872.362op/s; -6388.087op/s] or [-8.149%; -7.574%]

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

  • 🟥 throughput [-7994.140op/s; -6835.649op/s] or [-8.128%; -6.950%]

scenario:Benchmarks.Trace.AgentWriterBenchmark.WriteAndFlushEnrichedTraces net472

  • 🟥 allocated_mem [+1.637KB; +1.637KB] or [+49.731%; +49.747%]
  • 🟥 execution_time [+310.374ms; +312.709ms] or [+154.019%; +155.177%]
  • 🟥 throughput [-58.796op/s; -54.829op/s] or [-10.579%; -9.865%]

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

  • 🟥 allocated_mem [+1.013KB; +1.013KB] or [+37.563%; +37.575%]
  • 🟥 execution_time [+378.980ms; +383.993ms] or [+299.418%; +303.378%]
  • 🟩 throughput [+77.245op/s; +81.270op/s] or [+10.185%; +10.715%]

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

  • 🟥 allocated_mem [+1.090KB; +1.090KB] or [+40.417%; +40.429%]
  • 🟥 execution_time [+400.208ms; +401.173ms] or [+354.169%; +355.023%]

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

  • 🟥 allocated_mem [+4.725KB; +4.726KB] or [+99.482%; +99.497%]
  • 🟥 throughput [-60379.494op/s; -60025.193op/s] or [-46.978%; -46.703%]

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

  • 🟥 allocated_mem [+3.848KB; +3.848KB] or [+81.379%; +81.391%]
  • 🟩 execution_time [-16.188ms; -12.016ms] or [-7.561%; -5.612%]
  • 🟥 throughput [-62313.650op/s; -59523.127op/s] or [-45.485%; -43.449%]

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

  • 🟥 allocated_mem [+4.576KB; +4.576KB] or [+98.954%; +98.966%]
  • 🟥 throughput [-48830.544op/s; -46575.755op/s] or [-44.148%; -42.110%]

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

  • 🟥 allocated_mem [+1.348KB; +1.348KB] or [+109.063%; +109.078%]
  • 🟥 throughput [-306557.834op/s; -302952.376op/s] or [-31.301%; -30.933%]

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

  • 🟥 allocated_mem [+511 bytes; +512 bytes] or [+41.822%; +41.834%]
  • 🟩 execution_time [-26.690ms; -21.802ms] or [-11.902%; -9.723%]
  • 🟥 throughput [-91309.483op/s; -67564.637op/s] or [-9.755%; -7.218%]

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

  • 🟥 allocated_mem [+1.312KB; +1.312KB] or [+108.600%; +108.616%]
  • 🟥 throughput [-184758.037op/s; -165736.894op/s] or [-26.546%; -23.813%]

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

  • 🟥 allocated_mem [+3.378KB; +3.378KB] or [+89.003%; +89.017%]
  • 🟥 throughput [-72795.922op/s; -72020.207op/s] or [-48.991%; -48.469%]

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

  • 🟥 allocated_mem [+3.336KB; +3.336KB] or [+88.150%; +88.161%]
  • 🟥 throughput [-75536.493op/s; -72638.797op/s] or [-48.063%; -46.219%]

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

  • 🟥 allocated_mem [+3.264KB; +3.264KB] or [+88.493%; +88.506%]
  • 🟥 throughput [-56553.603op/s; -53931.760op/s] or [-45.052%; -42.964%]

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

  • 🟩 throughput [+248944.879op/s; +306381.060op/s] or [+8.301%; +10.216%]

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

  • 🟩 execution_time [-19.029ms; -14.696ms] or [-8.772%; -6.775%]

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

  • 🟩 allocated_mem [-13.759KB; -13.756KB] or [-42.324%; -42.316%]
  • 🟥 execution_time [+301.080ms; +301.769ms] or [+150.439%; +150.784%]
  • 🟩 throughput [+996.354op/s; +1018.769op/s] or [+11.005%; +11.252%]

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

  • 🟩 allocated_mem [-13.722KB; -13.718KB] or [-42.341%; -42.329%]
  • 🟥 execution_time [+300.387ms; +303.566ms] or [+151.486%; +153.089%]
  • 🟩 throughput [+2155.290op/s; +2425.374op/s] or [+16.485%; +18.550%]

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

  • 🟩 allocated_mem [-13.722KB; -13.718KB] or [-42.341%; -42.329%]
  • 🟥 execution_time [+300.203ms; +302.591ms] or [+151.219%; +152.422%]
  • 🟩 throughput [+1894.345op/s; +2021.050op/s] or [+18.289%; +19.512%]

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

  • 🟥 execution_time [+296.991ms; +298.271ms] or [+145.870%; +146.499%]
  • 🟩 throughput [+564.818op/s; +573.631op/s] or [+14.974%; +15.207%]

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

  • 🟥 execution_time [+293.809ms; +296.748ms] or [+143.632%; +145.069%]
  • 🟩 throughput [+2743.643op/s; +2825.317op/s] or [+39.860%; +41.047%]

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

  • 🟥 execution_time [+300.576ms; +300.994ms] or [+150.227%; +150.436%]
  • 🟩 throughput [+1431.833op/s; +1448.992op/s] or [+28.421%; +28.761%]

scenario:Benchmarks.Trace.Asm.AppSecWafBenchmark.RunWafRealisticBenchmark net472

  • 🟩 execution_time [-143.352µs; -138.276µs] or [-29.432%; -28.390%]
  • 🟩 throughput [+818.173op/s; +854.323op/s] or [+39.849%; +41.609%]

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

  • 🟩 execution_time [-129.135µs; -102.332µs] or [-29.617%; -23.470%]
  • 🟩 throughput [+761.132op/s; +885.905op/s] or [+33.091%; +38.516%]

scenario:Benchmarks.Trace.Asm.AppSecWafBenchmark.RunWafRealisticBenchmark netcoreapp3.1

  • 🟩 execution_time [-139.450µs; -117.258µs] or [-29.878%; -25.123%]
  • 🟩 throughput [+746.876op/s; +833.228op/s] or [+34.477%; +38.463%]

scenario:Benchmarks.Trace.Asm.AppSecWafBenchmark.RunWafRealisticBenchmarkWithAttack net472

  • 🟩 execution_time [-127.142µs; -122.586µs] or [-34.327%; -33.097%]
  • 🟩 throughput [+1347.095op/s; +1402.855op/s] or [+49.890%; +51.955%]

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

  • 🟩 execution_time [-98.798µs; -75.044µs] or [-31.541%; -23.958%]
  • 🟩 throughput [+1109.736op/s; +1317.829op/s] or [+34.594%; +41.081%]

scenario:Benchmarks.Trace.Asm.AppSecWafBenchmark.RunWafRealisticBenchmarkWithAttack netcoreapp3.1

  • 🟩 execution_time [-137.224µs; -114.832µs] or [-37.539%; -31.414%]
  • 🟩 throughput [+1320.754op/s; +1457.747op/s] or [+47.397%; +52.313%]

scenario:Benchmarks.Trace.AspNetCoreBenchmark.SendRequest net472

  • 🟥 execution_time [+299.899ms; +300.687ms] or [+149.681%; +150.074%]

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

  • 🟥 execution_time [+417.228ms; +423.913ms] or [+453.336%; +460.599%]
  • 🟩 throughput [+731.918op/s; +890.350op/s] or [+6.014%; +7.316%]

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

  • unstable execution_time [+299.180ms; +352.710ms] or [+227.165%; +267.809%]

scenario:Benchmarks.Trace.CIVisibilityProtocolWriterBenchmark.WriteAndFlushEnrichedTraces net472

  • unstable execution_time [+358.218ms; +408.070ms] or [+164.705%; +187.627%]
  • 🟥 throughput [-529.013op/s; -482.966op/s] or [-47.934%; -43.761%]

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

  • unstable execution_time [+205.344ms; +338.620ms] or [+87.509%; +144.306%]
  • 🟥 throughput [-675.714op/s; -591.958op/s] or [-45.070%; -39.484%]

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

  • 🟥 execution_time [+353.148ms; +363.519ms] or [+211.224%; +217.427%]
  • 🟥 throughput [-407.090op/s; -368.988op/s] or [-28.345%; -25.692%]

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

  • 🟩 execution_time [-163.547µs; -131.798µs] or [-8.285%; -6.676%]
  • 🟩 throughput [+37.268op/s; +45.557op/s] or [+7.357%; +8.993%]

scenario:Benchmarks.Trace.ElasticsearchBenchmark.CallElasticsearch net472

  • 🟥 execution_time [+301.335ms; +303.706ms] or [+151.747%; +152.941%]

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

  • 🟥 execution_time [+300.696ms; +301.837ms] or [+150.679%; +151.251%]

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

  • 🟥 execution_time [+301.076ms; +304.366ms] or [+151.248%; +152.900%]

scenario:Benchmarks.Trace.ElasticsearchBenchmark.CallElasticsearchAsync net472

  • 🟥 execution_time [+300.738ms; +302.265ms] or [+151.021%; +151.787%]

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

  • 🟥 execution_time [+301.140ms; +303.919ms] or [+148.900%; +150.275%]

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

  • 🟥 execution_time [+303.258ms; +307.070ms] or [+153.705%; +155.636%]

scenario:Benchmarks.Trace.GraphQLBenchmark.ExecuteAsync net472

  • 🟥 execution_time [+301.646ms; +304.291ms] or [+151.399%; +152.726%]

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

  • 🟥 execution_time [+302.083ms; +312.941ms] or [+150.561%; +155.972%]
  • 🟩 throughput [+31747.050op/s; +46924.717op/s] or [+6.304%; +9.318%]

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

  • 🟥 execution_time [+303.027ms; +307.929ms] or [+150.753%; +153.192%]

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

  • 🟩 execution_time [-16.427ms; -12.748ms] or [-7.638%; -5.928%]

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

  • unstable execution_time [+9.171µs; +53.951µs] or [+2.265%; +13.326%]

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

  • 🟩 allocated_mem [-20.519KB; -20.497KB] or [-7.485%; -7.477%]
  • unstable execution_time [+31.133µs; +129.722µs] or [+6.153%; +25.639%]
  • unstable throughput [-333.839op/s; -66.036op/s] or [-16.659%; -3.295%]

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

  • 🟩 allocated_mem [-17.175KB; -17.155KB] or [-6.261%; -6.254%]
  • unstable execution_time [-72.584µs; -11.325µs] or [-12.578%; -1.963%]

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

  • 🟥 execution_time [+8.034µs; +11.609µs] or [+18.989%; +27.440%]
  • 🟥 throughput [-5382.014op/s; -3696.704op/s] or [-22.657%; -15.562%]

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

  • unstable execution_time [-13.859µs; -6.147µs] or [-21.501%; -9.538%]
  • unstable throughput [+1490.292op/s; +3192.923op/s] or [+9.143%; +19.590%]

scenario:Benchmarks.Trace.Log4netBenchmark.EnrichedLog net472

  • 🟥 execution_time [+302.617ms; +304.551ms] or [+152.959%; +153.937%]

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

  • 🟥 execution_time [+302.337ms; +305.083ms] or [+153.889%; +155.286%]

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

  • 🟥 execution_time [+300.187ms; +302.617ms] or [+150.281%; +151.497%]

scenario:Benchmarks.Trace.SerilogBenchmark.EnrichedLog net472

  • 🟥 execution_time [+297.147ms; +299.489ms] or [+148.101%; +149.268%]

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

  • 🟥 execution_time [+300.227ms; +302.002ms] or [+150.760%; +151.651%]

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

  • 🟥 execution_time [+304.001ms; +306.848ms] or [+154.170%; +155.614%]

scenario:Benchmarks.Trace.SingleSpanAspNetCoreBenchmark.SingleSpanAspNetCore net472

  • 🟥 execution_time [+299.158ms; +300.198ms] or [+149.222%; +149.740%]
  • 🟩 throughput [+61018135.847op/s; +61374966.027op/s] or [+44.437%; +44.697%]

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

  • unstable execution_time [+318.880ms; +386.261ms] or [+396.583%; +480.385%]

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

  • 🟥 execution_time [+298.659ms; +299.866ms] or [+148.964%; +149.566%]
  • 🟩 throughput [+17992471.268op/s; +18973353.395op/s] or [+7.969%; +8.404%]

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

  • 🟩 throughput [+78755.204op/s; +91069.889op/s] or [+7.353%; +8.503%]

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

  • 🟩 throughput [+50237.427op/s; +70032.181op/s] or [+5.815%; +8.106%]

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

  • 🟩 throughput [+75295.499op/s; +82328.513op/s] or [+7.478%; +8.177%]

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

  • 🟩 throughput [+41210.153op/s; +47757.152op/s] or [+7.483%; +8.672%]

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

  • 🟩 throughput [+27590.749op/s; +37141.253op/s] or [+6.176%; +8.313%]

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

  • 🟩 throughput [+58473.879op/s; +76930.369op/s] or [+6.533%; +8.595%]

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.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.OptimizedCharSliceWithPool netcoreapp3.1
  • scenario:Benchmarks.Trace.CharSliceBenchmark.OriginalCharSlice net472
  • 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.StartFinishSpan net472
  • scenario:Benchmarks.Trace.SpanBenchmark.StartFinishSpan net6.0
  • scenario:Benchmarks.Trace.SpanBenchmark.StartFinishTwoScopes net472
  • scenario:Benchmarks.Trace.TraceAnnotationsBenchmark.RunOnMethodBegin net472
  • scenario:Benchmarks.Trace.TraceAnnotationsBenchmark.RunOnMethodBegin netcoreapp3.1

@github-actions

Copy link
Copy Markdown
Contributor

Snapshots difference summary

The following differences have been observed in committed snapshots. It is meant to help the reviewer.
The diff is simplistic, so please check some files anyway while we improve it.

1 occurrences of :

-      http.request.method: GET,
[...]
-      server.address: 127.0.0.1,
[...]
-      url.path: /proxy,
-      url.scheme: http,
[...]
-      server.port: 00000,

1 occurrences of :

-      http.request.method: GET,
[...]
-      server.address: 127.0.0.1,
[...]
-      url.path: /,
-      url.scheme: http,

1 occurrences of :

-      server.port: 00000,

@zacharycmontoya
zacharycmontoya force-pushed the otel-aspnet-rebased branch 13 times, most recently from 725c379 to d53e484 Compare August 19, 2026 23:48
@zacharycmontoya zacharycmontoya added the area:opentelemetry OpenTelemetry support label Aug 20, 2026
@zacharycmontoya
zacharycmontoya requested a review from a team as a code owner September 1, 2026 18:09
Base automatically changed from otel-aspnetcore to master September 1, 2026 19:47
…method in the resource name of OWIN spans (removes HttpSemanticConventions.GetServerResourceNameFromRawMethod in favor of HttpSemanticConventions.GetServerResourceName)
…ectly calculate the resource name with the "{method} {http.route}" pattern
… before the inferred proxy span logic runs so we don't have a new inferred proxy scope.

@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.

LGTM in general, though I confess I found it somewhat tricky to follow all the different pieces. Only vaguely looked at the snapshots so taking you're word they look good to you

/// <summary>
/// Gets a value indicating whether this request started the scope, and is therefore the
/// one that names and finishes it. <c>false</c> for a request produced by
/// <see cref="HttpServerUtility.TransferRequest(string)"/> under OpenTelemetry semantics,

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.

It's quite tricky to follow the logic here, so just to clarify, if the request was not created using otel semantics, what do we see? One span for the initial request, and one for the Transferred request? 🤔

Comment on lines +63 to +69
if (AspNetWebApi2Integration.UsesExistingServerSpan(tracer))
{
// With OpenTelemetry semantics a request has a single HTTP server span, so enrich the
// ASP.NET one instead of nesting an aspnet-webapi.request span inside it. The controller
// context is carried through so the route can be refreshed once the action has run.
AspNetWebApi2Integration.UpdateExistingServerSpan(tracer, boxedControllerContext);
return new CallTargetState(scope: null, state: boxedControllerContext);

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 still an issue?

Comment on lines +216 to +219
if (!StringUtil.IsNullOrEmpty(route) && StringUtil.IsNullOrEmpty(span.Tags.GetTag(Trace.Tags.HttpRoute)))
{
span.Tags.SetTag(Trace.Tags.HttpRoute, route);
}

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.

Should we have special cases for this, e.g. check if it's an AspNetTags etc? 🤔

All of which makes me wonder - given our custom tag bags already use the property when this method is called, is this actually more or less expensive than doing the if type check first? 😅 I don't know if we've benchmarked it, and it's possible we've been making incorrect assumptions there :oops:

/// the ddapm test-agent session the application under test exports OTLP to, where the fixture
/// should write its diagnostics, and a once-per-test-class initialization point.
/// </summary>
public interface IAspNetFixture

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.

This only applies to Otlp tests right, maybe we should rename this to IOtlpAspNetFixture to be clear?

EDIT: hmm, no, it looks like this is on the generalised IisFixture 🤔 In that case, should OtlpSession be nullable? Because it doesn't make any sense to have an OtlpTestAgentSession if we're doing "normal" aspnet tests right?

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.

Good point, I'll make OtlpTestAgentSession nullable

Comment on lines +48 to +49
/// minutes -- and which tolerates up to 16ms of skew when it does anchor. The two clocks
/// therefore disagree by a few milliseconds, so without a margin a span created just after the

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.

Doesn't the first statement say they could differ by up to 16ms? Do we not need that same tolerance to avoid flake here?

Comment on lines +115 to +120
if (iastInstance.Settings.Enabled && iastInstance.OverheadController.AcquireRequest())
{
var traceContext = scope.Span?.Context?.TraceContext;
traceContext?.EnableIastInRequest();
traceContext?.IastRequestContext?.AddRequestData(httpRequest);
}

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.

Transferred requests reuse the original server scope, but line 219 calls ReportToSecurityAndIast again. That executes this AcquireRequest() a second time for the same TraceContext, while TraceContext.CloseWebSpan releases the overhead-controller slot only once when the trace's single IastRequestContext is closed. Every transferred request can therefore leak one available IAST request slot and eventually disable IAST processing. Could we skip acquisition when this trace already has an IastRequestContext (while still adding the transferred request data), or otherwise balance every successful acquisition?

Comment on lines +63 to +69
if (AspNetWebApi2Integration.UsesExistingServerSpan(tracer))
{
// With OpenTelemetry semantics a request has a single HTTP server span, so enrich the
// ASP.NET one instead of nesting an aspnet-webapi.request span inside it. The controller
// context is carried through so the route can be refreshed once the action has run.
AspNetWebApi2Integration.UpdateExistingServerSpan(tracer, boxedControllerContext);
return new CallTargetState(scope: null, state: boxedControllerContext);

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.

Seems like yes, this is still present at the current head. The checked-in OTLP snapshots for TransferRequest/401, TransferRequest/500, and TransferRequest/503 each contain two exception.type events on the one reused server span. The transfer/proxy fixes do not change either exception-recording path. I think the coalesced-span path still needs to ensure that only one handler records the exception.

@anna-git anna-git 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.

lgtm for asm parts, left a few comments

if (responseObject is not null)
{
var scope = SharedItems.TryPeekScope(HttpContext.Current, AspNetMvcIntegration.HttpContextKey);
var scope = SharedItems.TryPeekScopeOrServerScope(HttpContext.Current, AspNetMvcIntegration.HttpContextKey);

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.

not sure but do we need to call TryPeekScopeOrServerScope too in method begin?
Related I'm getting a claude comment:

Code Origin for Spans lookups not updated for the new shared-span model — three call sites (AsyncControllerActionInvoker_BeginInvokeActionMethod_Integration.cs:69 (tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/AspNet/AsyncControllerActionInvoker_BeginInvokeActionMethod_Integration.cs#L69), ControllerActionInvoker_InvokeAction_Integration.cs:83 (tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/AspNet/ControllerActionInvoker_InvokeAction_Integration.cs#L83), ReflectedHttpActionDescriptor_ExecuteAsync_Integration.cs:83 (tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/AspNet/ReflectedHttpActionDescriptor_ExecuteAsync_Integration.cs#L83)) still call TryPeekScope instead of TryPeekScopeOrServerScope, while sibling AppSec lookups a few lines later in the same files were correctly converted. Under OTel semantics, Code Origin for Spans silently stops working for MVC/WebApi actions.

Comment thread tracer/src/Datadog.Trace/AspNet/SharedItems.cs
{
// No span of our own: the route information belongs on the ASP.NET server span, and
// this is the first point at which the executed route is guaranteed to be resolved.
AspNetWebApi2Integration.UpdateExistingServerSpan(Tracer.Instance, existingSpanContext);

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.

maybe we should set the exception on the existing server span as scope can now be null under otel semantics?

- Make the WebTags argument non-nullable in HttpSemanticConventions.SetHttpServerRequestValues
- Remove changes to the "newResourceNamesEnabled" calculation in the AspNet integrations (they're already handled in TracerSettings)
- Rename new AspNetWebApi2Integration helper methods
- Update AspNetWebApi2Integration.GetCurrentRequestProtocol so we let duck cast exceptions bubble up (to potentially disable the integration)
- Refactor and document AspNetWebApi2Integration helper methods to clarify which methods update the existing server span and which update the integration's web api span
- Add logging to GetRouteTemplate exception handling, so we can better understand what errors (if any) are emitted there
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:opentelemetry OpenTelemetry support

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants