Repository navigation
Conversation
…s config Adds the OpenTelemetry HTTP semantic convention attribute names next to the Datadog ones in ddtrace.ext.http and ddtrace.ext.net, and a client side error status setting so HTTP client spans can have their own error ranges. DD_TRACE_HTTP_CLIENT_ERROR_STATUSES (alias DD_HTTP_CLIENT_ERROR_STATUSES) is unset by default, which keeps the current behavior of applying the server ranges to client spans. DD_TRACE_HTTP_SERVER_ERROR_STATUSES now also accepts the DD_HTTP_SERVER_ERROR_STATUSES spelling that the other tracers support. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Implements the OpenTelemetry HTTP semantic conventions for client and server spans behind DD_TRACE_OTEL_SEMANTICS_ENABLED. The whole implementation lives in set_http_meta and the URL tagging helpers, so no integration needs to know which mode is active. set_http_meta is called on every HTTP span, so instead of branching on the flag per call the OTel implementation is bound to the name once at import time. The flag cannot change after startup, which makes the swap safe, and it keeps the Datadog path free of any new work. Values are deliberately produced by the same helpers as before, so enabling the flag moves attribute names without changing what a value contains: credential redaction and query obfuscation behave the same. The exceptions are the two places the spec requires: url.full keeps redacted userinfo instead of dropping it, and url.query is always obfuscated because it is the only place a server query string can live once http.url is gone. Server spans mark 5xx as errors, client spans mark 4xx and 5xx, and error.type carries the status code as a string unless an exception already set it. A user configured error status range still wins in either mode. Numeric attributes are written as integers when the process exports over OTLP and as strings otherwise, because DD MsgPack requires them in the meta map while OTLP requires the integer type. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Everything that reads HTTP tags back off a span now resolves the attribute name from the active semantics mode: endpoint simplification, API Security's URL fallback, AI Guard's client IP copies, peer.service inference, the inferred proxy span, and the few places outside set_http_meta that tag a status code, a URL, a query string or a method on a request span. http.endpoint has no OTel equivalent, so it is retained in both modes and stays the primary source for API Security and endpoint aggregation. The Datadog spellings that intake keys off, such as the ai_guard.* copies and the inferred proxy span's own tags, also stay as they are; only the attribute their value is read from moves. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The implementation is swapped in at import time, so patching the config at runtime has no effect and these tests set DD_TRACE_OTEL_SEMANTICS_ENABLED in a subprocess instead. They build spans directly rather than through a tracer so they do not need a trace agent. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Codeowners resolved asResolved from the full PR diff against |
Circular import analysis
|
🎉 All green!🧪 All tests passed 🔗 Commit SHA: 215d466 | Docs | View more details | Give us feedback! |
- gate the server.address peer-service source behind DD_TRACE_OTEL_SEMANTICS_ENABLED - drop the un-gated DD_HTTP_SERVER_ERROR_STATUSES alias - make _is_http_client_span span-kind aware - mark the ray_serve inbound span as SERVER - keep status and method on the span when get_request_uri throws - share one path_source_tag_value helper for the endpoint source tag - clarify the release note Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
BenchmarksBenchmark execution time: 2026-08-28 23:46:29 Comparing candidate commit 215d466 in PR branch Found 0 performance improvements and 7 performance regressions! Performance is the same for 574 metrics, 10 unstable metrics, 2 known flaky benchmarks, 16 flaky benchmarks without significant changes.
|
…e client setting No tracer ships a DD_HTTP_CLIENT_ERROR_STATUSES name, so aliasing to it invents a legacy spelling that never existed. Keep the single canonical DD_TRACE_HTTP_CLIENT_ERROR_STATUSES, matching this repo's existing DD_TRACE_HTTP_SERVER_ERROR_STATUSES, and add the missing docs entry. Also flag the server.address peer.service source as pending sign-off.
New configuration in every tracer, so it belongs in its own cross-tracer PR rather than riding along with the semconv attribute work. Under the flag the client error range is now fixed at 400-599. The pre-existing DD_TRACE_HTTP_SERVER_ERROR_STATUSES is untouched and still drives server spans.
DD_TRACE_HTTP_CLIENT_ERROR_STATUSES exists in Java, .NET, Go, Ruby and PHP. Python is one of the two tracers that lack it, which is the actual reason the range is fixed here.
…ntics Each integration composes its resource name its own way, and none of those are valid OTel span names. Django joins the method with the resolved route, falls back to the view handler when there is none, and normalizes a Resolver404 to "GET 404". Under DD_TRACE_OTEL_SEMANTICS_ENABLED the name is now recomputed from the span's own attributes instead, by the OpenTelemetry Collector algorithm the tracers agreed to adopt: http.request.method, plus the target if the span published one, and the bare method otherwise. The target is http.route on server spans and url.template on client spans. Deriving rather than composing is the point. The target can never be something the span does not also publish, which makes "instrumentation MUST NOT default to using URI path as a target" unreachable rather than merely forbidden, and the method token is whatever the attribute says, so a raw verb outside the accepted set cannot leak into the name. Two cases are deliberately left alone. A resource the application set itself wins, for the same reason Span.update_name wins over instrumentation in the OTel SDKs; Django already tracked that ownership and now records it. And an ASGI websocket handshake reports "websocket" as its method, which normalizes to _OTHER, so renaming those would collapse every websocket endpoint in an app onto one name. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Make configuration, resource naming, and sampling decisions consistent before OTLP export so downstream stats and rules observe the final semantic values. Co-authored-by: Cursor <cursoragent@cursor.com>
Dependency direction analysis📈 Existing violations got worse29 pre-existing violation(s) increased in severity (e.g. their target became more depended-on, or got pulled into an import cycle), though the edge itself isn't new: Show violations that got worse (showing 5 of 29 highest severity)
|
Force OTLP export, preserve explicit resources, and align HTTP naming, status, and credential handling with the agreed semantic-convention scope. Co-authored-by: Cursor <cursoragent@cursor.com>
dubloom
left a comment
There was a problem hiding this comment.
I mostly let some high level comments because there will be second round of reviews.
Something that I think can become quickly tricky to understand is that, sometimes otel compatibility is hidden (for instance the name of a tag changes based on otel or not), sometimes it is explicit. I think we should try to make it implicit as most as possible
| def _normalize_http_method(method: str) -> tuple[str, Optional[str]]: | ||
| """Split a raw HTTP method into (http.request.method, http.request.method_original). | ||
|
|
||
| The second element is None when it would duplicate the first, which is the only case | ||
| the semantic conventions allow http.request.method_original to be omitted. Case | ||
| normalization counts as a difference, so "get" yields ("GET", "get"). | ||
| """ | ||
| upper = method.upper() | ||
| if upper in _KNOWN_HTTP_METHODS: | ||
| return upper, (None if upper == method else method) | ||
| return OTHER_HTTP_METHOD, method |
There was a problem hiding this comment.
Can't we use an hashmap for that ? instead of calling upper ?
This is kind of a micro optimization especially as it is cached but I don't think we need a whole function call for that
There was a problem hiding this comment.
A finite raw-string map cannot recognize every mixed-case spelling without enumerating all case combinations or changing behavior.
I kept the cached upper() normalization because HTTP methods can arrive in arbitrary mixed case; a finite lookup would either enumerate every casing or change behavior.
Did you have a particular lookup structure in mind that preserves mixed-case normalization?
| return OTHER_HTTP_METHOD, method | ||
|
|
||
|
|
||
| def _credentials_redacted_url(url: str) -> str: |
There was a problem hiding this comment.
I think I saw munir making a comment like that but I think this kind of helper function should live in a opentelemetry module. That would make file smaller and makes code clearer
There was a problem hiding this comment.
This overlaps with Munir’s helper-placement thread. Span-dependent helpers cannot move without introducing dependency-direction violations. Would you prefer moving only the pure parsing and formatting helpers into ddtrace.internal.opentelemetry.utils?
Resolve the semantics mode once instead of storing it as a module-level flag: each dual-behaviour helper now has a Datadog and an OpenTelemetry implementation, and one block binds the public names. Nothing branches per span and no copy of the setting is kept for other modules to read. Report the DD_TRACE_SPAN_ATTRIBUTE_SCHEMA conflict once from Config rather than latching a module-level bool in the schema module, name the feature that gets disabled in the peer-service warning, and drop the duplicate telemetry entries for configurations _get_config already reports. Rename RESOURCE_SET_BY_OTEL to INSTRUMENTATION_HTTP_RESOURCE: the value stored is the resource the integration composed, which is not necessarily an OTel name. It is a span context item and never serialized, so span output is unchanged. Trim comments that restated the environment variable name, and shorten the release note to a summary plus a docs link.
The previous commit bound implementations at import. That removed the stored module-level flag but kept the part the review actually objected to: the value is still frozen at import, so mock.patch.object(config, "_otel_trace_semantics_enabled", True) has no effect and only subprocess tests can exercise the flag. #17718 reads config._otel_trace_semantics_enabled at call time and its tests patch it with mock.patch.object. Follow that: the helpers in trace_utils_base read the setting directly, and SERVER_URL_TAG / USER_AGENT_TAG become server_url_tag() / user_agent_tag() so the attribute name is resolved per call too. The lookup is a plain instance attribute, about 5ns more than a module global, which is not worth freezing behaviour for. Callers updated: starlette, and the AppSec flask and fastapi contribs.
Applies the review's core point: the span should be created with its OTel name rather than remapped later. set_http_meta is already the single writer of http.request.method and http.route, so it now composes the resource there via set_otel_http_resource. An integration that resolves its route on a second call, starlette being the case that forced the old two-phase design, simply calls again and the more specific name wins. OtelSpanNamingProcessor is deleted, along with its registration and the before_sampling hook in Tracer.sample. The guards it needed because it saw every span (span type, missing method) are unnecessary from the integration: spans that never call set_http_meta are never named. The naming algorithm, the websocket exclusion and the user-override detection move to ddtrace/_trace/otel_http_naming.py, which contrib may import (layers.json allows contrib -> product:tracing). Tests drive set_http_meta through tracer.start_span instead of constructing Span objects and invoking a processor, which also addresses the review note about building spans directly in this file.
Removing the before_sampling hook left a gap the review caught. Propagation
can force a sampling decision before the request finishes, and only the HTTP
client subscriber set a resource at span start, so a rule could match a
server span's Datadog resource (aiohttp.request) while the span exported as
"GET /users/{id}".
Both subscribers now name the span through set_otel_http_resource on start.
Server spans get the method alone, which is what before_sampling produced at
that point, and set_http_meta appends the route later. Routing the client
through the same helper also normalizes an unaccepted method at start, so
sampling no longer sees PROPFIND on a span that ships as HTTP.
An integration that writes span.resource mid-request is indistinguishable
from user code doing the same, so the OTel naming backed off and left the
Datadog value on the span. Falcon's process_resource is the clear case: it
set "GET SomeResource" and the span then exported with that instead of
"GET /things/{id}".
set_instrumentation_resource sets the resource and records instrumentation
as its owner in one step, so the marker cannot drift from the value. Applied
to the HTTP server integrations that write the resource outside the
subscriber: falcon (both sites), asgi's 404 path, and tornado's handler name.
Also records why the helpers stay in ddtrace/_trace: moving them under
ddtrace/internal/opentelemetry, as the review suggested, adds 7 dependency
direction violations, because that package is zoned as an isolated product
while this code operates on Span and is imported by both contrib and the
tracing subscribers.
…tests The HTTP helpers this PR adds to trace_utils_base are imported by the subscribers, the AppSec contribs and several integrations, so the leading underscore was wrong: normalize_http_method, set_method_tag, set_query_string_tag, set_status_code_tag, set_url_tags_server and http_block_metadata are now public. Helpers that were already private on main are left alone. DEFAULT_SCHEME_PORTS moves to ddtrace/internal/constants.py, which contrib may import, since it is a fixed scheme-to-port table rather than configuration. The Django coverage no longer mocks the resolver and the config to call a private helper. Three subprocess tests drive a real request instead and assert what the span carries: the route-derived name with the flag on, HTTP substituted for an unaccepted method, and the Datadog names unchanged with the flag off.
…s its own Several integrations create the span with a resource already set: requests passes "GET /actual/path" through HttpClientRequestEvent. The subscriber then asked set_otel_http_resource to name the span, which saw a resource matching neither the span name nor any marker, concluded a user owned it, and left the URI path as the span name for both sampling and export. That is the shape the conventions explicitly forbid. The subscribers now record instrumentation ownership of the event-supplied resource before naming, so the rename proceeds while a user replacing the resource later is still honoured. The same unmarked-write audit is extended past falcon, asgi and tornado to sanic, starlette and the WSGI span modifier, which all compose a resource outside the subscriber. httplib's post-set_http_meta override is deleted instead: set_http_meta now names the span, and the override reintroduced PROPFIND over the HTTP the conventions require.
The requests regression got through because every naming test called set_otel_http_resource directly with a default resource, so none of them exercised a span that arrives with a resource already set by the integration. These drive core.context_with_event with requests-shaped event data and assert what a sampling rule would match: GET and HTTP rather than the URI path, for an accepted method, an unaccepted one and a lowercase one. A second test pins that a user replacing the resource after start still wins, so the ownership marker cannot become a blanket override of user intent. Verified by removing the ownership record and watching the first test fail.
Deleting the before_sampling hook rested on an untested assumption: that the span carries its OTel name by the time a rule is evaluated. These two subprocess tests settle it from opposite directions. A rule on the OTel name rejects the span, and a rule on the resource the integration supplied does not match at all, so the rename provably happens before sampling rather than only before export.
_set_http_meta_otel was a 124-line near-copy of set_http_meta: 68 of its lines were identical, including client-IP resolution, header tracing, response headers, cookies and the AppSec dispatch. That duplication is what the review objected to, so the two are now one function that branches only where the conventions actually differ, which is eight places: the method attribute, URL tagging, server.address precedence, the status code and its error semantics, the query string, and the user-agent, client-IP and peer-IP names. Removing the import-time binding also finishes the earlier change: the setting is read per call here as well as in trace_utils_base, so mock.patch.object(config, "_otel_trace_semantics_enabled", True) now switches set_http_meta itself, matching how #17718's tests drive the flag. Previously only the helpers responded to it, which was the inconsistency raised in review. Verified by capturing the tags, metrics, resource and error flag for 14 request shapes in both flag states before and after: output is identical apart from per-process values. trace_utils.py loses 124 lines.
The review asked for snapshots as well as subprocesses. These were not possible while the setting was frozen at import, because a snapshot test drives the in-process client; collapsing set_http_meta made the flag readable per call, so mock.patch.object now switches the whole request path. Two snapshots over a real request: the route-derived name with the flag on, and an unaccepted method reported as _OTHER while the span is named HTTP. They capture the whole span rather than the handful of tags a unit test asserts, so the absence of the Datadog names is covered too.
Track late integration resource updates so OTel naming and trace-level sampling remain consistent, and cover the affected framework and client lifecycles. Co-authored-by: Cursor <cursoragent@cursor.com>
Keep user and processor resource customizations intact while ensuring sampling observes normalized routes without executing framework routing logic early. Co-authored-by: Cursor <cursoragent@cursor.com>
Prevent unrelated deprecated trace ID settings inherited from CI from polluting subprocess stderr. Co-authored-by: Cursor <cursoragent@cursor.com>
Retain manually resolved matches for downstream AppSec consumers and close test SQLite connections across URLConf reloads. Co-authored-by: Cursor <cursoragent@cursor.com>
Resolve routes through Django's own lifecycle so AppSec can block path attacks without invoking converters twice. Co-authored-by: Cursor <cursoragent@cursor.com>
Use Django's pre-view middleware stage on versions without resolve_request so AppSec blocks early without running path converters twice. Co-authored-by: Cursor <cursoragent@cursor.com>
emmettbutler
left a comment
There was a problem hiding this comment.
This change is quite large - consider breaking it up into smaller sections that can be merged independently. I flagged a few separation-of-concerns issues that should be adjusted before this is merged.
| except (TypeError, ValueError): | ||
| log.debug("failed to convert http status code %r to int", status_code) | ||
| return | ||
| span._set_attribute(http.OTEL_RESPONSE_STATUS_CODE, otel_number(int_status_code)) |
There was a problem hiding this comment.
Code that sets attributes on Span objects should live under ddtrace/_trace, not ddtrace/contrib
There was a problem hiding this comment.
Thank you @emmettbutler, I've applied those in the new PRs, feel free to review:
- feat(tracing): add OTel HTTP semantics foundation #20008
- feat(tracing): emit OTel HTTP semantics for client spans #20009
- feat(tracing): emit OTel HTTP semantics for server spans #20010
Let me know if you would like the separated by Integration change instead.
| if server_address is not None: | ||
| span._set_attribute(net.SERVER_ADDRESS, server_address) | ||
| elif target_host is not None: | ||
| span._set_attribute(net.SERVER_ADDRESS, target_host) |
There was a problem hiding this comment.
Code that sets attributes on Span objects should live under ddtrace/_trace, not ddtrace/contrib
There was a problem hiding this comment.
Thank you @emmettbutler, I've applied those in the new PRs, feel free to review:
- feat(tracing): add OTel HTTP semantics foundation #20008
- feat(tracing): emit OTel HTTP semantics for client spans #20009
- feat(tracing): emit OTel HTTP semantics for server spans #20010
Let me know if you would like the separated by Integration change instead.
…nsions (#2323) ### What does this PR do? Preserves OpenTelemetry HTTP semantics in libdatadog's in-process consumers and OTLP output. #### Trace stats and OTLP trace metrics - reads canonical Datadog HTTP method/status keys first, then falls back to `http.request.method` and `http.response.status_code` - accepts valid numeric or string status values without allowing empty, malformed, non-finite, or non-integral values to shadow a valid fallback - emits OTel trace-metric dimensions for method, status, route, span name, and kind - leaves successful `status.code` unset and prevents additional tags from overriding the computed status ### HTTP tag handling libdatadog does not rename tracer span tags in this PR. It accepts the Datadog and OpenTelemetry names below as equivalent inputs to trace-stat aggregation and emits the canonical OpenTelemetry name in OTLP trace metrics. | Concept | Datadog span input | OpenTelemetry span input | OTLP trace-metric output | | --- | --- | --- | --- | | HTTP method | `http.method` | `http.request.method` | `http.request.method` | | HTTP response status | `http.status_code` | `http.response.status_code` | `http.response.status_code` as an integer | | Span error status | Span error flag | Span error flag | `status.code=STATUS_CODE_ERROR` for errors; omitted for successful spans | When both naming variants are present, the Datadog name wins. Empty or malformed values fall through to the alternate name instead of hiding a valid value. #### OTLP sampled flags Sampling priority is a trace-level decision, but v0.4/v0.5 inputs may attach `_sampling_priority_v1` to only one span. The mapper now uses the same first-priority policy as `drop_chunks`, treats a missing priority as sampled, and applies one decision to every span in the chunk. This prevents sampled traces from exporting child spans with `flags=0` and keeps OTLP flags consistent with the existing chunk-retention policy. No public signature, wire-format, setting, or environment-variable change is introduced. ### Motivation Under `DD_TRACE_OTEL_SEMANTICS_ENABLED`, spans use OTel HTTP names before client-side stats are computed. Stats and OTLP metrics must preserve those dimensions, and all spans in an exported trace must carry the same sampled decision. Companion changes: - Historical full Python PR: [DataDog/dd-trace-py#19501](DataDog/dd-trace-py#19501) - Native Python stack: [foundation #20008](DataDog/dd-trace-py#20008), [client #20009](DataDog/dd-trace-py#20009), [server #20010](DataDog/dd-trace-py#20010) - [DataDog/dd-trace-js#9688](DataDog/dd-trace-js#9688) - [DataDog/system-tests#7455](DataDog/system-tests#7455) ### Testing - `cargo fmt --check -p libdd-trace-utils` - mapper unit tests: **31 passed** - OTLP trace-exporter integration tests: **3 passed** - trace-stats tests: **43 passed** End-to-end re-run from libdatadog head `0ed7fd9bd` with the final Python server layer and companion system tests: | Scenario | Result | | --- | ---: | | `OTEL_SEMANTICS_OTLP_TRACE_METRICS` | **4 passed** | | `OTEL_SEMANTICS_OTLP_SAMPLING_RULES` | **2 passed** | Co-authored-by: maximo.bautista <maximo.bautista@datadoghq.com>
## Description Adds the shared foundation for emitting OpenTelemetry HTTP semantic conventions when `DD_TRACE_OTEL_SEMANTICS_ENABLED=true`. This PR does **not** change HTTP integrations yet. The client and server integrations start using this foundation in #20009 and #20010. This is the first part of the implementation for #19501. ### Configuration `DD_TRACE_OTEL_SEMANTICS_ENABLED` remains disabled by default. When enabled, it enforces the settings required by the OTel HTTP semantics path: - OTLP trace export - Span attribute schema `v0` - Peer-service defaults disabled - Native span events enabled for OTLP export Conflicting settings are overridden and reported through logs and telemetry. ### Shared HTTP semantics The implementation lives under `ddtrace/_trace/otel/http/`: - `tags.py` handles HTTP method normalization, URL attributes, server address and port extraction, query-string handling, status codes, and client/server error classification. - `resource.py` generates low-cardinality HTTP resource names and prevents later instrumentation updates from overwriting resources set by user code. `OTelHTTPSpanAttributes` is shared by the upcoming client and server layers. Those layers will instantiate it from `set_http_meta()` and provide metadata as it becomes available during the request lifecycle. For example: ```python attributes = OTelHTTPSpanAttributes(span, integration_config) attributes.set_method("get") attributes.set_url("https://example.com/users/42") attributes.set_status_code(200) attributes.set_resource("/users/{id}") ``` This produces: ```text http.request.method = "GET" http.request.method_original = "get" url.scheme = "https" url.path = "/users/42" http.response.status_code = 200 resource = "GET /users/{id}" ``` Recreating the helper during later metadata phases is supported: method, resource, and status-error ownership are stored on the span. ### OTel requirements covered The behavior follows the [OpenTelemetry HTTP span conventions](https://opentelemetry.io/docs/specs/semconv/http/http-spans/). | Scenario | OTel behavior | Covered behavior | | --- | --- | --- | | Method `get` | Normalize the method and preserve its original spelling | Emits `http.request.method="GET"` and `http.request.method_original="get"` | | Unknown method `PROPFIND` | Emit `_OTHER` and retain the original method | Emits `_OTHER`, preserves `PROPFIND`, and uses `HTTP` for resource naming | | Server route `/users/{id}` | Use the low-cardinality route template instead of the raw request path | Refines the resource to `GET /users/{id}` | | Client URL containing credentials | Redact user information from `url.full` | Emits `https://REDACTED:REDACTED@example.com/search` | | Client 4xx response | Mark the span as an error | Sets `span.error=1` and a string status-derived `error.type` | | Server 4xx response | Do not mark the span as an error by default | Leaves `span.error=0` | | Server 5xx response | Mark the span as an error | Sets `span.error=1` | Existing Datadog query capture, query obfuscation, and custom server error-range settings remain supported. Exception information takes precedence over status-derived error metadata. ### Resource ownership Resource naming distinguishes instrumentation-generated values from user replacements: 1. Instrumentation may progressively refine a resource as more metadata becomes available. 2. If user code replaces that resource, subsequent instrumentation updates preserve the user value. 3. Raw request paths are never used as OTel server resources; route templates are used when available. WebSocket resources are left unchanged to avoid collapsing them to the unknown-method resource name. ## Testing Unit coverage includes: - Method normalization and stale `method_original` cleanup - Client and server URL attributes - Credential redaction and query-string obfuscation - Malformed URLs - Client/server status-code error classification - Repeated metadata updates - Exception preservation - User-owned resource preservation - Schema and peer-service overrides - OTLP writer selection and native span events End-to-end OTLP snapshots are added later in the stack because they depend on the harness introduced in #20688: - #20689: HTTP client snapshots - #20690: HTTP server and AppSec snapshots ## Risks The feature remains opt-in. This PR configures the enabled mode and adds the shared implementation, but it does not change emitted HTTP integration spans until the client and server follow-up PRs are merged. ## PR stack 1. #20008 — shared configuration and HTTP semantics foundation 2. #20688 — OTLP snapshot harness 3. #20009 — HTTP client integration 4. #20689 — client configuration and snapshots 5. #20010 — HTTP server/framework and AppSec integration 6. #20690 — AppSec block-path attributes and server snapshots Co-authored-by: Hanane-Laassilia <hanane.laassilia@datadoghq.com>
Important
This original PR is retained as historical review and system-test context. Active review continues in the native GitHub stack: foundation #20008, HTTP client #20009, and HTTP server/framework/AppSec #20010. Please place new feedback on the relevant layer to avoid duplicate work. All existing review threads, including resolved and outdated threads, were reconciled against the replacement stack.
What does this PR do?
Implements the agreed OpenTelemetry HTTP semantic conventions behind
DD_TRACE_OTEL_SEMANTICS_ENABLEDfor Python HTTP server and client spans.The flag now provides one coherent mode across capture, sampling, and export:
server.portandhttp.response.status_codeas numeric OTLP attributesHTTPfor unknown methodshttp.endpointfor route-less responsesv0, and peer-service defaults off when semantics mode is enabled@HTTP tag changes
When OpenTelemetry semantics are enabled, Python emits the following tag names and omits the corresponding DD aliases. Tags not listed here, including
http.route,http.endpoint, configured header tags, and retry/status-message tags, retain their existing names.http.methodhttp.request.methodhttp.request.method_originalwhen normalization changes the valuehttp.urlurl.fullwith credential redaction and query obfuscationhttp.urlurl.path,url.scheme, and optionalurl.queryhttp.query.stringor query insidehttp.urlurl.queryout.hostserver.addresshttp.urlserver.addressserver.porthttp.status_codeas a stringhttp.response.status_codeas an integer over OTLPhttp.useragentuser_agent.originalhttp.client_ipclient.addressnetwork.client.ipnetwork.peer.addresserror.typecontaining the response status when it caused the errorNo new HTTP status-code setting is introduced. Existing server status configuration remains covered.
DD_TRACE_HTTP_CLIENT_ERROR_STATUSESis not implemented by this PR, and its two system tests remain explicitmissing_featureexpectations.Dependency
This PR depends on DataDog/libdatadog#2323 being merged and released. Python currently consumes libdatadog v41, so the released compatible version must include the trace-metric and sampled-flag changes before this feature can be released. The dependency override used for local verification is not committed in this branch.
Motivation
Customers migrating from an OpenTelemetry SDK should retain OTel HTTP names, types, status behavior, low-cardinality span names, sampling inputs, and trace metrics. An export-only rename is insufficient because sampling and stats consume spans earlier.
Companion changes:
Testing
Focused Python verification: 24 passed.
System-tests using a wheel built from this branch with the source changes from libdatadog #2323 overlaid on the v41-compatible dependency graph:
OTEL_SEMANTICS_OTLPOTEL_SEMANTICS_OTLP_CUSTOM_ERROR_STATUSESOTEL_SEMANTICS_OTLP_TRACE_METRICSOTEL_SEMANTICS_OTLP_SAMPLING_RULES