-
Notifications
You must be signed in to change notification settings - Fork 340
Add DD_TRACE_OTEL_SEMANTICS_ENABLED for OTel HTTP semantic conventions #11652
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
base: master
Are you sure you want to change the base?
Changes from all commits
0734789
99d852d
d92c476
ef45f15
d2d97de
fc601e0
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 |
|---|---|---|
|
|
@@ -243,6 +243,7 @@ public AgentSpan onRequest( | |
| final REQUEST request, | ||
| final Context parentContext) { | ||
| Config config = Config.get(); | ||
| final boolean otelSemantics = config.isTraceOtelSemanticsEnabled(); | ||
|
|
||
| if (APPSEC_ACTIVE) { | ||
| RequestContext requestContext = span.getRequestContext(); | ||
|
|
@@ -308,13 +309,17 @@ public AgentSpan onRequest( | |
| } | ||
| String userAgent = extracted.getUserAgent(); | ||
| if (userAgent != null) { | ||
| span.setTag(Tags.HTTP_USER_AGENT, userAgent); | ||
| span.setTag(otelSemantics ? Tags.USER_AGENT_ORIGINAL : Tags.HTTP_USER_AGENT, userAgent); | ||
| } | ||
| } | ||
|
|
||
| if (request != null) { | ||
| String method = method(request); | ||
| span.setTag(Tags.HTTP_METHOD, method); | ||
| if (otelSemantics) { | ||
| OtelHttpSemantics.setRequestMethod(span, method); | ||
| } else { | ||
| span.setTag(Tags.HTTP_METHOD, method); | ||
| } | ||
|
|
||
| // Copy of HttpClientDecorator url handling | ||
| try { | ||
|
|
@@ -324,30 +329,71 @@ public AgentSpan onRequest( | |
| 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 (otelSemantics) { | ||
| // OTel server: url.path/url.scheme + server.address/port instead of the DD http.url | ||
| // tag. | ||
| if (valid) { | ||
| if (url.scheme() != null) { | ||
| span.setTag(Tags.URL_SCHEME, url.scheme()); | ||
| } | ||
| span.setTag(Tags.URL_PATH, path); | ||
| } else if (supportsRaw) { | ||
| span.setTag(Tags.URL_PATH, url.raw()); | ||
| } | ||
| String serverAddress = | ||
| extracted != null && extracted.getXForwardedHost() != null | ||
| ? extracted.getXForwardedHost() | ||
| : url.host(); | ||
| if (serverAddress != null) { | ||
| span.setTag(Tags.SERVER_ADDRESS, serverAddress); | ||
| } | ||
| if (url.port() > 0) { | ||
| span.setTag(Tags.SERVER_PORT, url.port()); | ||
|
Comment on lines
+350
to
+351
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 a proxied request supplies Useful? React with 👍 / 👎. |
||
| } | ||
| } 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()); | ||
| } | ||
| } | ||
|
|
||
| // url.query stays gated on the existing query-string toggle even under OTel semantics: | ||
| // that toggle is a privacy/PII control, so a user who disabled query capture should not | ||
| // start leaking query strings just because they switched conventions. | ||
| if (valid && config.isHttpServerTagQueryString()) { | ||
| String query = | ||
| supportsRaw && config.isHttpServerRawQueryString() ? url.rawQuery() : url.query(); | ||
| span.setTag(DDTags.HTTP_QUERY, query); | ||
| span.setTag(DDTags.HTTP_FRAGMENT, url.fragment()); | ||
| if (otelSemantics) { | ||
| // OTel: url.query is omitted when empty | ||
| if (query != null && !query.isEmpty()) { | ||
| span.setTag(Tags.URL_QUERY, 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) { | ||
| span.setRequestBlockingAction((RequestBlockingAction) flow.getAction()); | ||
| } | ||
| if (valid && SHOULD_SET_URL_RESOURCE_NAME) { | ||
| HTTP_RESOURCE_DECORATOR.withServerPath(span, method, path, encoded); | ||
| if (otelSemantics) { | ||
| // OTel span name is "{method}" (HTTP for unknown methods); route-based naming appends | ||
| // the route when known. The spec forbids using the URL path as the target. | ||
| span.setResourceName( | ||
| OtelHttpSemantics.spanNameMethod(method), | ||
| ResourceNamePriorities.HTTP_PATH_NORMALIZER); | ||
| } else { | ||
| HTTP_RESOURCE_DECORATOR.withServerPath(span, method, path, encoded); | ||
| } | ||
| } | ||
| } else if (SHOULD_SET_URL_RESOURCE_NAME) { | ||
| span.setResourceName(DEFAULT_RESOURCE_NAME); | ||
|
|
@@ -383,7 +429,9 @@ public AgentSpan onRequest( | |
| if (inferredAddress != null) { | ||
| inferredAddressStr = inferredAddress.getHostAddress(); | ||
| if (shouldTagIps) { | ||
| span.setTag(Tags.HTTP_CLIENT_IP, inferredAddressStr); | ||
| // OTel maps the resolved client IP (http.client_ip) to client.address | ||
| span.setTag( | ||
| otelSemantics ? Tags.CLIENT_ADDRESS : Tags.HTTP_CLIENT_IP, inferredAddressStr); | ||
| } | ||
| } | ||
| } else if (shouldTagIps && span.getLocalRootSpan() != span) { | ||
|
|
@@ -393,9 +441,10 @@ public AgentSpan onRequest( | |
| // likely already happened on the top span, so we don't need to do the resolution | ||
| // again. Instead, copy from the top span, should it exist | ||
| AgentSpan localRootSpan = span.getLocalRootSpan(); | ||
| Object clientIp = localRootSpan.getTag(Tags.HTTP_CLIENT_IP); | ||
| String clientIpTag = otelSemantics ? Tags.CLIENT_ADDRESS : Tags.HTTP_CLIENT_IP; | ||
| Object clientIp = localRootSpan.getTag(clientIpTag); | ||
| if (clientIp != null) { | ||
| span.setTag(Tags.HTTP_CLIENT_IP, clientIp); | ||
| span.setTag(clientIpTag, clientIp); | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -406,7 +455,8 @@ public AgentSpan onRequest( | |
| span.setTag(Tags.PEER_HOST_IPV4, peerIp); | ||
| } | ||
| if (shouldTagIps) { | ||
| span.setTag(Tags.NETWORK_CLIENT_IP, peerIp); | ||
| // OTel maps the socket peer IP (network.client.ip) to network.peer.address | ||
| span.setTag(otelSemantics ? Tags.NETWORK_PEER_ADDRESS : Tags.NETWORK_CLIENT_IP, peerIp); | ||
| } | ||
| } | ||
| if (shouldStashIps && (peerIp != null || inferredAddressStr != null)) { | ||
|
|
@@ -450,7 +500,9 @@ protected BlockResponseFunction createBlockResponseFunction( | |
| } | ||
|
|
||
| public AgentSpan onResponseStatus(final AgentSpan span, final int status) { | ||
| final boolean otelSemantics = Config.get().isTraceOtelSemanticsEnabled(); | ||
| if (status > UNSET_STATUS) { | ||
| // Status stays in the dedicated field; the serializer renames the key under OTel semantics. | ||
| span.setHttpStatusCode(status); | ||
| // explicitly set here because some other decorators might already set an error without | ||
| // looking at the status code | ||
|
|
@@ -460,9 +512,15 @@ public AgentSpan onResponseStatus(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); | ||
| } | ||
| // OTel error.type = status for server error responses (5xx; 4xx isn't an error), unless set. | ||
| if (otelSemantics && SERVER_ERROR_STATUSES.get(status)) { | ||
| OtelHttpSemantics.setErrorType(span, status); | ||
| } | ||
| } | ||
|
|
||
| if (SHOULD_SET_404_RESOURCE_NAME && status == 404) { | ||
| // Under OTel semantics the span name must not use the path/404 as the target, so leave the | ||
| // "{method}" name in place rather than overriding it with the Datadog "404" resource name. | ||
| if (!otelSemantics && SHOULD_SET_404_RESOURCE_NAME && status == 404) { | ||
| span.setResourceName(NOT_FOUND_RESOURCE_NAME, ResourceNamePriorities.HTTP_404); | ||
| } | ||
| return span; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,114 @@ | ||
| package datadog.trace.bootstrap.instrumentation.decorator; | ||
|
|
||
| import datadog.trace.api.DDTags; | ||
| import datadog.trace.bootstrap.instrumentation.api.AgentSpan; | ||
| import datadog.trace.bootstrap.instrumentation.api.Tags; | ||
| import java.net.URI; | ||
| import java.util.Arrays; | ||
| import java.util.Collections; | ||
| import java.util.HashSet; | ||
| import java.util.Set; | ||
|
|
||
| /** | ||
| * Helpers for emitting OpenTelemetry HTTP semantic-convention attributes, shared by the HTTP server | ||
| * and client decorators. See https://opentelemetry.io/docs/specs/semconv/http/http-spans/ | ||
| */ | ||
| final class OtelHttpSemantics { | ||
| private OtelHttpSemantics() {} | ||
|
|
||
| static final String OTHER_METHOD = "_OTHER"; | ||
|
|
||
| // "Known" HTTP methods per the spec: RFC 9110 + PATCH (RFC 5789) + QUERY (httpbis draft). | ||
| // Method names are case-sensitive and must match exactly. | ||
| private static final Set<String> KNOWN_METHODS = | ||
| Collections.unmodifiableSet( | ||
| new HashSet<>( | ||
| Arrays.asList( | ||
| "CONNECT", "DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT", "QUERY", | ||
| "TRACE"))); | ||
|
|
||
| /** | ||
| * Sets {@code http.request.method}, normalizing methods the instrumentation doesn't know to | ||
| * {@code _OTHER} and recording the original value in {@code http.request.method_original}. | ||
| */ | ||
| static void setRequestMethod(final AgentSpan span, final String method) { | ||
| if (method != null && !KNOWN_METHODS.contains(method)) { | ||
| span.setTag(Tags.HTTP_REQUEST_METHOD, OTHER_METHOD); | ||
| span.setTag(Tags.HTTP_REQUEST_METHOD_ORIGINAL, method); | ||
| } else { | ||
| span.setTag(Tags.HTTP_REQUEST_METHOD, method); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Returns the method component to use in the span name. Per the spec, when the request method is | ||
| * unknown ({@code http.request.method} is {@code _OTHER}), the span name uses the literal {@code | ||
| * HTTP} rather than the raw verb. | ||
| */ | ||
| static String spanNameMethod(final String method) { | ||
| return method != null && KNOWN_METHODS.contains(method) ? method : "HTTP"; | ||
| } | ||
|
|
||
| /** | ||
| * Returns the value for {@code url.full} with any embedded credentials redacted: the spec | ||
| * mandates that {@code url.full} MUST NOT contain credentials (e.g. {@code | ||
| * https://user:pass@host} becomes {@code https://REDACTED:REDACTED@host}). | ||
| */ | ||
| static String redactedUrl(final URI url) { | ||
| final String full = url.toString(); | ||
| final String userInfo = url.getRawUserInfo(); | ||
| if (userInfo == null || userInfo.isEmpty()) { | ||
| return full; | ||
| } | ||
| // Redact only the components that are actually present (user vs user:password) so we don't | ||
| // imply a password that wasn't there. | ||
| final String redacted = userInfo.indexOf(':') >= 0 ? "REDACTED:REDACTED" : "REDACTED"; | ||
| return full.replace(userInfo + "@", redacted + "@"); | ||
| } | ||
|
|
||
| /** | ||
| * Strips the query string and fragment from a URL, used to honor the client query-string opt-out | ||
| * ({@code DD_TRACE_HTTP_CLIENT_TAG_QUERY_STRING=false}) for {@code url.full}. | ||
| */ | ||
| static String withoutQueryAndFragment(final String url) { | ||
| int cut = url.length(); | ||
| final int query = url.indexOf('?'); | ||
| if (query >= 0) { | ||
| cut = query; | ||
| } | ||
| final int fragment = url.indexOf('#'); | ||
| if (fragment >= 0 && fragment < cut) { | ||
| cut = fragment; | ||
| } | ||
| return url.substring(0, cut); | ||
| } | ||
|
|
||
| /** | ||
| * Sets {@code error.type} to the HTTP status code (as a string) when the response indicates an | ||
| * error, unless an error type (e.g. from a thrown exception) has already been recorded — the spec | ||
| * prefers the exception type over the status code. | ||
| */ | ||
| static void setErrorType(final AgentSpan span, final int status) { | ||
| if (span.getTag(DDTags.ERROR_TYPE) == null) { | ||
| span.setTag(DDTags.ERROR_TYPE, Integer.toString(status)); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Resolves {@code server.port} for a client span, falling back to the scheme default (80/443) | ||
| * when the URL omits an explicit port, since the spec marks it required for client spans. | ||
| */ | ||
| static int serverPort(final URI url) { | ||
| final int port = url.getPort(); | ||
| if (port > 0) { | ||
| return port; | ||
| } | ||
| if ("https".equals(url.getScheme())) { | ||
| return 443; | ||
| } | ||
| if ("http".equals(url.getScheme())) { | ||
| return 80; | ||
| } | ||
| return -1; | ||
| } | ||
| } |
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.
With
DD_TRACE_OTEL_SEMANTICS_ENABLED=true, every client request with a host still runs the unconditionalonURI(span, url)immediately above this branch, andUriBasedClientDecorator.onURIsets legacypeer.hostname/peer.portbefore this code addsserver.address/server.port. That means OTel client spans are not actually replacing the Datadog peer tags, so consumers see both naming schemes for the same endpoint; make the URI peer tagging OTel-aware or skip it in the OTel path.Useful? React with 👍 / 👎.