Repository navigation
Emit OpenTelemetry semantic conventions on HTTP spans #12765
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
f41e6a8
3a36999
a35b10b
22f8e54
45944f6
c8d62ca
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,6 +14,7 @@ | |
| import datadog.context.propagation.Propagators; | ||
| import datadog.trace.api.Config; | ||
| import datadog.trace.api.DDTags; | ||
| import datadog.trace.api.KnownTags; | ||
| import datadog.trace.api.datastreams.DataStreamsTransactionExtractor; | ||
| import datadog.trace.api.datastreams.DataStreamsTransactionTracker; | ||
| import datadog.trace.api.function.TriConsumer; | ||
|
|
@@ -34,6 +35,7 @@ | |
| import datadog.trace.bootstrap.instrumentation.api.ClientIpAddressData; | ||
| import datadog.trace.bootstrap.instrumentation.api.ErrorPriorities; | ||
| import datadog.trace.bootstrap.instrumentation.api.InternalSpanTypes; | ||
| import datadog.trace.bootstrap.instrumentation.api.OtelHttpSemantics; | ||
| import datadog.trace.bootstrap.instrumentation.api.ResourceNamePriorities; | ||
| import datadog.trace.bootstrap.instrumentation.api.TagContext; | ||
| import datadog.trace.bootstrap.instrumentation.api.Tags; | ||
|
|
@@ -68,13 +70,15 @@ public abstract class HttpServerDecorator<REQUEST, CONNECTION, RESPONSE, REQUEST | |
| public static final String DD_IGNORE_COMMIT_ATTRIBUTE = "datadog.commit.ignore"; | ||
|
|
||
| private static final UTF8BytesString DEFAULT_RESOURCE_NAME = UTF8BytesString.create("/"); | ||
| private static final boolean OTEL_SEMANTICS = Config.get().isTraceOtelSemanticsEnabled(); | ||
| protected static final UTF8BytesString NOT_FOUND_RESOURCE_NAME = UTF8BytesString.create("404"); | ||
| protected static final boolean SHOULD_SET_404_RESOURCE_NAME = | ||
| Config.get().isRuleEnabled("URLAsResourceNameRule") | ||
| !OTEL_SEMANTICS | ||
| && Config.get().isRuleEnabled("URLAsResourceNameRule") | ||
| && Config.get().isRuleEnabled("Status404Rule") | ||
| && Config.get().isRuleEnabled("Status404Decorator"); | ||
| private static final boolean SHOULD_SET_URL_RESOURCE_NAME = | ||
| Config.get().isRuleEnabled("URLAsResourceNameRule"); | ||
| !OTEL_SEMANTICS && Config.get().isRuleEnabled("URLAsResourceNameRule"); | ||
|
|
||
| private static final BitSet SERVER_ERROR_STATUSES = Config.get().getHttpServerErrorStatuses(); | ||
| private static final String DEFAULT_INSTRUMENTATION_NAME = "http-server"; | ||
|
|
@@ -287,7 +291,9 @@ protected void doOnRequest( | |
| // - peer.ipv4 | ||
| // - peer.ipv6 | ||
| final boolean shouldTagIps = | ||
| config.isClientIpEnabled() || traceClientIpResolverEnabled && APPSEC_ACTIVE; | ||
| OTEL_SEMANTICS | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The OTel HTTP server conventions mark Could you drop Written by Claude, reviewed by @dougqh
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's a perf side to this too. With Written by Claude, reviewed by @dougqh
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ack, will remove |
||
| || config.isClientIpEnabled() | ||
| || traceClientIpResolverEnabled && APPSEC_ACTIVE; | ||
| // Whether to stash IP data for later tagging or not. | ||
| // AI Guard requires client IP tags on the local root span when an ai_guard span is created. | ||
| // Resolve the IPs eagerly but do not tag the span yet; stash them on the request context so | ||
|
|
@@ -329,7 +335,12 @@ protected void doOnRequest( | |
|
|
||
| if (request != null) { | ||
| String method = method(request); | ||
| span.setTag(Tags.HTTP_METHOD, method); | ||
| if (OTEL_SEMANTICS) { | ||
| OtelHttpSemantics.setRequestMethod(span, method); | ||
| OtelHttpSemantics.setMethodResourceName(span, method); | ||
| } else { | ||
| span.setTag(Tags.HTTP_METHOD, method); | ||
| } | ||
|
|
||
| // Copy of HttpClientDecorator url handling | ||
| try { | ||
|
|
@@ -339,23 +350,59 @@ protected void doOnRequest( | |
| boolean encoded = supportsRaw && config.isHttpServerRawResource(); | ||
| boolean valid = url.isValid(); | ||
| String path = encoded ? url.rawPath() : url.path(); | ||
| if (valid) { | ||
| span.setTag( | ||
| Tags.HTTP_URL, URIUtils.lazyValidURL(url.scheme(), url.host(), url.port(), path)); | ||
| } else if (supportsRaw) { | ||
| span.setTag(Tags.HTTP_URL, URIUtils.lazyInvalidUrl(url.raw())); | ||
| } | ||
| if (extracted != null && extracted.getXForwardedHost() != null) { | ||
| span.setTag(Tags.HTTP_HOSTNAME, extracted.getXForwardedHost()); | ||
| } else if (url.host() != null) { | ||
| span.setTag(Tags.HTTP_HOSTNAME, url.host()); | ||
| if (OTEL_SEMANTICS) { | ||
| String scheme = | ||
| OtelHttpSemantics.forwardedValue( | ||
| extracted == null ? null : extracted.getXForwardedProto()); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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
🤖 Bits Code Review · Open Bits AI session |
||
| if (scheme == null) { | ||
| scheme = url.scheme(); | ||
| } | ||
| if (valid) { | ||
| if (scheme != null) { | ||
| span.setTag(KnownTags.URL_SCHEME_NAME, scheme); | ||
| } | ||
| // Ensure that path components have a leading '/' | ||
| if (path == null || path.isEmpty()) { | ||
| path = "/"; | ||
| } else if (path.charAt(0) != '/') { | ||
| path = "/" + path; | ||
| } | ||
| span.setTag(KnownTags.URL_PATH_NAME, path); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For OTel server spans without an Useful? React with 👍 / 👎. |
||
| } else if (supportsRaw) { | ||
| span.setTag(KnownTags.URL_PATH_NAME, url.raw()); | ||
| } | ||
| OtelHttpSemantics.setServerAddressAndPort( | ||
| span, | ||
| extracted == null ? null : extracted.getXForwardedHost(), | ||
| extracted == null ? null : extracted.getXForwardedPort(), | ||
| scheme, | ||
| url.host(), | ||
| url.port()); | ||
| } else { | ||
| if (valid) { | ||
| span.setTag( | ||
| Tags.HTTP_URL, URIUtils.lazyValidURL(url.scheme(), url.host(), url.port(), path)); | ||
| } else if (supportsRaw) { | ||
| span.setTag(Tags.HTTP_URL, URIUtils.lazyInvalidUrl(url.raw())); | ||
| } | ||
| if (extracted != null && extracted.getXForwardedHost() != null) { | ||
| span.setTag(Tags.HTTP_HOSTNAME, extracted.getXForwardedHost()); | ||
| } else if (url.host() != null) { | ||
| span.setTag(Tags.HTTP_HOSTNAME, url.host()); | ||
| } | ||
| } | ||
|
|
||
| if (valid && config.isHttpServerTagQueryString()) { | ||
| if (valid && (OTEL_SEMANTICS || config.isHttpServerTagQueryString())) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Similar to the client-IP gate: The OTel conventions do mark Written by Claude, reviewed by @dougqh |
||
| String query = | ||
| supportsRaw && config.isHttpServerRawQueryString() ? url.rawQuery() : url.query(); | ||
| span.setTag(DDTags.HTTP_QUERY, query); | ||
| span.setTag(DDTags.HTTP_FRAGMENT, url.fragment()); | ||
| if (OTEL_SEMANTICS) { | ||
| if (query != null && !query.isEmpty()) { | ||
| span.setTag(KnownTags.URL_QUERY_NAME, query); | ||
| } | ||
| } else { | ||
| span.setTag(DDTags.HTTP_QUERY, query); | ||
| span.setTag(DDTags.HTTP_FRAGMENT, url.fragment()); | ||
| } | ||
| } | ||
| Flow<Void> flow = callIGCallbackURI(span, url, method); | ||
| if (flow.getAction() instanceof RequestBlockingAction) { | ||
|
|
@@ -422,6 +469,9 @@ protected void doOnRequest( | |
| } | ||
| if (shouldTagIps) { | ||
| span.setTag(Tags.NETWORK_CLIENT_IP, peerIp); | ||
| if (OTEL_SEMANTICS && span.getTag(KnownTags.HTTP_CLIENT_IP_OTEL_NAME) == null) { | ||
| span.setTag(KnownTags.HTTP_CLIENT_IP_OTEL_NAME, peerIp); | ||
| } | ||
| } | ||
| } | ||
| if (shouldStashIps && (peerIp != null || inferredAddressStr != null)) { | ||
|
|
@@ -484,6 +534,9 @@ protected void doOnResponseStatus(final AgentSpan span, final int status) { | |
| if (!BlockingException.class.getName().equals(span.getTag("error.type"))) { | ||
| span.setError(SERVER_ERROR_STATUSES.get(status), ErrorPriorities.HTTP_SERVER_DECORATOR); | ||
| } | ||
| if (OTEL_SEMANTICS && SERVER_ERROR_STATUSES.get(status)) { | ||
| OtelHttpSemantics.setErrorType(span, status); | ||
| } | ||
| } | ||
|
|
||
| if (SHOULD_SET_404_RESOURCE_NAME && status == 404) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,7 @@ | |
| import datadog.trace.api.Config; | ||
| import datadog.trace.api.normalize.HttpResourceNames; | ||
| import datadog.trace.bootstrap.instrumentation.api.AgentSpan; | ||
| import datadog.trace.bootstrap.instrumentation.api.OtelHttpSemantics; | ||
| import datadog.trace.bootstrap.instrumentation.api.ResourceNamePriorities; | ||
| import datadog.trace.bootstrap.instrumentation.api.Tags; | ||
| import datadog.trace.bootstrap.instrumentation.api.URIUtils; | ||
|
|
@@ -12,18 +13,27 @@ public class HttpResourceDecorator { | |
| public static final HttpResourceDecorator HTTP_RESOURCE_DECORATOR = new HttpResourceDecorator(); | ||
|
|
||
| private static final UTF8BytesString DEFAULT_RESOURCE_NAME = UTF8BytesString.create("/"); | ||
| private static final boolean OTEL_SEMANTICS = Config.get().isTraceOtelSemanticsEnabled(); | ||
|
|
||
| private final boolean shouldSetUrlResourceName = | ||
| Config.get().isRuleEnabled("URLAsResourceNameRule"); | ||
|
|
||
| private HttpResourceDecorator() {} | ||
|
|
||
| public final void withClientPath(AgentSpan span, CharSequence method, CharSequence path) { | ||
| HttpResourceNames.setForClient(span, method, path, false); | ||
| if (OTEL_SEMANTICS) { | ||
| OtelHttpSemantics.setMethodResourceName(span, method); | ||
|
Comment on lines
+24
to
+25
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When Useful? React with 👍 / 👎. |
||
| } else { | ||
| HttpResourceNames.setForClient(span, method, path, false); | ||
| } | ||
| } | ||
|
|
||
| public final void withServerPath( | ||
| AgentSpan span, CharSequence method, CharSequence path, boolean encoded) { | ||
| if (OTEL_SEMANTICS) { | ||
| OtelHttpSemantics.setMethodResourceName(span, method); | ||
| return; | ||
| } | ||
| if (!shouldSetUrlResourceName) { | ||
| span.setResourceName(DEFAULT_RESOURCE_NAME); | ||
| return; | ||
|
|
@@ -44,7 +54,11 @@ public final void withRoute( | |
| routeTag = URIUtils.decode(route.toString()); | ||
| } | ||
| span.setTag(Tags.HTTP_ROUTE, routeTag); | ||
| if (Config.get().isHttpServerRouteBasedNaming()) { | ||
| if (OTEL_SEMANTICS) { | ||
| final CharSequence resourceName = | ||
| HttpResourceNames.join(OtelHttpSemantics.spanNameMethod(method), routeTag); | ||
| span.setResourceName(resourceName, ResourceNamePriorities.HTTP_FRAMEWORK_ROUTE); | ||
| } else if (Config.get().isHttpServerRouteBasedNaming()) { | ||
| final CharSequence resourceName = HttpResourceNames.join(method, route); | ||
| span.setResourceName(resourceName, ResourceNamePriorities.HTTP_FRAMEWORK_ROUTE); | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
🤖 Bits Code Review · Open Bits AI session