Skip to content

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

Merged
zacharycmontoya merged 36 commits into
masterfrom
otel-aspnetcore
Sep 1, 2026
Merged

zacharycmontoya merged 36 commits into
masterfrom
otel-aspnetcore

Conversation

@zacharycmontoya

@zacharycmontoya zacharycmontoya commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Summary of changes

Updates the server spans produced by the ASP.NET Core automatic instrumentation so that their span name and request attributes match the OpenTelemetry HTTP server span specification when DD_TRACE_OTEL_SEMANTICS_ENABLED=true, mirroring the HTTP client work from #8975. Also adds an OTLP-based ASP.NET Core test harness so the new attribute set can be snapshot-tested end-to-end, alongside the existing msgpack-based Datadog snapshot tests.

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, not just HTTP clients. Today an ASP.NET Core server span emits 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 align with the OpenTelemetry HTTP server span specification. Ideally, users running the latest .NET OpenTelemetry ASP.NET Core instrumentation would be able to switch freely between the two without needing to update dashboards, monitors, or downstream tooling, but these changes will prefer adhering to the specification if any differences arise.

Implementation details

WebTags changes

Following the same pattern established for HttpTags in #8975, WebTags now carries first-class properties for OpenTelemetry concepts, in addition to the existing Datadog ones. When a property is represented in both semantics, the corresponding tag name is emitted based on Span.OpenTelemetrySemanticsEnabled:

Property Datadog semantics OpenTelemetry semantics
HttpMethod http.method http.request.method
HttpUserAgent http.useragent user_agent.original
HttpClientIp http.client_ip client.address
NetworkClientIp network.client.ip network.peer.address
HttpStatusCode http.status_code http.response.status_code
HttpRequestHeadersHost http.request.headers.host (not emitted)
HttpUrl http.url (remapped to url. attributes)
HttpRequestMethodOriginal (not emitted) http.request.method_original
UrlScheme (not emitted) url.scheme
UrlPath (not emitted) url.path
UrlQuery (not emitted) url.query
ServerAddress (not emitted) server.address
ServerPort (not emitted) server.port

HttpUrl/HttpRequestHeadersHost have no OpenTelemetry equivalent because the spec splits that single concept into url.scheme/url.path/url.query and server.address/server.port.

Conversely, HttpRequestMethodOriginal and the url.*/server.* properties are OpenTelemetry-only and are never populated under Datadog semantics. HttpStatusCode and ServerPort are backed by an int? and serialized as integer attributes over OTLP.

Datadog-specific attributes (aspnet_core.route, aspnet_core.endpoint, http.endpoint) are retained as we aim to be a superset of attributes, but we may consider removing them at a later time.

HttpSemanticConventions

Adds HttpSemanticConventions.SetHttpServerUrlTags(tags, scheme, host, port, pathBase, path, queryString, queryStringManager), the server-side counterpart to the client entry point from #8975. It builds the url.*/server.* attributes directly from the individual ASP.NET Core request components and runs the query string through the same QueryStringManager truncation/obfuscation used elsewhere.

GetRequestMethodOriginal(originalMethod, normalizedMethod) is extracted out of the client-only method-normalization logic added in #8975 so both the client and server code paths share it.

AspNetCoreHttpRequestHandler

  • GetDefaultResourceName uses the new otelSemanticsEnabled parameter to quickly return the {method} since no http.route was identified.
  • StartAspNetCorePipelineScope generates an ASP.NET Core span with correct naming/attributes and calls SetHttpServerUrlTags when OpenTelemetry semantics are enabled.
  • RouteTemplateResourceNamesEnabled is now implicitly forced on by OtelSemanticsEnabled, because the spec requires the low-cardinality route in http.route, which Datadog only tracks when that feature is on.
  • IsKnownWebTag is now extended to also recognize the new OpenTelemetry tag names, so the ASP.NET Core Activity (which sets some of the same OpenTelemetry attributes independently) can't clobber the values this instrumentation already set. This allows us to continue using the backing WebTags, even though the attribute values should be identical.

