Skip to content

Emit OpenTelemetry semantic conventions on HTTP spans - #12765

Open
zacharycmontoya wants to merge 6 commits into
masterfrom
otel-http-semconv
Open

zacharycmontoya wants to merge 6 commits into
masterfrom
otel-http-semconv

Conversation

@zacharycmontoya

@zacharycmontoya zacharycmontoya commented Oct 6, 2026 •

Copy link
Copy Markdown

What Does This Do

This PR updates HTTP client and server spans produced by the shared automatic-instrumentation decorators so that their span names, attributes, and error handling follow the OpenTelemetry HTTP semantic conventions when DD_TRACE_OTEL_SEMANTICS_ENABLED=true.

Behavior remains unchanged when OpenTelemetry semantics are disabled.

Implementation Details

Configuration

Enabling DD_TRACE_OTEL_SEMANTICS_ENABLED overrides the effective values of the following settings:

  • OTEL_TRACES_EXPORTER=otlp
  • DD_WRITER_TYPE=OtlpWriter
  • DD_TRACE_SPAN_ATTRIBUTE_SCHEMA=v0
  • DD_TRACE_PEER_SERVICE_DEFAULTS_ENABLED=false
  • DD_HTTP_CLIENT_ERROR_STATUSES=400-599 (in line with the OpenTelemetry specification)

HTTP client changes

When DD_TRACE_OTEL_SEMANTICS_ENABLED=true:

  • Span resource names become a low-cardinality {method} instead of {method} {path}.
  • Unknown methods use HTTP as the span name, emit _OTHER as http.request.method, and preserve the original method in http.request.method_original.
  • URLs emit as url.full instead of http.url, with credentials redacted and query parameters obfuscated.
  • Destination information uses server.address and server.port, including default ports 80 and 443.
  • HTTP status codes 400–599 are errors by default (changed from 400-499).
  • Error responses set error.type to the status code unless a more specific error type already exists.
  • Legacy peer-host, peer-port, and peer-service attributes are not generated.

HTTP server changes

When DD_TRACE_OTEL_SEMANTICS_ENABLED=true:

  • Span names become {method} {http.route} when a route is available, or {method} otherwise. Request paths and 404 are no longer used as naming fallbacks.
  • Unknown methods use HTTP as the span name, emit _OTHER as http.request.method, and preserve the original method in http.request.method_original.
  • The request URL is split into url.scheme, url.path, and the obfuscated url.query, rather than a singular http.url.
  • Server identity uses server.address and server.port, honoring forwarded protocol, host, and port headers.
  • Client information emits as client.address and network.peer.address.
  • HTTP status codes 500–599 are errors by default (unchanged).
  • Error responses set error.type to the status code unless a more specific error type already exists.
  • Inferred-proxy and Axway spans follow the same naming and attribute rules.
  • Endpoint discovery falls back to url.path when the legacy http.url attribute is absent

Test coverage

Unit and component tests

  • OtelHttpSemanticsTest covers method normalization, URL credential redaction, query removal, fragment preservation, default ports, and error-type behavior.
  • HttpDecoratorsOtelSemanticsForkedTest covers client and server decorator output, route naming, status handling, forwarded origins, explicit forwarded ports, and IPv6 hosts.
  • ConfigTest verifies the forced OpenTelemetry settings, calculated configuration values, and client error-status defaults.
  • QueryObfuscatorTest verifies sanitized client url.full reconstruction and server/client handling of url.query.
  • HttpEndpointPostProcessorTest verifies endpoint discovery from url.path.
  • KnownTagsTest and OtlpTraceJsonCollectorTest verify tag registration and the OpenTelemetry names emitted over OTLP.

Instrumentation tests

The shared HTTP test suites run in a forked JVM with OpenTelemetry semantics enabled for:

HTTP clients:

  • Apache HTTP Client
  • Apache HTTP Async Client
  • Google HTTP Client
  • HttpURLConnection
  • JAX-RS Client
  • OkHttp
  • Play WSClient

HTTP servers:

  • Akka-Http Server
  • JAX-RS Server
  • Jersey Server
  • Netty HTTP server
  • Servlet 3 / Jetty
  • Play 2.6
  • Ratpack HTTP server
  • Spring Web MVC 6
  • Spring WebFlux
  • Tomcat
  • Undertow
  • Vert.x

