Skip to content

test(otel): OTel HTTP semantics coverage against the cross-tracer RFC - #7455

Draft
link04 wants to merge 30 commits into
mainfrom
otel-semantics-system-tests-v2
Draft

test(otel): OTel HTTP semantics coverage against the cross-tracer RFC#7455
link04 wants to merge 30 commits into
mainfrom
otel-semantics-system-tests-v2

Conversation

@link04

@link04 link04 commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Motivation

End-to-end coverage for the OTel HTTP semantic conventions the tracers are adding behind DD_TRACE_OTEL_SEMANTICS_ENABLED, checked against the cross-tracer RFC rather than against whatever any one tracer currently emits.

This supersedes #7139, which was closed unmerged. This branch is cut from its head, so its commits are included here.

Changes

31 tests to 44, across six classes: 20 server, 11 client, 5 OTLP-only server, 4 OTLP-only client, 2 error-status config, 2 stats.

One new scenario: OTEL_SEMANTICS_STATS, separate rather than extra env on OTEL_SEMANTICS, because DD_TRACE_COMPUTE_STATS also turns on client-side P0 dropping, which would break every span assertion in the existing scenario.

Substance is in tests/test_otel_http_semantics.py: derived span names, a helper that reads an int attribute from either meta or metrics depending on export path, shared legacy-key tuples, and the server/client split of the OTLP-only class.

What is in the suite, scenario by scenario

44 tests in tests/test_otel_http_semantics.py, six classes, four scenarios. Two classes are shared between OTEL_SEMANTICS and OTEL_SEMANTICS_OTLP, so the same 44 tests produce 75 executions per weblog.

Scenario Classes Tests run Extra env on top of the flag
OTEL_SEMANTICS Server, Client 31 DD_TRACE_CLIENT_IP_ENABLED, DD_TRACE_RESOURCE_RENAMING_ENABLED, obfuscation regexp
OTEL_SEMANTICS_OTLP Server, Client, OTLP_Server, OTLP_Client 40 same, plus DD_TRACE_OTEL_ENABLED and the OTLP exporter pointed at the proxy
OTEL_SEMANTICS_OTLP_CUSTOM_ERROR_STATUSES ErrorStatusConfiguration 2 OTLP, plus DD_TRACE_HTTP_SERVER_ERROR_STATUSES=200
OTEL_SEMANTICS_STATS Stats_Consistency 2 DD_TRACE_COMPUTE_STATS, DD_TRACE_FEATURES=discovery, client_drop_p0s=False

Every class asserts against the same span twice when it runs in both scenarios: once as Datadog MsgPack meta, once as OTLP attributes. That is deliberate, it is how the two-representation rule gets checked without duplicating the test bodies.

Test_OtelSemantics_Spans_Http_Server (20): OTEL_SEMANTICS, OTEL_SEMANTICS_OTLP

Test Request What it asserts
test_otel_attributes_present GET / http.request.method, url.path, url.scheme, status code all present with the right values
test_legacy_attributes_absent GET / + X-Forwarded-For none of http.method, http.url, http.status_code, http.useragent, http.client_ip, http.hostname, network.client.ip, http.query.string survive, in meta and metrics
test_span_name_with_route GET /sample_rate_route/1 span name is {method} {http.route}, derived from the attribute rather than hardcoded
test_span_name_without_route GET /no_such_route_xyz name is {method} {route} conditioned on whatever route the span itself reports, so a framework catch-all still passes, and the URI path never appears in the name
test_span_name_unknown_method PROPFIND / method normalizes to _OTHER, http.request.method_original keeps PROPFIND, and the raw verb never appears in the span name
test_span_name_route_invariance /sample_rate_route/1 vs /99999 two different path params produce the same span name, the low-cardinality guarantee
test_status_500_no_exception_is_status_code_error GET /status?code=500 5xx alone makes a server span an error, with error.type set to the status as a string
test_status_400_is_not_error GET /status?code=400 4xx does not make a server span an error, this is where server and client rules differ
test_status_3xx_is_not_error GET /status?code=302 3xx is not an error
test_url_query_present_with_query_string GET /?otel=visible url.query present, without the leading ?
test_url_query_absent_without_query_string GET / url.query absent rather than empty-string
test_url_query_obfuscation GET /?token=<sensitive> the configured obfuscation regexp applies to url.query, value replaced by redacted
test_user_agent GET / user_agent.original carries the request UA
test_client_address GET / + X-Forwarded-For client.address resolves from the forwarded header, requires DD_TRACE_CLIENT_IP_ENABLED
test_network_peer_address GET / network.peer.address is the socket peer, distinct from client.address
test_server_address GET / server.address is the host the request was sent to
test_server_port GET / server.port is 7777, int on OTLP, string in meta, required once server.address is set
test_http_endpoint_retained GET /no_such_route_xyz the Datadog-only http.endpoint fallback is not stripped by the flag
test_span_kind_is_server GET / span.kind=server / OTLP SERVER
test_http_route_retained GET /sample_rate_route/1 http.route is present and is a template, not the raw path with the parameter baked in

