feat(otel): add OpenTelemetry database semantic conventions#8961
feat(otel): add OpenTelemetry database semantic conventions#8961khanayan123 wants to merge 14 commits into
Conversation
Opt-in DD_TRACE_OTEL_SEMANTICS_ENABLED (default false). When enabled, HTTP client and server spans emit OpenTelemetry HTTP semantic-convention attribute names instead of the Datadog ones (replacement, not additive). Client: http.request.method, url.full, server.address, server.port, http.response.status_code (+ error.type on 4xx). Server: http.request.method, url.path, url.scheme, url.query, server.address, server.port, http.response.status_code, http.route, user_agent.original, client.address, network.peer.address (+ error.type on error responses). Branches at the shared chokepoints (datadog-plugin-http client + web.js server util) via a new http-otel-semantics helper, mirroring dd-trace-dotnet#8791 and dd-trace-java#11652. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…erage, all HTTP integrations) Follow-up to the adversarial review of the initial change: - Drop the global span_format.js change; stringify http.response.status_code at the source (client.js, web.js) so only OTel-mode spans are affected. - web.js: skip OTel error.type when an exception is present (don't clobber the exception-derived error.type); gate the OTel client-ip hasTag behind the flag. - decomposeServerUrl: omit server.address for Host-less requests; strip IPv6 brackets. - inferred_proxy, http2 client, undici (native path), next: branch HTTP tags on the flag so all HTTP client/server integrations are consistent (ws stays out of scope: websocket spans are not HTTP request spans). - appsec: emit client.address (not http.client_ip) in OTel mode so it is not additive on top of web.js's client.address. - Tests: web.spec OTel branch coverage (query/obfuscation, client.address, status, 5xx error.type, exception no-clobber, endpoint-omit); http client 4xx error.type; http2 client+server OTel; appsec client.address; helper boundaries (IPv6, Host-less, malformed+query). Drop the tautological constant test; refresh url.js comment. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Closes the codecov/patch gap: the inferred-proxy test covers createInferredProxySpan's OTel block plus web.js's inferred-proxy status line; the undici test covers the native diagnostics-channel OTel path. Verified locally (undici 1 version fails an unrelated old-undici-vs-Node-20 beforeEach, same as the existing undici test). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the per-integration OTel branches with a single flag-gated transform (applyHttpOtelSemantics) invoked from span_format at serialization. The span keeps Datadog tag names throughout its lifetime, so peer.service, AppSec, and trace stats are unaffected; only the serialized output is renamed. Covers every HTTP integration (http/https, http2, undici, fetch, next, all web.js-based servers, inferred-proxy) and ws upgrade spans for free, plus any future HTTP integration, with no per-plugin code. ws/wss schemes are remapped to http/https. Reverts the per-integration branches in http/http2/undici/next/inferred_proxy/appsec and the web.js rename branches. web.js still sets network.peer.address (OTel-gated) since the socket isn't available at serialization. Tests: applyHttpOtelSemantics unit suite + span_format gated-transform test + web.js network.peer.address; existing http/http2/undici/inferred integration OTel tests pass unchanged (they assert serialized output). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…THER, url redaction, client port) From the cross-tracer comparison vs dd-trace-dotnet#8791 and dd-trace-java#11652, in applyHttpOtelSemantics: - Method normalization: verbs outside the known set (RFC 9110 + PATCH + QUERY) become http.request.method=_OTHER with the raw value on http.request.method_original (spec-required; matches java). - Client url.full credential redaction: user:pass@ -> REDACTED:REDACTED@, user@ -> REDACTED@ (spec-mandatory; matches java; no-op when absent). - Client server.port falls back to the scheme default (443/https, 80/http) when no explicit port, since server.port is required for client spans (matches java). Span/resource-name rename (GET /route -> GET) is intentionally NOT changed — a genuine cross-tracer split (java renames, dotnet does not); left as a product decision. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…onv spec Per https://opentelemetry.io/docs/specs/semconv/http/http-spans/: - Span name uses the literal "HTTP" for unknown methods (the spec forbids the raw verb / URL path there). Known-method names stay {method} {route} / {method}, which already matched the spec — no broad rename. - error.type follows the client/server split: client spans on status >= 400 (4xx and 5xx), server spans on 5xx only (4xx MUST be left unset); still no-clobber on an exception-derived type. Adds the previously-missing client-5xx case and marks such spans errored. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Zach Montoya <zach.montoya@datadoghq.com>
Two OTel HTTP semconv fixes from PR review: - http.response.status_code is typed `int` by the spec, but it was emitted as a meta string, which the OTLP exporter serializes as stringValue. Write the parsed status into metrics instead (serialized as intValue), mirroring how server.port is handled; error.type stays the string status. - Client url.full dropped the query (the http/http2/undici plugins strip it when building http.url). url.full must be the absolute URL including the query, so the client plugins now retain it — obfuscated via the configured query-string obfuscation — when OTel semantics are enabled. The URL filter still uses the query-stripped form, and non-OTel http.url is unchanged. getQsObfuscator moved from web.js to url.js (shared, memoized) to compile the obfuscator client-side. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…anFormat The serialization-time transform mutated the formatted span that SpanProcessor also hands to span stats, so SpanAggKey (span_stats.js) read deleted/renamed Datadog keys (http.status_code, http.method, http.endpoint) and aggregated HTTP stats with status 0, empty method, and missing endpoint when DD_TRACE_OTEL_SEMANTICS_ENABLED and stats were both enabled. Move applyHttpOtelSemantics out of spanFormat into SpanProcessor.process, after stats.onSpanFinished and before export, so stats see the Datadog tag names and only the exported payload carries the OTel names. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Rebuild the formatted span's meta/metrics as fresh objects that omit the renamed Datadog HTTP keys, instead of deleting them in place. A single `delete` demotes the formatted span to V8 dictionary mode; the rebuild keeps fast properties — ~40% faster on the transform (~390ns -> ~240ns per HTTP span, reproduced via microbenchmark) and it cannot leak a renamed key as `undefined` on the OTLP export path. - redactUrlCredentials: redact userinfo up to the LAST '@' in the authority, not the first, so `user:p@ss@host` no longer leaks `@ss`. - Guard the http.response.status_code metric with Number.isFinite so a non-numeric status cannot emit a NaN metric. - Drop 11 unused constant exports (only network.peer.address and the two functions cross the module boundary). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The fetch plugin passes a WHATWG URL object as the client options, where the query lives in `options.search` while `options.pathname` is path-only — so `options.path || options.pathname` dropped the query and url.full lost it under DD_TRACE_OTEL_SEMANTICS_ENABLED (unlike http/http2/undici, whose query rides in the raw path). Fold `options.search` in when `options.path` is absent, in the shared http client, so url.full keeps the (obfuscated) query for every client. Adds a fetch OTel integration test covering it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… guard Closes the patch-coverage gaps on the changed lines: getQsObfuscator's boolean / empty / ".*" / invalid-regex / non-string branches plus its compiled cache, and the Number.isFinite guard that skips a non-numeric http.response.status_code metric. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extend DD_TRACE_OTEL_SEMANTICS_ENABLED (HTTP today) to SQL database spans, applied centrally in span_processor alongside the HTTP rename so it covers every SQL integration (pg, mysql, mysql2, mariadb, tedious, ...) and reaches both the agent and OTLP exporters. New packages/dd-trace/src/plugins/util/db-otel-semantics.js renames, per https://opentelemetry.io/docs/specs/semconv/db/database-spans/: db.type -> db.system.name (mapped to the stable value, e.g. postgres -> postgresql) db.name / db.instance -> db.namespace out.host -> server.address network.destination.port (metric) -> server.port (metric, kept numeric for OTLP int typing) and derives db.operation.name + db.collection.name from the query, and db.query.text from the span resource. Datadog-only attributes without an OTel equivalent (db.user, db.pid) are kept; the renamed Datadog names are dropped (mutually exclusive), matching the HTTP behaviour. The rename is export-only — the span keeps Datadog names through its lifetime, so peer.service and trace stats are unaffected. Unit tests in db-otel-semantics.spec.js (8 cases). eslint clean.
Overall package sizeSelf size: 6.27 MB Dependency sizes| name | version | self size | total size | |------|---------|-----------|------------| | import-in-the-middle | 3.1.0 | 101.28 kB | 840.46 kB | | opentracing | 0.14.7 | 194.81 kB | 194.81 kB | | dc-polyfill | 0.1.11 | 25.74 kB | 25.74 kB |🤖 This report was automatically generated by heaviest-objects-in-the-universe |
|
BenchmarksBenchmark execution time: 2026-06-17 17:18:30 Comparing candidate commit 57db962 in PR branch Found 0 performance improvements and 0 performance regressions! Performance is the same for 1935 metrics, 15 unstable metrics.
|
Summary
Extends the opt-in
DD_TRACE_OTEL_SEMANTICS_ENABLEDflag (HTTP today, #8933) to SQL database spans, so they emit OpenTelemetry database semantic-convention attribute names instead of the Datadog ones. When enabled, only the OTel set is emitted (Datadog names replaced, not added alongside).Spec: https://opentelemetry.io/docs/specs/semconv/db/database-spans/
Attribute mapping (Datadog → OTel)
db.typedb.system.namepostgres→postgresql)db.name/db.instancedb.namespaceout.hostserver.addressnetwork.destination.port(metric)server.port(metric)intValuedb.operation.namedb.collection.namedb.query.textDatadog-only attributes without an OTel equivalent (
db.user,db.pid) are preserved.Implementation
packages/dd-trace/src/plugins/util/db-otel-semantics.jswithapplyDatabaseOtelSemantics(formattedSpan), mirroringhttp-otel-semantics.js.span_processor.jsnext to the HTTP rename, under the same flag — export-only (the span keeps Datadog names through its lifetime, sopeer.serviceand trace stats are unaffected) and central, so it covers every SQL integration (pg, mysql, mysql2, mariadb, tedious, …) and reaches both the agent and OTLP exporters. No-op for non-SQL spans (gated on the Datadogdb.typetag).Verification
db-otel-semantics.spec.js(8 cases) — rename,db.system.namevalue mapping, operation/collection parsing, mutual exclusion, no-op for non-DB spans.eslintclean.OTEL_SEMANTICS_DB(feat(otel): system-tests for DB (postgres) OpenTelemetry semantic conventions system-tests#7166). Against this branch, all 7 DB semantic-convention tests pass — the postgres SQL span emits:db.type/db.name/out.host/network.destination.portall absent. (On dd-trace-js master those same tests xfail, confirming this PR is what enables them.)Notable
db.system.nameis the stable spec name (the experimental name wasdb.system); the value is mapped to the stable identifier (postgresql,mysql,mariadb,microsoft.sql_server,oracle.db).Follow-ups (out of scope)
db.query.textsanitization (literals →?) for non-parameterized queries — currently the (agent-obfuscated) query text; the spec recommends tracer-side sanitization.db.response.status_codeon failed queries; non-SQL stores (mongodb) which use a different attribute set.🤖 Generated with Claude Code