System tests

This is being tested by this PR: DataDog/system-tests#7919 . Currently the only test skips across all web servers are for Akka and Play servers, where the http.route needs to be set on the root span. I added this below as follow-up work so the majority of frameworks could be served here without being blocked.

Motivation

When DD_TRACE_OTEL_SEMANTICS_ENABLED=true, the Java tracer should produce HTTP spans that align with the OpenTelemetry semantic conventions instead of emitting a mixture of Datadog and OpenTelemetry naming and attributes. This lets users switch between OpenTelemetry and Datadog automatic instrumentation with reduced updates to dashboards, monitors, or downstream processing.

Additional Notes

This PR is easiest to review commit-by-commit: configuration, tag registration, client behavior, server behavior, client integration coverage, and server integration coverage are separated into focused commits.

Related .NET implementations: HTTP client spans and HTTP server spans.

Follow-up work requires the following:

  • Spring WebClient: The two HTTP client spans must be consolidated to 1 span, in accordance with the OpenTelemetry HTTP semantic conventions
  • Akka HTTP and Play: Both frameworks need to apply routing information to the root span's http.route

Contributor Checklist

Jira ticket(s): [https://datadoghq.atlassian.net/browse/APMAPI-2038], [https://datadoghq.atlassian.net/browse/APMAPI-2039]

@zacharycmontoya
zacharycmontoya changed the base branch from master to dougqh/tag-registry-otel October 6, 2026 20:21
@datadog-prod-us1-5

datadog-prod-us1-5 Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

🎯 Code Coverage (details)
• Patch Coverage: 69.78%
• Overall Coverage: 59.85% (+0.03%)

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: c8d62ca | Docs | Give us feedback!

@zacharycmontoya
zacharycmontoya force-pushed the otel-http-semconv branch 4 times, most recently from f6f083f to 1f7dfff Compare October 6, 2026 21:11
@dd-octo-sts

dd-octo-sts Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

🟢 Java Benchmark SLOs — All performance SLOs passed

Suite Status
Startup 🟢 pass

SLO thresholds are defined here based on automatically generated metrics. A warning is raised when results are within 5% of the threshold.

PR vs. master results
Scenario Candidate master Δ (95% CI of mean)
startup:insecure-bank:iast:Agent 13.89 s 13.95 s [-1.2%; +0.4%] (no difference)
startup:insecure-bank:tracing:Agent 12.90 s 12.99 s [-1.4%; -0.1%] (maybe better)
startup:petclinic:appsec:Agent 17.12 s 16.92 s [+0.3%; +2.0%] (maybe worse)
startup:petclinic:iast:Agent 16.92 s 17.02 s [-1.4%; +0.2%] (no difference)
startup:petclinic:profiling:Agent 16.52 s 16.97 s [-3.6%; -1.6%] (significantly better)
startup:petclinic:sca:Agent 17.04 s 17.01 s [-0.8%; +1.1%] (no difference)
startup:petclinic:tracing:Agent 16.08 s 16.04 s [-0.8%; +1.2%] (no difference)

Commit: c8d62ca2 · CI Pipeline · Benchmarking Platform UI


Load and DaCapo benchmarks can be triggered manually in the GitLab pipeline. Results will appear in the Benchmarking Platform UI after completion.

@zacharycmontoya
zacharycmontoya force-pushed the otel-http-semconv branch 6 times, most recently from 4891a63 to 8cfde98 Compare October 6, 2026 23:45
@zacharycmontoya
zacharycmontoya force-pushed the otel-http-semconv branch 5 times, most recently from 06fde25 to 4788421 Compare October 7, 2026 23:42
Base automatically changed from dougqh/tag-registry-otel to master October 8, 2026 21:33
@zacharycmontoya
zacharycmontoya force-pushed the otel-http-semconv branch 4 times, most recently from 984c8d3 to f5b4d45 Compare October 13, 2026 21:36
@zacharycmontoya zacharycmontoya changed the title feat(otel): emit OpenTelemetry semantic conventions on HTTP spans Emit OpenTelemetry semantic conventions on HTTP spans Oct 13, 2026
@zacharycmontoya
zacharycmontoya force-pushed the otel-http-semconv branch 3 times, most recently from 6906acf to ed69f54 Compare October 15, 2026 19:53
@zacharycmontoya
zacharycmontoya marked this pull request as ready for review October 16, 2026 01:49
@zacharycmontoya
zacharycmontoya requested review from a team as code owners October 16, 2026 01:49
@zacharycmontoya
zacharycmontoya requested review from Mascarpone, ValentinZakharov, bric3, jandro996 and manuel-alvarez-alvarez and removed request for a team October 16, 2026 01:49
@dd-octo-sts

dd-octo-sts Bot commented Oct 16, 2026

Copy link
Copy Markdown
Contributor

Hi! 👋 Thanks for your pull request! 🎉

To help us review it, please make sure to:

  • Add at least one type, and one component or instrumentation label to the pull request

If you need help, please check our contributing guidelines.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-16T02:00:41.345819Z c8d62ca Draft marked ready
🔒 Security Review ✅ Completed 2026-10-16T01:59:52.303067Z c8d62ca Draft marked ready
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

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

ℹ️ 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 on lines +24 to +25
if (OTEL_SEMANTICS) {
OtelHttpSemantics.setMethodResourceName(span, method);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Limit method-only naming to HTTP spans

When DD_TRACE_OTEL_SEMANTICS_ENABLED=true, this shared helper is also called by ElasticsearchRestClientDecorator.onRequest and OpensearchRestClientDecorator.onRequest, even though those decorators create ELASTICSEARCH and OPENSEARCH database spans. Consequently, resources such as GET /index/_search collapse to just GET, destroying endpoint-level grouping for every Elasticsearch/OpenSearch request. Keep the OTel naming behavior in the HTTP decorators or exempt these non-HTTP callers.

Useful? React with 👍 / 👎.

Comment thread tag-conventions.yaml
Comment on lines +176 to +177
- { dd-name: peer.hostname, type: string, required: recommended }
- { dd-name: peer.port, type: int }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve OTel peer names for database client spans

With OTel semantics enabled, OTLP serialization projects known tags through this registry, and db.client also includes the outbound_peer mixin. Removing these mappings therefore makes existing database client spans that emit peer.hostname and peer.port serialize those legacy keys instead of server.address and server.port; only the HTTP decorators were migrated to emit the new canonical tags. Preserve the mappings for non-HTTP outbound spans or migrate all affected client decorators before removing them.

Useful? React with 👍 / 👎.

Comment on lines +120 to +122
if (domainName != null && !domainName.isEmpty()) {
span.setTag(KnownTags.URL_SCHEME_NAME, "https");
span.setTag(KnownTags.SERVER_ADDRESS_NAME, domainName);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Emit the HTTPS port on inferred proxy spans

When OTel semantics are enabled and x-dd-proxy-domain-name is present, inferred AWS API Gateway and HTTP API spans record url.scheme=https and server.address but never record server.port. The inferred URL is explicitly HTTPS, so omitting the known default port makes these spans inconsistent with the ordinary HTTP paths and causes queries or aggregations on server.port=443 to miss inferred proxy traffic. Set server.port to 443 alongside the address.

Useful? React with 👍 / 👎.

Comment on lines +128 to +129
String queryTag = otelSemanticsEnabled ? KnownTags.URL_QUERY_NAME : DDTags.HTTP_QUERY;
Object query = unsafeTags.getObject(queryTag);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Continue obfuscating Lambda query tags

When OTel semantics are enabled, selecting only url.query bypasses LambdaAppSecHandler.applyHttpTags, which still writes the raw query to DDTags.HTTP_QUERY and explicitly relies on QueryObfuscator to scrub and reappend it. For HTTP-triggered Lambda requests with AppSec active and server query tagging enabled, sensitive values such as token=secret therefore remain in the emitted http.query.string tag. Process both query tags or migrate the Lambda tag construction to the OTel representation.

Useful? React with 👍 / 👎.

} else if (path.charAt(0) != '/') {
path = "/" + path;
}
span.setTag(KnownTags.URL_PATH_NAME, path);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve API Security URL fallback for OTel server spans

For OTel server spans without an http.route, this replacement leaves only url.path. GatewayBridge.maybeSampleForApiSecurity explicitly reads http.route and then http.url to infer an endpoint, so it receives neither value and cannot perform URL-based API Security sampling or endpoint discovery for route-less requests such as unmatched and low-level server traffic. Teach that consumer to fall back to url.path or retain a compatible URL value.

Useful? React with 👍 / 👎.

@datadog-prod-us1-5 datadog-prod-us1-5 Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bits Code Review: FAIL

With OpenTelemetry semantics enabled, percent-encoded sensitive parameter names can bypass redaction and expose secrets in exported URLs. Forwarded-header extraction, non-HTTP destination attributes, and configured client resource names also have confirmed correctness gaps.

Open Bits AI session

🤖 Bits Code Review · Commit c8d62ca

span.setTag(KnownTags.HTTP_URL_OTEL_NAME, withoutQuery(redactedUrl(url)));
// QueryObfuscator scrubs this before rebuilding url.full and removes it from client spans.
final String query = url.getRawQuery();
if (query != null) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Redact encoded sensitive query parameter names

With OpenTelemetry semantics enabled, a URL such as https://example.com/?t%6fken=secret passes the raw encoded parameter name to QueryObfuscator. Its default pattern misses that name, so url.full exports the secret. The previous shared client path used getQuery(), which decoded the name to token for redaction. Normalize encoded names for matching while preserving valid URL encoding during reconstruction.

React: 👍 useful · 👎 wrong · 😕 unclear · 👀 right, but not acting on it

What each reaction means
  • 👍 Useful: correct, and you're fixing it or will.
  • 👎 Wrong: the finding is incorrect, for example the bug can't happen or the code was misread.
  • 😕 Unclear: you can't tell what it means or why it matters.
  • 👀 Right, not acting: correct, but intended, out of scope for this PR, a known risk, or too minor.

🤖 Bits Code Review · Open Bits AI session

if (OTEL_SEMANTICS) {
String scheme =
OtelHttpSemantics.forwardedValue(
extracted == null ? null : extracted.getXForwardedProto());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Enable forwarded-header extraction for OpenTelemetry semantics

With AppSec and AI Guard disabled and the default DD_TRACE_CLIENT_IP_ENABLED=false, ContextInterpreter still discards forwarded headers. Behind a reverse proxy, these new attributes consequently report the backend scheme, host, and port, while client.address falls back to the proxy IP. Enable collection during context extraction for OpenTelemetry semantics too; the added decorator tests bypass extraction by constructing HttpHeaders directly.

React: 👍 useful · 👎 wrong · 😕 unclear · 👀 right, but not acting on it

What each reaction means
  • 👍 Useful: correct, and you're fixing it or will.
  • 👎 Wrong: the finding is incorrect, for example the bug can't happen or the code was misread.
  • 😕 Unclear: you can't tell what it means or why it matters.
  • 👀 Right, not acting: correct, but intended, out of scope for this PR, a known risk, or too minor.

🤖 Bits Code Review · Open Bits AI session

Comment thread tag-conventions.yaml
tags:
- { dd-name: peer.hostname, type: string, required: recommended, otel-name: server.address }
- { dd-name: peer.port, type: int, otel-name: server.port }
- { dd-name: peer.hostname, type: string, required: recommended }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Preserve destination attributes on non-HTTP client spans

Removing these shared mappings also affects non-HTTP clients. LettuceClientDecorator still writes peer.port, and DatabaseClientDecorator writes peer.hostname; neither emits the new canonical destination tags. With OpenTelemetry semantics enabled, their OTLP output therefore loses server.address and server.port. Migrate those producers or preserve their conversion through registry/exporter handling; simply restoring the aliases conflicts with the newly registered canonical names.

React: 👍 useful · 👎 wrong · 😕 unclear · 👀 right, but not acting on it

What each reaction means
  • 👍 Useful: correct, and you're fixing it or will.
  • 👎 Wrong: the finding is incorrect, for example the bug can't happen or the code was misread.
  • 😕 Unclear: you can't tell what it means or why it matters.
  • 👀 Right, not acting: correct, but intended, out of scope for this PR, a known risk, or too minor.

🤖 Bits Code Review · Open Bits AI session

OtelHttpSemantics.setClientUrl(span, url);
// url.full may trigger URLAsResourceNameRule; restore the OTel span name afterwards.
if (shouldSetResourceName()) {
OtelHttpSemantics.setMethodResourceName(span, method);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Prevent path mappings from overriding method-only client names

When a request matches DD_TRACE_HTTP_CLIENT_PATH_RESOURCE_NAME_MAPPING, setting url.full invokes TagInterceptor and installs a path-based resource name at priority 4. This restoration uses priority 1, so DDSpanContext rejects it and retains a name such as GET /orders/{id} instead of GET. Prevent legacy URL resource naming in OpenTelemetry mode across the relevant URL-setting paths.

React: 👍 useful · 👎 wrong · 😕 unclear · 👀 right, but not acting on it

What each reaction means
  • 👍 Useful: correct, and you're fixing it or will.
  • 👎 Wrong: the finding is incorrect, for example the bug can't happen or the code was misread.
  • 😕 Unclear: you can't tell what it means or why it matters.
  • 👀 Right, not acting: correct, but intended, out of scope for this PR, a known risk, or too minor.

🤖 Bits Code Review · Open Bits AI session

// - peer.ipv6
final boolean shouldTagIps =
config.isClientIpEnabled() || traceClientIpResolverEnabled && APPSEC_ACTIVE;
OTEL_SEMANTICS

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

OTEL_SEMANTICS || here turns on client-IP collection whenever OTel semantics are enabled. That covers http.client_ip, network.client.ip, the raw X-Forwarded-For / Forwarded header tags, and the new client.address fallback below. On master these are deliberately opt-in (DD_TRACE_CLIENT_IP_ENABLED or AppSec). Collecting client IPs by default, especially when they can be geolocated, violates privacy rules in some jurisdictions, so we can't enable it through a semantics flag.

The OTel HTTP server conventions mark client.address and network.peer.address as Recommended, not Required, so leaving them out still conforms. Opt-in also needs no OTel-specific code: with DD_TRACE_CLIENT_IP_ENABLED=true, the registry already exports http.client_ip as client.address and network.client.ip as network.peer.address.

Could you drop OTEL_SEMANTICS || here, and change HttpDecoratorsOtelSemanticsForkedTest (L104–105) to assert the IP attributes are absent with only the semantics flag, plus a second case asserting them present with DD_TRACE_CLIENT_IP_ENABLED=true? It's also worth checking that system-tests#7919 doesn't expect them without the opt-in.

Written by Claude, reviewed by @dougqh

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

There's a perf side to this too. With shouldTagIps forced on, every server request in OTel mode runs ClientIpAddressResolver.resolve (header parsing plus InetAddress handling) and the private-IP checks. It also adds up to five http.forwarded* tags plus http.client_ip / network.client.ip, which makes every exported span bigger. Dropping OTEL_SEMANTICS || takes care of this as well.

Written by Claude, reviewed by @dougqh

Comment thread tag-conventions.yaml
- { dd-name: http.status_code, type: int, required: conditional, otel-name: http.response.status_code }
- { dd-name: network.protocol.version, type: string, required: recommended } # passes through: dd-name already is the OTel name
- { dd-name: http.url, type: string, required: required, otel-name: url.full } # shared by http.server and http.client => one otel-name. url.full is the client-correct rename; server's spec mapping (url.path + url.scheme + url.query) is a one-to-many split reserved for the derivation layer (needs span.kind). TODO(otel): server split.
- { dd-name: server.address, type: string, required: conditional } # OTel-only; set only while OTel semantics are enabled

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Declaring server.address / server.port as dd-names on the http parent, and removing the otel-names from http.hostname, peer.hostname and outbound peer.port, causes a few problems:

  • OTLP regressions. Every non-HTTP outbound span that uses outbound_peer (db clients, Lettuce, …) stops exporting server.address / server.port, as the bots noted. So do HTTP spans for anyone using OTEL_TRACES_EXPORTER=otlp without OTel semantics, because the decorators only write server.address while the flag is on.
  • The direction-scoped export already exists. The OtlpTraceJsonCollectorTest rows commented out with "restore when an OTel name can be qualified by a direction" are the master test for exactly that: http.hostname → server.address inbound, and peer.hostname → server.address / peer.port → server.port outbound, from Scope OpenTelemetry tag names by span direction in the tag registry (otlp - tag registry - phase 2) #12713.
  • Two stored forms for one concept. In OTel mode, client spans have no peer.hostname and server spans have no http.hostname, so in-process readers keyed on the Datadog tags (stats peer tags, sampling rules, peer service) silently miss them. Resolve direction-dependent tag names by the span's kind (otlp - tag registry - phase 3) #12731 goes the other way: server.address is a direction-scoped alias that resolves to the single Datadog tag.

Suggestion: keep writing peer.hostname / peer.port (adding the 80/443 default in OTel mode) and http.hostname, and let the registry rename them on export. The only genuinely new tag is inbound server.port, which can be declared on http.server alone without colliding with outbound peer.port → server.port, since uniqueness is checked per direction. That also removes the conflict.

Written by Claude, reviewed by @dougqh

Comment thread tag-conventions.yaml
# Inbound only: the client's socket address. Server spans also carry that address in peer.ipv4 /
# peer.ipv6, so do not map those inbound too, or the attribute is exported twice.
- { dd-name: network.client.ip, type: string, required: recommended, otel-name: network.peer.address }
- { dd-name: network.client.ip, type: string, required: recommended, otel-name: network.peer.address, span-kind-neutral: true } # Temporary direction-free alias until name resolution is span-kind-aware

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

#12713 scoped this name to inbound spans on purpose. With span-kind-neutral: true, network.peer.address enters the direction-free lookup, so writing network.peer.address on an outbound span (OTel API or DD API) gets stored as network.client.ip. That's the wrong concept: on a client span it's the server's address.

As far as I can tell, this is only needed for two test reads: span.getTag("network.peer.address") in the forked decorator test, and the span with no kind in collectTagsAttributeKeys. Could those read by the Datadog name and give the span a kind, so this can stay inbound-only? The existing @TableTest already covers the inbound export.

Written by Claude, reviewed by @dougqh

@dougqh

dougqh commented Oct 16, 2026

Copy link
Copy Markdown
Contributor

More general concern: this adds seven copies of static final boolean OTEL_SEMANTICS and a few dozen branches across the decorators, Axway, InferredProxySpan, QueryObfuscator and HttpEndpointPostProcessor. Every other integration that writes these tags directly will need the same branches.

Most of it could be one projection step at export, matching .NET's model where the writer doesn't know the output convention:

  • Renames: the registry already handles method, URL, client address, and the direction-scoped server address/port.
  • _OTHER + method_original, error.type, default ports, the server url.scheme/url.path/url.query split: all derivable from the Datadog tags (http.method, http.status_code, scheme, http.url + http.query.string).
  • Seam: a TagsPostProcessor installed only in OTel mode, next to QueryObfuscator, so the flag is checked once when the processor chain is built, not per request. It also moves the work off the request thread.

What really needs write-site changes is small: the config defaults, inbound server.port, forwarded values if we capture them outside the IP gate, and resource naming. OTLP name is the resource name, so that one needs its own decision. Happy to pair on the projection if it helps.

Written by Claude, reviewed by @dougqh

return -1;
}
try {
final int port = Integer.parseInt(value);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

parsePort runs on X-Forwarded-Port and the forwarded Host port, which clients control. A malformed value throws and catches a NumberFormatException on every such request; the exception and its stack-trace capture are the expensive part. Ports are at most 5 digits, so a digit loop is simple and allocation-free:

int port = 0;
int len = value.length();
if (len == 0 || len > 5) return -1;
for (int i = 0; i < len; i++) {
  char c = value.charAt(i);
  if (c < '0' || c > '9') return -1;
  port = port * 10 + (c - '0');
}
return port > 0 && port <= 65535 ? port : -1;

(Same pattern as #12791 for the Lambda forwarded-port path.)

Written by Claude, reviewed by @dougqh

}

public static void setClientUrl(final AgentSpan span, final URI url) {
span.setTag(KnownTags.HTTP_URL_OTEL_NAME, withoutQuery(redactedUrl(url)));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

On master the client URL is stored as URIUtils.lazyValidURL(...), which builds nothing until serialization. Here url.full is built eagerly on the application thread: withoutQuery / redactedUrl copy the URL whenever there's a query, a fragment or userinfo. QueryObfuscator then rebuilds it in post-processing with the query re-appended. So a client URL with a query is now copied twice on every outbound call, where before it was built once, lazily.

Could this store a lazy CharSequence that does the redact and strip at serialization, following the lazyValidURL precedent? It also fits the "project at export" direction from my earlier comment: if the projection happens at export, the copying moves off the request thread entirely.

Written by Claude, reviewed by @dougqh

@dougqh dougqh left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It looks like this is adding some conditional code that can be avoided by taking advantage of tag-conventions.yaml a bit more.

In particular, there are cases where just specifying a dd-name and otel-name on the same tag should be sufficient now. The tag registry will even handle disambiguating automatically based on inbound vs outbound spans. Admittedly, I'm still refining some of the mechanics (see #12731).

I'd also like to avoid working around tag interceptors (e.g. resetting the resource name after url.full triggers URLAsResourceNameRule), and/or move some of the translation into the OTLP serializer instead of placing it in instrumentations; there's more detail in my earlier comment. We can take those case by case.

I do have longer-term ideas for making all of that better, but they are longer term. I appreciate that you've already given me some time to bring our efforts together, so we won't hold this on every capability I want to add to the core.

}

if (valid && config.isHttpServerTagQueryString()) {
if (valid && (OTEL_SEMANTICS || config.isHttpServerTagQueryString())) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Similar to the client-IP gate: OTEL_SEMANTICS || here overrides a user's DD_HTTP_SERVER_TAG_QUERY_STRING=false opt-out. OtelHttpSemantics.setClientUrl does the same on the client side, ignoring isHttpClientTagQueryString(). Query strings often carry tokens or PII.

The OTel conventions do mark url.query as Conditionally Required when a query is present. Even so, a user's explicit opt-out should win over conformance, and the conventions themselves say sensitive content in url.query should be scrubbed. Could the semantics flag respect these opt-outs rather than override them?

Written by Claude, reviewed by @dougqh

@dougqh

dougqh commented Oct 16, 2026

Copy link
Copy Markdown
Contributor

Following up on my earlier comment with a concrete shape for the URL piece, using TagExtractor / AgentSpan.setTagsFrom from #11817 (now on master).

There are two axes, and both can be decided statically:

  • Direction (server vs client): decided by which decorator is calling.
  • Style (Datadog vs OTel): decided once at class init and held in a static final.
// HttpServerDecorator
private static final TagExtractor<HttpUrl> URL_TAGS =
    Config.get().isTraceOtelSemanticsEnabled() ? OtelServerUrl.INSTANCE : DatadogServerUrl.INSTANCE;
...
span.setTagsFrom(url, URL_TAGS);

HttpUrl here stands for a single URL record; the lazy URL behind URIUtils.lazyValidURL is a natural starting point. That gives four small extractor classes, and every call site sees a single constant type.

What this gets:

  • The URL if (OTEL_SEMANTICS) branches in the server/client/URI/URL-connection decorators, Axway and InferredProxySpan collapse into one setTagsFrom call each. Each style's logic lives in one class.
  • The OTel URL logic can be unit-tested directly as extractors, rather than through the forked decorator tests.
  • Another style or mode later means adding an extractor, not another flag threaded through every call site.

Two suggestions so it plays well with the rest of the tracer:

  • Have OtelServerUrl still store http.url and add only the OTel-only extras (the url.scheme / url.path split). In-process readers (QueryObfuscator, HttpEndpointPostProcessor, AppSec) keep working, and the registry renames whatever has a direct OTel equivalent.
  • Resolve forwarded proto/host/port once into the URL record (effective scheme/host/port), so the extractor doesn't need extra inputs or the client-IP gate.

This also leaves room for the export-time projection: if the extractors depend only on the URL record and the span, moving them to serialization later is mechanical.

Written by Claude, reviewed by @dougqh

@bric3
bric3 added this pull request to stack #12903 October 16, 2026 07:34

This branch has not been deployed

No deployments
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.

2 participants