Test_OtelSemantics_Spans_Http_Client (11): OTEL_SEMANTICS, OTEL_SEMANTICS_OTLP

All of these drive the weblog's outbound /make_distant_call endpoint back at itself.

Test Request What it asserts
test_otel_attributes_present outbound GET http.request.method, url.full, server.address, server.port, status code
test_legacy_attributes_absent outbound GET http.method, http.url, http.status_code, out.host, peer.hostname, network.destination.name all gone, meta and metrics
test_url_path_absent_on_client outbound to a routed path url.full present and url.path / url.scheme / url.query absent, the client table is url.full only
test_span_name_is_method outbound GET name is {method} (no tracer emits url.template), and specifically not HTTP GET, which is what upstream otelhttp produces
test_span_name_unknown_method outbound PROPFIND _OTHER + method_original, raw verb absent from the name
test_span_kind_is_client outbound GET span.kind=client / OTLP CLIENT
test_status_400_is_error outbound /status?code=400 4xx is an error on a client span, the deliberate divergence from the Datadog default
test_status_500_is_error outbound /status?code=500 5xx is an error
test_status_3xx_not_error outbound /status?code=302 3xx is not
test_url_full_credential_redaction outbound with user:pass@ userinfo never reaches url.full; if the HTTP API kept it, both halves read REDACTED
test_url_full_query_obfuscation outbound ?token=<sensitive> the obfuscation regexp applies inside url.full, not just to url.query

Test_OtelSemantics_OTLP_Server (5): OTEL_SEMANTICS_OTLP only

Split from the client class on purpose: a tracer converts one span kind at a time, and a combined test reports half-done work as not-started.

Test What it asserts
test_status_code_is_integer_type http.response.status_code is an OTLP intValue, explicitly not a doubleValue
test_server_port_is_integer_type server.port is an intValue
test_http_endpoint_retained_otlp http.endpoint survives the OTLP export, an OTLP-side filter that strips Datadog-only keys must not take it
test_otel_attributes_present_otlp required server keys present on the OTLP span
test_legacy_attributes_absent_otlp legacy server keys absent on the OTLP span

Test_OtelSemantics_OTLP_Client (4): OTEL_SEMANTICS_OTLP only

Same four checks as above, on the client span: two integer types, required keys present, legacy keys absent.

Test_OtelSemantics_OTLP_Spans_Http_ErrorStatusConfiguration (2): OTEL_SEMANTICS_OTLP_CUSTOM_ERROR_STATUSES

Server spans only. DD_TRACE_HTTP_CLIENT_ERROR_STATUSES exists in Java, .NET, Go, Ruby and PHP but not in Python or Node.js, so there is nothing to configure on the client side in the two tracers under test.

Test Request What it asserts
test_server_error_statuses_config_overrides GET / (200) with the range configured to 200, a 200 is an error and error.type is "200"
test_server_error_statuses_config_excludes_500 GET /status?code=500 a 500 is outside the configured range, so it is not an error. This is the half that catches a tracer that unions the configured range with a hardcoded 5xx rule instead of replacing it

Test_OtelSemantics_Stats_Consistency (2): OTEL_SEMANTICS_STATS

Three requests each, then waits for a client-side stats payload.

Test What it asserts
test_stats_agree_with_span_error 3x /status?code=500: the span is an error, a stats bucket exists keyed on the span name with HTTPStatusCode=500, and Hits == Errors
test_stats_agree_with_span_success 3x /status?code=200: the span is not an error and the matching bucket records zero errors

Why this scenario exists: an export-time rename keeps in-process consumers working but lets the span and the stats be computed from two different rules. It also catches the second failure mode, that http.status_code is the key the agent's aggregation reads and the flag suppresses it, so HTTPStatusCode silently becomes 0 unless the tracer routes the numeric status to stats another way.

Tag changes under test, for review

Every assertion below is gated on the flag. With it off, nothing in this suite runs.

Server spans

Datadog name asserted OTel name
http.method http.request.method, _OTHER for an unaccepted verb with http.request.method_original carrying the raw token
http.url url.path, url.query (obfuscated), url.scheme
http.status_code http.response.status_code
http.useragent user_agent.original
http.client_ip client.address
network.destination.ip network.peer.address
http.host server.address, server.port
http.route http.route, unchanged
http.endpoint http.endpoint, unchanged, no OTel equivalent

Client spans

Datadog name asserted OTel name
http.method http.request.method, same _OTHER rule
http.url url.full, no url.path on a client span
http.status_code http.response.status_code
out.host / out.port server.address / server.port

Each rename is asserted twice: the new name present, and the legacy name absent. The legacy tuples are shared constants so a reviewer can see the whole removal list in one place.

Typing. On OTLP exactly two attributes are ints, server.port and http.response.status_code. That is asserted in the OTLP-only classes. On the Agent/MsgPack path every attribute is a string in meta, so the same tests read from meta there. This two-representation rule is why the int helper exists.

Span naming. {method} {http.route} for server, {method} {url.template} for client, bare {method} with no target. This is the OpenTelemetry Collector's set_semconv_span_name (processor/transformprocessor/internal/traces), agreed with the PM and the OTel team. The collector does no substitution, so an unaccepted method names the span _OTHER /users, not HTTP /users. Worth flagging in review: the HTTP semconv prose says HTTP there, and every implementation that normalizes the attribute correctly (dd-trace-js among them) currently emits HTTP. The collector is the divergent one, and it is what we agreed to follow.

Notable fixes inside the branch

  • Client correlation. _server_span correlated the request on user_agent.original only, so for a tracer that has converted its client side but not its server side (dotnet) the lookup missed, fell through to a "first SERVER span in the batch" fallback, and handed every client assertion an unrelated trace id. Reported as 0/11 for a tracer that scores 10/11 on the same span via the Agent path. Fixed by matching whichever user-agent key the tracer emits; correlation is plumbing, not the thing under test. Worth keeping in mind for any shared suite: correlate on something the flag cannot rename.
  • http.endpoint tests were asking for a routed path with DD_TRACE_RESOURCE_RENAMING_ENABLED unset, so the attribute could never exist and all four combinations failed for a reason unrelated to the flag.
  • Removed test_method_original_on_case_variant. It sent a lowercase verb and expected http.request.method_original to preserve it, but no weblog can ever see a lowercase token: requests uppercases in prepare_method, Node's http server uppercases req.method, WSGI uppercases REQUEST_METHOD. It failed identically on every weblog, and the RFC states no case-sensitivity rule. If a language whose server does preserve the token needs checking later, the test has to come back with a raw-socket client.

khanayan123 and others added 24 commits June 15, 2026 22:10
…tests

Add an end-to-end scenario and cross-language tests asserting that HTTP
server and client spans honor the OpenTelemetry HTTP semantic conventions
when DD_TRACE_OTEL_SEMANTICS_ENABLED=true.

- OTEL_SEMANTICS EndToEndScenario sets DD_TRACE_OTEL_SEMANTICS_ENABLED=true
- tests/test_otel_http_semantics.py validates the OTel attribute names are
  emitted and the legacy Datadog names are absent (the flag is mutually
  exclusive), for both server (url.path/url.scheme) and client (url.full,
  no url.path/url.query) spans
- manifests: dotnet and java left active; other languages marked
  missing_feature until their tracers ship the flag

Validates DataDog/dd-trace-dotnet#8791 and DataDog/dd-trace-java#11652.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ked vs tracer PRs

Verified assertions against the actual implementations in dd-trace-dotnet#8791
(SpanMetadataOTelRules.cs, HttpOtelHelper.cs) and dd-trace-java#11652
(OtelHttpSemantics.java):

- server url.query test (was http.query.string) — present when a query is sent
- server error.type test on 5xx — set to the status-code string. java emits this;
  dd-trace-dotnet does not map error status to error.type, so it is gated
  missing_feature for dotnet (per-language manifest)
- server client.address / network.peer.address validated only when present
  (Recommended-level), with a clarifying note on the requirement-level approach
- left client server.port as an if-present check: dotnet omits default ports and
  java falls back to 80/443, so always-present would be wrong

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Round out the OTel HTTP semantic-convention coverage:

- server http.route: asserts the low-cardinality route template (not the raw
  URL path), using the established /sample_rate_route/{i} endpoint. http.route
  is unchanged by the feature but the spec requires it be a template.
- client error.type on a 5xx distant call (java emits; gated missing_feature
  for dd-trace-dotnet, which does not map error status to error.type).
- network.peer.address / network.peer.port validated when present (Recommended)
  on both server and client spans.

15 tests total (9 server, 6 client). ruff + manifest validator + meta-tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…s-meta ports

Verified the resource/span-name behavior across all three tracer PRs
(dd-trace-dotnet#8791, dd-trace-java#11652, dd-trace-js#8933):

- test_resource_name: the server entry-span resource is the low-cardinality
  "{method} {http.route}", never the raw URL path. java rewrites the resource to
  this form; dotnet and nodejs leave the already-compliant DD resource unchanged.
  Expected value derived from the same span's http.route so per-tracer template
  syntax ({i} / {i:int} / :i) cancels out. Gated on the root web span so the java
  spring.handler child (also type==web) is excluded.
- nodejs activated: dd-trace-js#8933 implements the full feature including
  error.type, so its missing_feature gates are removed (now dotnet+java+nodejs).
- numeric attributes (server.port, network.peer.port) read from meta OR metrics:
  dd-trace-js routes numerics into metrics, java/dotnet keep them in meta strings.

16 tests total. ruff + manifest validator + meta-tests green; collect-only passes
for both dotnet and nodejs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ation)

Principle: the system-tests are the authoritative harness — assert exactly what
the OTel HTTP spec requires and record tracer divergences as manifest gates
rather than weakening the assertions. Behavior verified against all three PRs
(dd-trace-dotnet#8791, dd-trace-java#11652, dd-trace-js#8933).

- client server.port: now asserted present (Required per spec; the distant-call
  URL uses a non-default port so every compliant tracer emits it).
- test_no_route_resource: resource is the bare "{method}", never the URL path.
  java + js compliant; dd-trace-dotnet keeps the path -> bug gate.
- test_request_method_normalization: unknown method -> http.request.method=_OTHER
  + http.request.method_original. Only java implements it; dotnet + js -> missing_feature.
- test_other_method_span_name: per spec the _OTHER resource must be "HTTP". No
  tracer complies yet (java keeps raw method -> bug; dotnet/js -> missing_feature) —
  kept as a fully-gated spec anchor that activates as tracers comply.

19 tests total. ruff + manifest validator + meta-tests green; collect-only passes
for dotnet, java, and nodejs.

Note: the unknown-method tests are the first in system-tests to send a non-standard
verb; verified by static analysis, pending a live OTEL_SEMANTICS run to confirm each
weblog produces a span for an unknown method (else gate that language incomplete_test_app).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…query-drop)

Running OTEL_SEMANTICS against dd-trace-js#8933 locally revealed that the express
weblog's /make_distant_call drops the target URL's query string, so a distant call
to /status?code=500 actually hit /status (no code) and returned 400. The PR was
correct (it set error.type="400"); the test was hardcoded to "500".

Fix: distant-call an unmatched route (/no_such_route_xyz -> 404, no query needed) and
assert error.type equals the span's own status code for any 4xx/5xx client response.

Verified end-to-end vs dd-trace-js#8933 (nodejs@6.0.0-pre, express4): 17 passed,
2 xfailed (the _OTHER method-normalization tests, correctly gated missing_feature
for nodejs), 0 failed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…crash

Two things:

1. OTEL_SEMANTICS_OTLP scenario — the OTel-semantics flag plus the APM_TRACING_OTLP
   exporter env, so HTTP attributes are validated as *typed* OpenTelemetry values
   (http.response.status_code / server.port as int; method / url.* / server.address /
   user_agent.original as string) which the Datadog agent protocol cannot represent.
   - tests/test_otel_http_semantics_otlp.py reads typed attributes via
     interfaces.open_telemetry.get_otel_spans().
   - utils/proxy/traces/otlp_v1.py: coerce OTLP intValue to a Python int so the type is
     comparable regardless of JSON vs binary OTLP encoding.
   - Verified e2e vs dd-trace-js#8933 (nodejs@6.0.0-pre): server+client names and types
     pass; status_code-is-int xfails -> the PR emits http.response.status_code as a STRING
     instead of an int (finding for the JS PR; the DD-agent path masks this).
   - Gated the new classes missing_feature for languages where the feature is unimplemented
     (python/golang/php/ruby/rust) or OTLP is unverified (dotnet/java); nodejs active.

2. Fix the "Test the test" CI crash: two manifest entries used `bug (<text>)` with a
   non-JIRA reason. add_pytest_marker -> _ensure_jira_ticket_as_reason calls pytest.exit
   during collection, which aborted the whole TEST_THE_TEST run (surfacing as an unrelated
   test_library_conf remote-config INTERNALERROR). Changed them to `missing_feature`
   (dotnet test_no_route_resource, java test_other_method_span_name). TEST_THE_TEST now
   collects and deselects correctly (288 passed, 2311 deselected locally).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ate OTLP int tests

The HTTP tracer PR now emits http.response.status_code as a numeric metric
(int-typed, so OTLP serializes it as intValue) rather than a meta string —
which is what the spec requires. Update the DD-agent server/client
test_response_status_code and test_error_type to read the value via the
existing _numeric_tag(meta-or-metrics) helper instead of meta only, and
compare error.type against the stringified int.

Now that the tracer emits an int, the OTLP test_status_code_is_int checks
pass, so drop their nodejs missing_feature gates.

Verified by replay against the HTTP+DB tracer build:
  OTEL_SEMANTICS      19 passed, 2 xfailed (method-normalization gates)
  OTEL_SEMANTICS_OTLP  8 passed

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ef_6_0_0)

dd-trace-js#8933 (DD_TRACE_OTEL_SEMANTICS_ENABLED HTTP semantics) merged to
master (semver-minor) and ships in the upcoming v6 release; master is
6.0.0-pre and the released v5.x line does not have it. Leaving the nodejs
tests un-gated would fail them on prod/5.x runs.

Gate the four HTTP semantic-convention test classes to *ref_6_0_0
('>=6.0.0-pre'), the established convention for master-only features (cf.
Test_Startup_Logs::test_startup_logs_default). This means:
  - dev pipeline (dd-trace-js#master == 6.0.0-pre): tests run and must pass
  - prod/5.x: tests xfail cleanly (feature legitimately absent) -> CI green
  - auto-activates once 6.0.0 is released

Other languages remain missing_feature (not yet implemented). The two
server method-normalization tests stay missing_feature -- that behavior
isn't implemented even on master (they xfail on 6.0.0-pre too).

Verified by replay against the 6.0.0-pre build:
  OTEL_SEMANTICS      19 passed, 2 xfailed
  OTEL_SEMANTICS_OTLP  8 passed

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
# Conflicts:
#	manifests/golang.yml
#	manifests/python.yml
The OpenTelemetry HTTP semantic-convention suite needs /sample_rate_route/<i>,
/status, /make_distant_call, and a root route that accepts non-GET methods.
The express4-otel and flask-poc-otel weblogs only had "/" on GET, so the suite
could not be pointed at an upstream OpenTelemetry SDK at all.

Paths and query parameter names are kept identical to the Datadog express and
flask weblogs so the same test code drives both.
OTEL_SEMANTICS_STATS covers the RFC requirement that the span error flag and
the client-side trace stats agree. It is a separate scenario rather than extra
env on OTEL_SEMANTICS because turning on stats computation also turns on
client-side P0 dropping, which would break the span assertions.

OTEL_SEMANTICS_UPSTREAM_SDK runs the same behavior tests against an upstream
OpenTelemetry SDK weblog instead of a Datadog tracer. It is a measurement, not
a release gate: an assertion an upstream SDK fails is either a bug in the
assertion or a place where the RFC deviates from upstream behavior.
OTEL_SEMCONV_STABILITY_OPT_IN=http is required, otherwise the SDKs still emit
the pre-1.0 attribute names and every assertion fails for the wrong reason.

The test_the_test updates keep the legacy scenario matrix and the
tracer_release group rule in agreement with the new scenario.
Four classes of defect, plus the tests the RFC requires and did not have.

Span names are now derived from the attributes the span itself reports rather
than hardcoded. The RFC's system-tests section excludes the server span's
http.route value and name value from the cross-tracer contract as
framework-specific, so a hardcoded "GET /users" contradicted the RFC it was
meant to enforce. Deriving keeps the assertion framework-neutral and still
catches the two real bugs: falling back to the URI path as a target, and
substituting the whole name instead of only the method token for _OTHER.

test_span_name_unknown_method asserted the name was exactly "HTTP". That is
wrong for any framework that resolves a route, since _OTHER substitutes only
the method token. It now derives, and separately asserts the raw verb never
reaches the name.

Int-typed attributes go through _assert_int_attribute, which asserts where the
key lives and what type it has, and that it is never in both meta and metrics.
A bare equality check against an int silently required the value to be a number
in metrics on the agent path, encoding non-standard behavior as the contract.

New tests for RFC requirements that had none: DD_TRACE_HTTP_*_ERROR_STATUSES
taking precedence in both directions, http.request.method_original on a
case-variant method, server.port as an intValue, http.endpoint retained under
the flag including on OTLP, span kind, route invariance across parameter
values, and span error versus trace stats consistency.
_client_span matched on "is a CLIENT span to weblog:7777", which is true of
every distant call in the file. An OTLP payload is a batch, so on a real SDK it
holds spans from unrelated requests and the lookup returned whichever distant
call happened to be first in the batch. Four client tests were asserting
against another test's span and reporting status 200 for a request that asked
for 400, 500, or 302.

Pin the lookup to the trace of the request-correlated server span.
/make_distant_call always issued a GET, so the client-side semantic
convention tests could not drive a non-standard verb through the client
instrumentation and test_span_name_unknown_method looked like a tracer
bug when it was really a weblog gap. The nodejs express weblog already
accepts a method query param; django, flask, fastapi and tornado now do
the same.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
http.endpoint is the endpoint-aggregation fallback a tracer emits only
when the framework resolved no route, and every tracer gates it on
DD_TRACE_RESOURCE_RENAMING_ENABLED. The two "retained under the flag"
tests asked for a routed path with the option unset, so the attribute
could never be present and both tests failed on every tracer for a
reason that had nothing to do with the OTel semantics flag.

Set the option in the OTEL_SEMANTICS and OTEL_SEMANTICS_OTLP scenarios
and request a path with no route.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
manifests/python.yml disabled the whole suite with a blanket
"DD_TRACE_OTEL_SEMANTICS_ENABLED not implemented" declaration, which hid
the fact that most of it already passes. Replace it with per-test
entries for the seven cases that really fail, each with the measured
reason.

manifests/nodejs.yml gets the same weblog_declaration shape for
Test_OtelSemantics_OTLP_Spans_Http_ErrorStatusConfiguration as its
sibling classes, since the four tests xpass, and an incomplete_test_app
entry for the lowercase-method case.

Both languages declare test_method_original_on_case_variant
incomplete_test_app: the HTTP server uppercases the method before any
instrumentation hook runs, so no tracer can see the original token.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The OTEL_SEMANTICS_UPSTREAM_SDK scenario existed but only java_otel,
nodejs_otel and python_otel could run it, so the measurement had a hole
where .NET should be. This adds the fourth.

The weblog is a minimal-API ASP.NET Core 8 app running the real upstream
distro, installed from otel-dotnet-auto-install.sh and activated by
sourcing instrument.sh. Its endpoints mirror flask-poc-otel and
express4-otel path for path and query parameter for query parameter,
including honouring ?method= on /make_distant_call, because a weblog
that answers a slightly different request turns a cross-language
comparison into noise.

Registering a library touches five places plus the CI orchestrator
self-check, which carries its own hardcoded list of otel weblogs.

Run: 30 passed, 7 failed of 37, the best of the three upstream SDKs, and
the only client class that comes back clean. It also surfaces something
new: .NET emits url.query with the leading "?" attached and every value
replaced by "Redacted", which fails url_query_present_with_query_string
and makes both obfuscation tests pass for a reason unrelated to what
they assert.
… emits

A tracer that has converted its HTTP client spans but not its server spans still
reports the system-tests request id under http.useragent, not user_agent.original.
_server_span only looked at user_agent.original, so for that tracer it fell through
to an arbitrary SERVER span in the batch, and _client_span then searched the wrong
trace and found nothing. Every client assertion failed for a reason that had nothing
to do with the client.
Test_OtelSemantics_OTLP asserted on the server span and the client span inside the
same test in four of its five tests. A tracer converts one span kind at a time, so
that shape reports a tracer which has finished half the work as having finished none
of it, and gives whoever is doing the other half no signal. dd-trace-dotnet is the
live example: client-side work only, scored 1/5 on this class while every one of the
four failures was the server span.

Now Test_OtelSemantics_OTLP_Server (5) and Test_OtelSemantics_OTLP_Client (4).
server.port and http.response.status_code stay the only two attributes typed as int,
which is what the RFC's attribute tables specify for both span kinds.
The span name for an unaccepted method was asserted as the literal "HTTP", which is what
the HTTP semconv prose asks for. The decision taken with the PM and the OTel team is to
adopt the collector's set_semconv_span_name algorithm instead, since the Datadog backend
discards OTLP Span.Name anyway and the question is what non-Datadog backends consume.

That algorithm's httpSpanName reads http.request.method and one target key, http.route for
SERVER and url.template for CLIENT, and returns method + " " + target or bare method. It
substitutes nothing, so an unaccepted method names the span _OTHER /users, never HTTP /users.

dd-trace-js currently emits the HTTP literal and passed the old assertion, so it now fails
this test until it follows the agreed algorithm.
It asserted that a lowercase "get" on the wire is reported through
http.request.method_original. The token never reaches the wire: the harness builds requests
with requests.Request(...).prepare(), and PreparedRequest.prepare_method uppercases it. Every
implementation measured failed it identically, all three Datadog branches and all three
upstream OTel SDKs, which is the signature of a harness bug rather than six tracers being
wrong.

Nothing is lost. http.request.method_original is still asserted on both the server and the
client span by test_span_name_unknown_method. What the deleted test uniquely covered was case
sensitivity, and the RFC has no rule for it: lines 142 and 186 define the accepted set as the
RFC 9110 verbs plus PATCH and QUERY without saying whether the comparison is case-sensitive,
and the RFC's own test tables do not list this test. If we want the rule, it belongs in the
RFC first, and the collector's naming algorithm never reads method_original anyway.
@datadog-prod-us1-5

datadog-prod-us1-5 Bot commented Aug 4, 2026

Copy link
Copy Markdown

Pipelines  Tests

Unblock PR with BitsAI

⚠️ Warnings

🚦 11 Pipeline jobs failed

DataDog/system-tests | K8S_LIB_INJECTION_PROFILING_ENABLED: [dd-lib-java-init-test-app, ${PRIVATE_DOCKER_REGISTRY}/system-tests/dd-lib-java-init-test-app, 235494822917.dkr.ecr.us-east-1.amazonaws.com/ssi/cluster-agent:7.81.1, 235494822917.dkr.ecr.us-east-1.amazonaws.com/ssi/apm-inject:latest, 3.231.5, 235494822917.dkr.ecr.us-east-1.amazonaws.com/ssi/dd-lib-java-init:latest]   View in Datadog   GitLab

DataDog/system-tests | K8S_LIB_INJECTION_PROFILING_ENABLED: [dd-lib-java-init-test-app, ${PRIVATE_DOCKER_REGISTRY}/system-tests/dd-lib-java-init-test-app, 235494822917.dkr.ecr.us-east-1.amazonaws.com/ssi/cluster-agent:7.81.1, 235494822917.dkr.ecr.us-east-1.amazonaws.com/ssi/apm-inject:latest, 3.231.5, 235494822917.dkr.ecr.us-east-1.amazonaws.com/ssi/dd-lib-java-init:latest_snapshot]   View in Datadog   GitLab

DataDog/system-tests | K8S_LIB_INJECTION_PROFILING_ENABLED: [dd-lib-java-init-test-app, ${PRIVATE_DOCKER_REGISTRY}/system-tests/dd-lib-java-init-test-app, 235494822917.dkr.ecr.us-east-1.amazonaws.com/ssi/cluster-agent:7.81.1, 235494822917.dkr.ecr.us-east-1.amazonaws.com/ssi/apm-inject:latest_snapshot, 3.231.5, 235494822917.dkr.ecr.us-east-1.amazonaws.com/ssi/dd-lib-java-init:latest]   View in Datadog   GitLab

View all 11 failed jobs.

ℹ️ Info

No other issues found (see more)

🧪 All tests passed
❄️ No new flaky tests detected

Useful? React with 👍 / 👎

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 90b21fb | Docs | Datadog PR Page | Give us feedback!

link04 and others added 5 commits August 4, 2026 18:05
- silence shellcheck SC1091 on the dotnet_otel entrypoint
- declare Test_OtelSemantics_Stats_Consistency in every manifest, not just python
- gate the python activation on v4.15.0-dev
- keep p0 traces in OTEL_SEMANTICS_STATS so the 200 span survives
- wire the five new scenarios into run-end-to-end.yml
- fail loudly instead of picking a stranger when no OTLP server span correlates
- require url.full to exist before asserting credentials were redacted
- normalize an attribute-less OTLP span to a dict, and dedupe spans across payloads
- keep poc-otel out of OTEL_INTEGRATIONS and spring-boot-otel out of the semantics suite

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Upstream-SDK comparison is covered elsewhere, so drop the scenario, the
dotnet_otel poc-otel weblog, and the plumbing that only existed to support
them.
DD_TRACE_HTTP_CLIENT_ERROR_STATUSES is out of scope for the tracer PRs, so no
tracer reads it. Under the flag the client error range is fixed at 400-599,
which the client class already asserts. The server setting is pre-existing and
its two tests stay. 46 tests to 44.
dd-trace-js has no DD_TRACE_HTTP_SERVER_ERROR_STATUSES, so the scenario hands
it a range it cannot read and both assertions fail. Declare it rather than
leave it red; adding the setting to the tracer is separate work.

Also correct the class docstring: DD_TRACE_HTTP_CLIENT_ERROR_STATUSES does
exist, in Java, .NET, Go, Ruby and PHP. It is Python and Node.js that lack it.
libdatadog only turns client-side stats on when the agent advertises
client_drop_p0s, so with it False a tracer built on libdatadog computed no stats
at all and the agent computed them instead. The scenario was reading the agent's
numbers and reporting them as the tracer's, which is the one thing it exists to
check. dd-trace-py sent no v0.6/stats payload at all under it.

Safe to advertise because the scenario pins the sampling rate to 1, so every
trace is p1 and none of the spans these tests read can be dropped, which was the
original reason for turning it off.

Also drops five python manifest entries that are no longer true: the naming ones
said dd-trace-py deliberately does not remap the resource under the flag, which
it now does, and the stats one blamed libdatadog's concentrator, which now reads
the OTel attribute names.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The two Stats_Consistency tests pass against a native extension rebuilt by hand
at the libdatadog patch rev. In CI the python weblog installs a wheel that
compiles src/native against the pinned libdatadog v38.0.0, which predates the
fix, so dropping the declaration would have turned both tests red on this PR.

Reinstated with the dependency named rather than as a bare missing_feature, so
whoever bumps the pin knows this is the thing to re-enable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants