Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
import datadog.trace.bootstrap.instrumentation.api.AgentTracer;
import datadog.trace.bootstrap.instrumentation.api.InternalSpanTypes;
import datadog.trace.bootstrap.instrumentation.api.ResourceNamePriorities;
import datadog.trace.bootstrap.instrumentation.api.Tags;
import datadog.trace.bootstrap.instrumentation.api.URIUtils;
import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString;
Expand Down Expand Up @@ -102,8 +103,13 @@ public AgentSpan onRequest(final AgentSpan span, final REQUEST request) {
request,
DSM_TRANSACTION_SOURCE_READER);

final boolean otelSemantics = Config.get().isTraceOtelSemanticsEnabled();
String method = method(request);
span.setTag(Tags.HTTP_METHOD, method);
if (otelSemantics) {
OtelHttpSemantics.setRequestMethod(span, method);
} else {
span.setTag(Tags.HTTP_METHOD, method);
}

if (CLIENT_TAG_HEADERS) {
for (Map.Entry<String, String> headerTag :
Expand All @@ -120,15 +126,40 @@ public AgentSpan onRequest(final AgentSpan span, final REQUEST request) {
final URI url = url(request);
if (url != null) {
onURI(span, url);
span.setTag(
Tags.HTTP_URL,
URIUtils.lazyValidURL(url.getScheme(), url.getHost(), url.getPort(), url.getPath()));
if (Config.get().isHttpClientTagQueryString()) {
span.setTag(DDTags.HTTP_QUERY, url.getQuery());
span.setTag(DDTags.HTTP_FRAGMENT, url.getFragment());
}
if (shouldSetResourceName()) {
HTTP_RESOURCE_DECORATOR.withClientPath(span, method, url.getPath());
if (otelSemantics) {
// OTel client: the absolute URL goes in url.full only. Clients don't use the decomposed
// url.path/url.query, and url.scheme is opt-in (already contained in url.full), so we
// don't emit it. Target is server.address/port; span is named by method only.
String urlFull = OtelHttpSemantics.redactedUrl(url);
if (!Config.get().isHttpClientTagQueryString()) {
// honor the query-string opt-out (privacy/PII) just like the Datadog http.url path
urlFull = OtelHttpSemantics.withoutQueryAndFragment(urlFull);
}
span.setTag(Tags.URL_FULL, urlFull);

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 Suppress legacy peer tags in OTel client mode

With DD_TRACE_OTEL_SEMANTICS_ENABLED=true, every client request with a host still runs the unconditional onURI(span, url) immediately above this branch, and UriBasedClientDecorator.onURI sets legacy peer.hostname/peer.port before this code adds server.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 👍 / 👎.

if (url.getHost() != null) {
span.setTag(Tags.SERVER_ADDRESS, url.getHost());
}
int serverPort = OtelHttpSemantics.serverPort(url);
if (serverPort > 0) {
span.setTag(Tags.SERVER_PORT, serverPort);
}
if (shouldSetResourceName()) {
span.setResourceName(
OtelHttpSemantics.spanNameMethod(method),
ResourceNamePriorities.HTTP_FRAMEWORK_ROUTE);
}
} else {
span.setTag(
Tags.HTTP_URL,
URIUtils.lazyValidURL(
url.getScheme(), url.getHost(), url.getPort(), url.getPath()));
if (Config.get().isHttpClientTagQueryString()) {
span.setTag(DDTags.HTTP_QUERY, url.getQuery());
span.setTag(DDTags.HTTP_FRAGMENT, url.getFragment());
}
if (shouldSetResourceName()) {
HTTP_RESOURCE_DECORATOR.withClientPath(span, method, url.getPath());
}
}
// SSRF exploit prevention check
onHttpClientRequest(span, url.toString());
Expand All @@ -149,11 +180,18 @@ public AgentSpan onRequest(final AgentSpan span, final REQUEST request) {
public AgentSpan onResponse(final AgentSpan span, final RESPONSE response) {
if (response != null) {
final int status = status(response);
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);
if (CLIENT_ERROR_STATUSES.get(status)) {
// OTel treats client 4xx and 5xx as errors; the Datadog default only marks the configured
// client error range (4xx). Keep error flag and error.type consistent under OTel.
if (CLIENT_ERROR_STATUSES.get(status) || (otelSemantics && status >= 400)) {
span.setError(true);
}
if (otelSemantics && status >= 400) {
OtelHttpSemantics.setErrorType(span, status);
}
}

if (CLIENT_TAG_HEADERS) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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 {
Expand All @@ -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

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 Pair forwarded host with forwarded port

When a proxied request supplies X-Forwarded-Host (and possibly X-Forwarded-Port) and the local request URL has a different port, this OTel branch takes server.address from the forwarded host but server.port only from url.port(). The extracted forwarded port is already available on this context, so proxied requests can be reported as server.address=public.example with the internal listener port (or no port) instead of the original client-facing port.

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);
Expand Down Expand Up @@ -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) {
Expand All @@ -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);
}
}

Expand All @@ -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)) {
Expand Down Expand Up @@ -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
Expand All @@ -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;
Expand Down
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;
}
}
Loading
Loading