AspNetCoreDiagnosticObserver / SingleSpanAspNetCoreDiagnosticObserver

  • The resource/span name is rebuilt as {method} {http.route} using the route verbatim (lower-cased, unsimplified, reported exactly as-is or / if empty) instead of the Datadog "simplified route template" logic.
  • Under OpenTelemetry semantics, StartMvcCoreSpan no longer creates the aspnet_core_mvc.request child span since OpenTelemetry requires a single span. Instead, the route/resource name is written onto the root span directly.
  • AppSec's path-params check, which previously ran against the (now-absent) MVC child span, is re-pointed at the root span when OpenTelemetry semantics are enabled (SingleSpanAspNetCoreDiagnosticObserver already did for its own single-span mode).

TracerSettings

When OtelSemanticsEnabled is true, three related feature flags are now force enabled/disabled so the tracing output aligns with the spec (and the final config value is recorded to telemetry as Calculated):

  • RouteTemplateResourceNamesEnabled → forced true (spec requires the low-cardinality route).
  • SingleSpanAspNetCoreEnabled → forced true, but only on .NET 6+, since the single-span observer isn't supported on older TFMs. On pre-.NET 6 runtimes, AspNetCoreDiagnosticObserver's existing OTel-semantics branches (e.g. skipping the MVC child span) remain the fallback.
  • ExpandRouteTemplatesEnabled → forced false (spec requires the unexpanded route template, not one with parameter values substituted in).

Span tag lookup refactor

To account for other internal call sites that request HTTP information via span.GetTag, the following helpers were added:

  • Tagging/IHasHttpMethod — implemented by HttpTags, WebTags, AwsSdkTags, and InferredProxyTags to enable more optimized access to the HttpMethod
  • SpanExtensions.GetHttpMethod(span) — reads the new strongly-typed property when available and otherwise falls back to GetTag
  • SpanExtensions.GetHttpClientIp(span) and SpanExtensions.GetNetworkClientIp(span) — used by AppSec's SecurityCoordinator and SecurityReporter and by ApiSecurity/IastModule/StatsAggregator.

Test coverage

Unit tests

  • TagsListTests — New coverage asserting that each WebTags 1:1 rename resolves through GetTag/SetTag under either name, that exactly one wire name is enumerated per semantics mode, and that the OpenTelemetry-only attributes (url.*, server.*, http.request.method_original) are emitted under their own name and omitted under Datadog semantics.

Integration tests — new OTLP test harness

  • VerifyHelper — Scrubs values reported under OpenTelemetry-specified span attributes so snapshots stay stable.
  • OtlpTestAgentSession — Consolidates shared logic for getting OTLP from the ddapm test-agent, keyed by a unique X-Datadog-Test-Session-Token header per test case so concurrent XUnit sessions don't collide. This also introduces ClearSessionWhenQuietAsync and CheckAvailabilityAsync for long-lived servers (IIS/Kestrel) that outlive a single test case and need their own health-check bypassed once OTLP export takes over from the mock DD agent, and filters out any spans whose timestamp predates the request start so leftover spans from a prior test don't leak into a snapshot.
  • OtlpAspNetCoreTestBase — The OTLP parallel to AspNetCoreMvcTestBase which creates the AspNetCoreTestFixture and identifies shared test input routes.
  • OtlpAspNetCoreMvc31Tests, OtlpAspNetCoreMinimalApisTests, OtlpAspNetCoreMvc21Tests — New end-to-end snapshot tests to verify OTLP export + OTel semantics for the test applications Samples.AspNetCoreMvc31, Samples.AspNetCoreMinimalApis, and Samples.AspNetCoreMvc21 respectively.

Understanding the snapshots
To understand how OTel semantics are reported based on the actual HTTP requests/responses, I recommend observing the diffs between the following snapshots:

  • "GET /api/delay/0" vs "GET /api/delay/0?id=1" vs "GET /api/delay/0?token=SUPER-SECRET-TOKEN-VALUE" - Identifies basic routes and how the query string is reported
  • "GET /path-base/api/delay/0" vs "GET /api/delay/0" - Identifies that http.route reported without the request's PathBase
  • "GET /ping" vs "FOO /ping" - Identifies how http.request.method_original is reported
  • "GET /status-code/302" vs "GET /status-code/400" vs "GET /status-code/500" vs "GET /bad-request" - Identifies how errors are handled for HTTP server spans

Also, I ran all of the following configurations so we could see the difference in all scenarios, but we can reduce the tested configurations as needed:

  • Default
  • DD_TRACE_ROUTE_TEMPLATE_RESOURCE_NAMES_ENABLED=true
  • DD_TRACE_SINGLE_SPAN_ASPNETCORE_ENABLED=true (.NET 6+ only)
  • DD_TRACE_OTEL_SEMANTICS_ENABLED=true

Other details

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

JIRA: APMAPI-2054

Follow-up work

  1. The recommended network attributes (network.protocol.name, network.protocol.version) are still unset on server spans.
  2. ASP.NET server spans will be tackled in a subsequent PR.

@dd-trace-dotnet-ci-bot

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

Copy link
Copy Markdown

Execution-Time Benchmarks Report ⏱️

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

✅ No regressions detected

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

@pr-commenter

pr-commenter Bot commented Aug 5, 2026

Copy link
Copy Markdown

Benchmarks

Benchmark execution time: 2026-09-01 18:59:26

Comparing candidate commit 9f89fbe in PR branch otel-aspnetcore with baseline commit 05fca85 in branch master.

📊 Benchmarking dashboard

Found 0 performance improvements and 0 performance regressions! Performance is the same for 72 metrics, 0 unstable metrics, 67 known flaky benchmarks, 59 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 ----------------------------------'

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.OpenTelemetry.InstrumentedApi.Trace.TelemetrySpanBenchmark.StartSpan_AddEvent_Sampled net472

  • 🟩 throughput [+8757.067op/s; +9408.615op/s] or [+5.572%; +5.986%]

scenario:Benchmarks.Trace.ActivityBenchmark.StartStopWithChild net472

  • 🟥 throughput [-8972.334op/s; -8475.324op/s] or [-10.639%; -10.049%]

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

  • 🟥 throughput [-9280.620op/s; -7937.679op/s] or [-9.436%; -8.071%]

scenario:Benchmarks.Trace.AgentWriterBenchmark.WriteAndFlushEnrichedTraces net472

  • 🟥 allocated_mem [+1.573KB; +1.573KB] or [+47.789%; +47.804%]
  • 🟥 execution_time [+306.539ms; +308.594ms] or [+152.115%; +153.135%]
  • 🟥 throughput [-54.838op/s; -51.008op/s] or [-9.867%; -9.177%]

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

  • 🟥 allocated_mem [+1.014KB; +1.014KB] or [+37.603%; +37.615%]
  • 🟥 execution_time [+375.698ms; +377.652ms] or [+296.824%; +298.368%]
  • 🟩 throughput [+71.134op/s; +77.952op/s] or [+9.379%; +10.278%]

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

  • 🟥 allocated_mem [+1.090KB; +1.090KB] or [+40.417%; +40.429%]
  • 🟥 execution_time [+397.725ms; +399.612ms] or [+351.971%; +353.641%]

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

  • 🟥 allocated_mem [+4.725KB; +4.726KB] or [+99.482%; +99.497%]
  • 🟥 throughput [-60977.261op/s; -60542.335op/s] or [-47.443%; -47.105%]

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

  • 🟥 allocated_mem [+3.848KB; +3.848KB] or [+81.379%; +81.391%]
  • 🟩 execution_time [-16.528ms; -12.360ms] or [-7.719%; -5.773%]
  • 🟥 throughput [-59644.915op/s; -56887.889op/s] or [-43.537%; -41.525%]

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

  • 🟥 allocated_mem [+4.576KB; +4.576KB] or [+98.954%; +98.966%]
  • 🟥 throughput [-49666.321op/s; -47421.821op/s] or [-44.904%; -42.875%]

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

  • 🟥 allocated_mem [+1.348KB; +1.348KB] or [+109.063%; +109.078%]
  • 🟥 throughput [-299671.103op/s; -295552.827op/s] or [-30.598%; -30.177%]

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

  • 🟥 allocated_mem [+511 bytes; +512 bytes] or [+41.822%; +41.834%]
  • 🟩 execution_time [-27.368ms; -14.018ms] or [-12.205%; -6.251%]
  • 🟥 throughput [-134556.444op/s; -97359.175op/s] or [-14.375%; -10.401%]

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

  • 🟥 allocated_mem [+1.312KB; +1.312KB] or [+108.600%; +108.616%]
  • 🟥 throughput [-184073.621op/s; -168267.099op/s] or [-26.448%; -24.177%]

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

  • 🟥 allocated_mem [+3.378KB; +3.378KB] or [+89.003%; +89.017%]
  • 🟥 throughput [-72476.286op/s; -71725.209op/s] or [-48.776%; -48.270%]

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

  • 🟥 allocated_mem [+3.336KB; +3.336KB] or [+88.150%; +88.161%]
  • 🟥 throughput [-72768.591op/s; -69883.626op/s] or [-46.301%; -44.466%]

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

  • 🟥 allocated_mem [+3.264KB; +3.264KB] or [+88.493%; +88.506%]
  • 🟥 throughput [-56279.207op/s; -53663.624op/s] or [-44.834%; -42.750%]

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

  • 🟩 throughput [+248269.158op/s; +364545.134op/s] or [+8.278%; +12.155%]

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

  • 🟩 execution_time [-19.155ms; -14.819ms] or [-8.830%; -6.831%]

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

  • 🟩 allocated_mem [-13.759KB; -13.756KB] or [-42.324%; -42.316%]
  • 🟥 execution_time [+300.857ms; +301.665ms] or [+150.328%; +150.732%]
  • 🟩 throughput [+1006.734op/s; +1027.673op/s] or [+11.119%; +11.351%]

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

  • 🟩 allocated_mem [-13.722KB; -13.718KB] or [-42.341%; -42.329%]
  • 🟥 execution_time [+300.075ms; +303.361ms] or [+151.329%; +152.986%]
  • 🟩 throughput [+2399.626op/s; +2619.707op/s] or [+18.354%; +20.037%]

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

  • 🟩 allocated_mem [-13.722KB; -13.718KB] or [-42.341%; -42.329%]
  • 🟥 execution_time [+300.292ms; +302.838ms] or [+151.264%; +152.546%]
  • 🟩 throughput [+1793.505op/s; +1922.012op/s] or [+17.315%; +18.556%]

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

  • 🟥 execution_time [+296.587ms; +298.478ms] or [+145.672%; +146.601%]
  • 🟩 throughput [+554.667op/s; +572.983op/s] or [+14.704%; +15.190%]

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

  • 🟥 execution_time [+294.469ms; +297.729ms] or [+143.955%; +145.549%]
  • 🟩 throughput [+2763.232op/s; +2822.603op/s] or [+40.145%; +41.007%]

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

  • 🟥 execution_time [+301.051ms; +302.475ms] or [+150.465%; +151.177%]
  • 🟩 throughput [+1230.835op/s; +1318.657op/s] or [+24.431%; +26.174%]

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

  • 🟩 execution_time [-146.396µs; -142.188µs] or [-30.057%; -29.193%]
  • 🟩 throughput [+850.685op/s; +879.281op/s] or [+41.432%; +42.825%]

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

  • 🟩 execution_time [-139.108µs; -112.451µs] or [-31.905%; -25.791%]
  • 🟩 throughput [+862.832op/s; +985.058op/s] or [+37.512%; +42.826%]

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

  • 🟩 execution_time [-144.873µs; -122.903µs] or [-31.040%; -26.332%]
  • 🟩 throughput [+797.237op/s; +879.618op/s] or [+36.802%; +40.605%]

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

  • 🟩 execution_time [-127.972µs; -123.828µs] or [-34.552%; -33.433%]
  • 🟩 throughput [+1367.785op/s; +1415.259op/s] or [+50.656%; +52.415%]

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

  • 🟩 execution_time [-103.463µs; -79.900µs] or [-33.030%; -25.508%]
  • 🟩 throughput [+1206.195op/s; +1406.485op/s] or [+37.601%; +43.844%]

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

  • 🟩 execution_time [-137.988µs; -115.620µs] or [-37.748%; -31.629%]
  • 🟩 throughput [+1334.586op/s; +1470.618op/s] or [+47.893%; +52.775%]

scenario:Benchmarks.Trace.AspNetCoreBenchmark.SendRequest net472

  • 🟥 execution_time [+299.663ms; +300.477ms] or [+149.562%; +149.969%]

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

  • 🟥 execution_time [+419.447ms; +427.273ms] or [+455.747%; +464.250%]
  • 🟩 throughput [+778.732op/s; +973.425op/s] or [+6.399%; +7.999%]

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

  • unstable execution_time [+245.363ms; +310.817ms] or [+186.302%; +236.000%]

scenario:Benchmarks.Trace.CIVisibilityProtocolWriterBenchmark.WriteAndFlushEnrichedTraces net472

  • unstable execution_time [+349.005ms; +407.919ms] or [+160.469%; +187.557%]
  • 🟥 throughput [-566.718op/s; -516.182op/s] or [-51.350%; -46.771%]

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

  • unstable execution_time [+207.403ms; +340.691ms] or [+88.386%; +145.188%]
  • 🟥 throughput [-672.861op/s; -589.418op/s] or [-44.880%; -39.314%]

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

  • unstable execution_time [+254.728ms; +311.279ms] or [+152.357%; +186.181%]
  • 🟥 throughput [-411.866op/s; -373.998op/s] or [-28.678%; -26.041%]

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

  • 🟩 execution_time [-367.949µs; -145.439µs] or [-12.801%; -5.060%]
  • unstable throughput [+22.964op/s; +71.569op/s] or [+6.601%; +20.572%]

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

  • 🟩 execution_time [-172.885µs; -149.912µs] or [-8.758%; -7.594%]
  • 🟩 throughput [+41.923op/s; +48.836op/s] or [+8.276%; +9.641%]

scenario:Benchmarks.Trace.ElasticsearchBenchmark.CallElasticsearch net472

  • 🟥 execution_time [+301.158ms; +303.597ms] or [+151.657%; +152.886%]

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

  • 🟥 execution_time [+302.596ms; +305.413ms] or [+151.632%; +153.043%]

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

  • 🟥 execution_time [+300.578ms; +304.515ms] or [+150.998%; +152.975%]

scenario:Benchmarks.Trace.ElasticsearchBenchmark.CallElasticsearchAsync net472

  • 🟥 execution_time [+302.814ms; +304.918ms] or [+152.063%; +153.119%]

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

  • 🟥 execution_time [+299.193ms; +300.980ms] or [+147.938%; +148.821%]

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

  • 🟥 execution_time [+303.882ms; +307.511ms] or [+154.021%; +155.860%]

scenario:Benchmarks.Trace.GraphQLBenchmark.ExecuteAsync net472

  • 🟥 execution_time [+302.257ms; +304.962ms] or [+151.706%; +153.063%]

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

  • 🟥 execution_time [+303.884ms; +307.255ms] or [+151.459%; +153.138%]
  • 🟩 throughput [+42399.243op/s; +52403.522op/s] or [+8.419%; +10.406%]

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

  • 🟥 execution_time [+301.327ms; +304.201ms] or [+149.907%; +151.338%]

scenario:Benchmarks.Trace.ILoggerBenchmark.EnrichedLog net472

  • 🟥 throughput [-18293.511op/s; -16591.304op/s] or [-7.356%; -6.672%]

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

  • 🟩 execution_time [-16.904ms; -13.245ms] or [-7.861%; -6.159%]

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

  • unstable execution_time [+11.890µs; +55.961µs] or [+2.937%; +13.823%]

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

  • 🟩 allocated_mem [-19.186KB; -19.164KB] or [-6.999%; -6.991%]
  • unstable execution_time [-27.166µs; +30.561µs] or [-5.369%; +6.040%]
  • unstable throughput [-110.077op/s; +91.285op/s] or [-5.493%; +4.555%]

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

  • unstable execution_time [-69.755µs; -7.742µs] or [-12.088%; -1.342%]
  • unstable throughput [+41.450op/s; +219.892op/s] or [+2.368%; +12.563%]

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

  • unstable execution_time [+9.629µs; +14.986µs] or [+22.759%; +35.422%]
  • 🟥 throughput [-6170.394op/s; -4109.325op/s] or [-25.975%; -17.299%]

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

  • unstable execution_time [-15.856µs; -8.182µs] or [-24.601%; -12.694%]
  • unstable throughput [+2206.328op/s; +3878.865op/s] or [+13.537%; +23.798%]

scenario:Benchmarks.Trace.Log4netBenchmark.EnrichedLog net472

  • 🟥 execution_time [+302.392ms; +304.179ms] or [+152.845%; +153.749%]

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

  • 🟥 execution_time [+299.062ms; +302.437ms] or [+152.222%; +153.939%]

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

  • 🟥 execution_time [+299.182ms; +302.088ms] or [+149.777%; +151.232%]

scenario:Benchmarks.Trace.RedisBenchmark.SendReceive net6.0

  • 🟩 throughput [+30473.673op/s; +39113.690op/s] or [+5.768%; +7.403%]

scenario:Benchmarks.Trace.SerilogBenchmark.EnrichedLog net472

  • 🟥 execution_time [+299.551ms; +301.635ms] or [+149.299%; +150.338%]

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

  • 🟥 execution_time [+298.868ms; +300.968ms] or [+150.077%; +151.132%]

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

  • 🟥 execution_time [+301.545ms; +303.967ms] or [+152.924%; +154.152%]

scenario:Benchmarks.Trace.SingleSpanAspNetCoreBenchmark.SingleSpanAspNetCore net472

  • 🟥 execution_time [+299.485ms; +300.339ms] or [+149.385%; +149.810%]
  • 🟩 throughput [+61226413.220op/s; +61521830.853op/s] or [+44.589%; +44.804%]

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

  • 🟥 execution_time [+414.995ms; +418.399ms] or [+516.120%; +520.353%]

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

  • 🟥 execution_time [+299.783ms; +300.811ms] or [+149.525%; +150.038%]

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

  • 🟩 throughput [+80859.705op/s; +89328.675op/s] or [+7.550%; +8.340%]

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

  • 🟩 throughput [+66135.580op/s; +74098.746op/s] or [+6.568%; +7.359%]

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

  • 🟩 throughput [+47796.286op/s; +54905.244op/s] or [+8.679%; +9.970%]

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

  • 🟩 throughput [+74200.460op/s; +92203.878op/s] or [+8.290%; +10.301%]

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 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.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 netcoreapp3.1
  • scenario:Benchmarks.Trace.Iast.StringAspectsBenchmark.StringConcatBenchmark net472
  • scenario:Benchmarks.Trace.RedisBenchmark.SendReceive net472
  • 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.StartFinishSpan net6.0
  • 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

@zacharycmontoya
zacharycmontoya force-pushed the otel-aspnetcore branch 2 times, most recently from f1664a2 to 30e3052 Compare August 11, 2026 22:20
@zacharycmontoya zacharycmontoya added area:opentelemetry OpenTelemetry support docker_image_artifacts Use to label PRs for which you would need a Docker Image created for. labels Aug 11, 2026
@zacharycmontoya zacharycmontoya changed the title feat(otel): Emit OpenTelemetry HTTP semantic convention on HTTP server spans feat(otel): Emit OpenTelemetry HTTP semantic conventions on HTTP server spans (ASP.NET Core) Aug 11, 2026
Base automatically changed from otel-httpclient to master August 12, 2026 16:33
@zacharycmontoya
zacharycmontoya force-pushed the otel-aspnetcore branch 7 times, most recently from 94908cc to f13c1e3 Compare August 14, 2026 17:23
@zacharycmontoya
zacharycmontoya marked this pull request as ready for review August 14, 2026 20:05
@zacharycmontoya
zacharycmontoya requested review from a team as code owners August 14, 2026 20:05
@zacharycmontoya
zacharycmontoya requested review from vandonr and removed request for a team August 14, 2026 20:06

@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: f043b02dc7

ℹ️ 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/DiagnosticListeners/AspNetCoreDiagnosticObserver.cs Outdated
@github-actions

github-actions Bot commented Aug 14, 2026

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.

2 occurrences of :

-      http.request.method: GET,

@bouwkast bouwkast left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM I just have a few comments on semantics mainly

Comment thread tracer/src/Datadog.Trace/OpenTelemetry/HttpSemanticConventions.cs Outdated
Diff the new OtlpAspNetCoreMinimalApisTests.DatadogSemantics.* snapshots with the corresponding OtlpAspNetCoreMinimalApisTests.OtelSemantics file to see the difference in output when each feature flag is turned on. Notably, the OTLP output is NOT affected by the DD feature flags.

At a high-level, the diff from Datadog semantics <> OTel semantics is the following.

Modified (resource) names.

Removed attributes:
- component
- http.request.headers.host
- operation.name
- resource.name
- runtime-id
- service.name
- span.kind
- span.type

Renamed attributes:
- http.method => http.request.method
- http.url => url.scheme + server.address + server.port + url.path + url.query
- http.status_code => http.response.status_code
- http.useragent => user_agent.original

Added attributes:
- http.request.method_original
- http.route
Diff with AspNetCoreMvc21Tests.cs To see how this OTLP test setup differs from the previous DD MsgPack tests
…bled AspNetCore instrumentation only emits Datadog HTTP semantics (instead of both Datadog and OpenTelemetry)
…-Session-Token header for communicating with the dd apm test agent for each individual test case to prevent multiple concurrent XUnit sessions from colliding
…s-only, and its OpenTelemetry-semantics equivalent is HttpSemanticConventions.SetHttpServerRequestValues.
- Improve logic for getting the http.request.method and http.request.method_original attribute values
- Update OpenTelemetrySdkTests to use dd-apm-test-agent sessions
- Use file scoped namespaces (applied to OtlpTestAgentSession.cs)
- Remove the XUnit Test Collection TestAgentOtlpCollection since now different classes can run concurrently because they submit OTLP traces by sessions (and tests under Datadog.Trace.ClrProfiler.IntegrationTests already have parallelization disabled by our CustomTestFramework)
…iving from OtlpAspNetCoreTestBase.

This is accomplished by doing the following:
- At the end of each test case execution, the OTLP traces for this test case are cleared, by calling Fixture.OtlpSession.ClearSessionAsync() in OtlpAspNetCoreTestBase.DisposeAsync.
- At the start of each test case execution, invoke the test Fixture with a cached initialization result,  similar to our common Fixture.TryStartApp, by calling ixture.EnsureInitializedAsync in OtlpAspNetCoreTestBase.InitializeAsync. Rather than use the pre-existing EnsureServerStarted, we pass our own delegate to cache.
- Now we can remove the unnecessary polling so it's done at the start of each test class rather than each test case.
- Re-insert the OTLP start time assertion in OtlpSnapshotHelper.NormalizeSpans now that traces are properly isolated per test case
- Force SingleSpanAspNetCoreEnabled to true when OpenTelemetry semantic mode is enabled
- Force RouteTemplateResourceNamesEnabled to true when OpenTelemetry semantic mode is enabled
- Update AspNetCoreHttpRequestHandler.IsKnownWebTag to only filter the OpenTelemetry attribute names when the span is using OpenTelemetry semantics, since that's the only time we're trying to deduplicate span attributes.
- Remove unused regexes in VerifyHelper
- Add explicit unit tests to cover the behavior of SpanExtensions.GetHttpMethod() to only search the tag for the span's semantic mode, rather than both DD and OTel keys
…osable and its consumer classes implement IAsyncLifetime, to make sure we start/clear sessions in a consistent manner
- Small changes to comments in AspNetCoreDiagnosticObserver and OtlpAspNetCoreTestBase to note that we're targeting the OTel spec first, not the OpenTelemetry ASP.NET Core instrumentation
- Optimize WebTags storage so we don't allocate additional fields for the default Datadog semantics
…P.NET Core:

- Report http.route exactly as-is (not lowercased), or "/" if empty. This is handled by a new helper method HttpSemanticConventions.GetHttpRoute
- Update our OtlpAspNetCoreTestBase with two more routes to execute some ASP.NET Core edge cases: "GET /" and "GET /rewrite-me"
- Fix WebTags unit tests regression by allowing the user to set OTel tags in the WebTags object, but still allocate the OtelTags object lazily
- Update AspNetCore snapshots with route fix
- Update Ocelot snapshot with updates to IsKnownWebTag implementation
- Update OtlpTestAgentSession.WaitForSpansAsync and its invocation in OtlpAspNetCoreTestBase so we filter out any spans whose time stamp is older than the request start time
…spNetCoreTestBase with "GET /path-base/api/delay/0"

With OTel semantics, the http.route is reported without the PathBase, so http.route reports the same as "GET /api/delay/0"
…/path-base/api/delay" call so we snapshot test it correctly
- Simplify the InitializeAsync / DisposeAsync for OpenTelemetryHttpClientTests and OpenTelemetryWebRequestTests after the OTLP agent session refactoring
- Remove the need to expose internal types to the Datadog.Trace.ClrProfiler.IntegrationTests by making types public and using the actual Newtonsoft.Json package
- Update TracerSettings when OTel semantics is enabled to only force enable SingleSpanAspNetCoreEnabled on .NET 6+ (where it's supported) and force disable ExpandRouteTemplatesEnabled
@zacharycmontoya
zacharycmontoya merged commit 0dc64ab into master Sep 1, 2026
144 checks passed
@zacharycmontoya
zacharycmontoya deleted the otel-aspnetcore branch September 1, 2026 19:47
@github-actions github-actions Bot added this to the vNext-v3 milestone Sep 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:opentelemetry OpenTelemetry support docker_image_artifacts Use to label PRs for which you would need a Docker Image created for.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants