diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 09e44b790e1..b226a6e2848 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -161,6 +161,7 @@ /dd-trace-api/src/*/*/datadog/trace/api/TracePropagation* @DataDog/apm-sdk-capabilities /dd-trace-api/src/*/*/datadog/trace/api/sampling @DataDog/apm-sdk-capabilities /internal-api/src/*/*/datadog/trace/bootstrap @DataDog/apm-sdk-capabilities +/internal-api/src/*/*/datadog/trace/bootstrap/instrumentation/api/http/ @DataDog/apm-sdk-capabilities /internal-api/src/*/*/datadog/trace/api/propagation/ @DataDog/apm-sdk-capabilities /internal-api/src/*/*/datadog/trace/api/sampling @DataDog/apm-sdk-capabilities /internal-api/src/*/*/datadog/trace/api/scopemanager/ @DataDog/apm-sdk-capabilities diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/BaseDecorator.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/BaseDecorator.java index 63838a636bb..55a8784aee3 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/BaseDecorator.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/BaseDecorator.java @@ -14,7 +14,6 @@ import datadog.trace.bootstrap.instrumentation.api.AgentSpan; import datadog.trace.bootstrap.instrumentation.api.ErrorPriorities; import datadog.trace.bootstrap.instrumentation.api.SpanPrototype; -import datadog.trace.bootstrap.instrumentation.api.Tags; import java.lang.reflect.Method; import java.net.Inet4Address; import java.net.Inet6Address; @@ -224,12 +223,12 @@ public final void onPeerConnection( } public void setPeerPort(AgentSpan span, String port) { - span.setTag(Tags.PEER_PORT, port); + span.setTag(KnownTags.PEER_PORT_ID, port); } public void setPeerPort(AgentSpan span, int port) { if (port > UNSET_PORT) { - span.setTag(Tags.PEER_PORT, port); + span.setTag(KnownTags.PEER_PORT_ID, port); } } diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/HttpClientDecorator.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/HttpClientDecorator.java index 3fc7004fd56..71d9a89c5f6 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/HttpClientDecorator.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/HttpClientDecorator.java @@ -7,8 +7,9 @@ import datadog.appsec.api.blocking.BlockingException; import datadog.trace.api.Config; -import datadog.trace.api.DDTags; +import datadog.trace.api.DDSpanTypes; import datadog.trace.api.InstrumenterConfig; +import datadog.trace.api.KnownTags; import datadog.trace.api.ProductActivation; import datadog.trace.api.appsec.HttpClientRequest; import datadog.trace.api.datastreams.DataStreamsTransactionExtractor; @@ -23,9 +24,11 @@ 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.Tags; -import datadog.trace.bootstrap.instrumentation.api.URIUtils; +import datadog.trace.bootstrap.instrumentation.api.OtelHttpSemantics; +import datadog.trace.bootstrap.instrumentation.api.TagExtractor; import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; +import datadog.trace.bootstrap.instrumentation.api.http.DatadogClientUrl; +import datadog.trace.bootstrap.instrumentation.api.http.OtelClientUrl; import java.net.URI; import java.net.URISyntaxException; import java.util.BitSet; @@ -42,6 +45,9 @@ public abstract class HttpClientDecorator extends UriBasedCli private static final String DD_CLIENT_LIBRARY_LANGUAGE_HEADER_NAME = "DD-Client-Library-Language"; private static final BitSet CLIENT_ERROR_STATUSES = Config.get().getHttpClientErrorStatuses(); + private static final boolean OTEL_SEMANTICS = Config.get().isTraceOtelSemanticsEnabled(); + private static final TagExtractor URL_TAGS = + OTEL_SEMANTICS ? OtelClientUrl.INSTANCE : DatadogClientUrl.INSTANCE; private static final UTF8BytesString DEFAULT_RESOURCE_NAME = UTF8BytesString.create("/"); @@ -118,7 +124,11 @@ protected void doOnRequest(final AgentSpan span, final REQUEST request) { DSM_TRANSACTION_SOURCE_READER); String method = method(request); - span.setTag(Tags.HTTP_METHOD, method); + if (OTEL_SEMANTICS) { + OtelHttpSemantics.setRequestMethod(span, method); + } else { + span.setTag(KnownTags.HTTP_METHOD_ID, method); + } if (CLIENT_TAG_HEADERS) { for (Map.Entry headerTag : @@ -130,24 +140,23 @@ protected void doOnRequest(final AgentSpan span, final REQUEST request) { } } - // Copy of HttpServerDecorator url handling + boolean resourceNameSet = false; try { 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()); - } + span.setTagsFrom(url, URL_TAGS); + // Explicit path mappings take precedence over the default name in either semantic mode. if (shouldSetResourceName()) { - HTTP_RESOURCE_DECORATOR.withClientPath(span, method, url.getPath()); + String path = url.getPath(); + if (path != null) { + HTTP_RESOURCE_DECORATOR.withClientPath(span, method, path); + resourceNameSet = true; + } } // SSRF exploit prevention check onHttpClientRequest(span, url.toString()); - } else if (shouldSetResourceName()) { + } else if (!OTEL_SEMANTICS && shouldSetResourceName()) { span.setResourceName(DEFAULT_RESOURCE_NAME); } } catch (final BlockingException e) { @@ -155,11 +164,25 @@ protected void doOnRequest(final AgentSpan span, final REQUEST request) { } catch (final Exception e) { log.debug("Error tagging url", e); } finally { + if (!resourceNameSet) { + setOtelResourceNameFallback(span, method); + } ssrfIastCheck(request); } } } + private void setOtelResourceNameFallback(AgentSpan span, String method) { + if (OTEL_SEMANTICS && shouldSetResourceName()) { + CharSequence spanType = span.getSpanType(); + if (spanType != null && DDSpanTypes.HTTP_CLIENT.contentEquals(spanType)) { + HTTP_RESOURCE_DECORATOR.withClientPath(span, method, null); + } else { + OtelHttpSemantics.setMethodResourceName(span, method); + } + } + } + public final void onResponse(final AgentSpan span, final RESPONSE response) { try { doOnResponse(span, response); @@ -177,6 +200,9 @@ protected void doOnResponse(final AgentSpan span, final RESPONSE response) { span.setHttpStatusCode(status); if (CLIENT_ERROR_STATUSES.get(status)) { span.setError(true); + if (OTEL_SEMANTICS) { + OtelHttpSemantics.setErrorType(span, status); + } } } diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/HttpServerDecorator.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/HttpServerDecorator.java index a962c759f92..60ebcf8a1f9 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/HttpServerDecorator.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/HttpServerDecorator.java @@ -13,7 +13,7 @@ import datadog.context.Context; 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,12 +34,16 @@ 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.TagExtractor; import datadog.trace.bootstrap.instrumentation.api.Tags; import datadog.trace.bootstrap.instrumentation.api.URIDataAdapter; -import datadog.trace.bootstrap.instrumentation.api.URIUtils; import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; +import datadog.trace.bootstrap.instrumentation.api.http.DatadogServerUrl; +import datadog.trace.bootstrap.instrumentation.api.http.HttpServerUrl; +import datadog.trace.bootstrap.instrumentation.api.http.OtelServerUrl; import datadog.trace.bootstrap.instrumentation.decorator.http.ClientIpAddressResolver; import java.net.InetAddress; import java.util.BitSet; @@ -68,13 +72,17 @@ public abstract class HttpServerDecorator URL_TAGS = + OTEL_SEMANTICS ? OtelServerUrl.INSTANCE : DatadogServerUrl.INSTANCE; 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"; @@ -323,15 +331,19 @@ protected void doOnRequest( } String userAgent = extracted.getUserAgent(); if (userAgent != null) { - span.setTag(Tags.HTTP_USER_AGENT, userAgent); + span.setTag(KnownTags.HTTP_USERAGENT_ID, userAgent); } } 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(KnownTags.HTTP_METHOD_ID, method); + } - // Copy of HttpClientDecorator url handling try { final URIDataAdapter url = url(request); if (url != null) { @@ -339,24 +351,16 @@ 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 (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()); - } + span.setTagsFrom( + HttpServerUrl.fromRequest( + url, + path, + extracted, + // Only OTel origin resolution uses Host to resolve ports after a scheme change. + OTEL_SEMANTICS && extracted != null && extracted.getXForwardedProto() != null + ? getRequestHeader(request, "Host") + : null), + URL_TAGS); Flow flow = callIGCallbackURI(span, url, method); if (flow.getAction() instanceof RequestBlockingAction) { span.setRequestBlockingAction((RequestBlockingAction) flow.getAction()); @@ -380,6 +384,7 @@ protected void doOnRequest( } String inferredAddressStr = null; + boolean clientIpTagged = false; if (shouldResolveIp && extracted != null) { InetAddress inferredAddress = ClientIpAddressResolver.resolve(extracted, span); // the peer address should be used if: @@ -398,7 +403,8 @@ protected void doOnRequest( if (inferredAddress != null) { inferredAddressStr = inferredAddress.getHostAddress(); if (shouldTagIps) { - span.setTag(Tags.HTTP_CLIENT_IP, inferredAddressStr); + span.setTag(KnownTags.HTTP_CLIENT_IP_ID, inferredAddressStr); + clientIpTagged = true; } } } else if (shouldTagIps && span.getLocalRootSpan() != span) { @@ -410,18 +416,24 @@ protected void doOnRequest( AgentSpan localRootSpan = span.getLocalRootSpan(); Object clientIp = localRootSpan.getTag(Tags.HTTP_CLIENT_IP); if (clientIp != null) { - span.setTag(Tags.HTTP_CLIENT_IP, clientIp); + span.setTag(KnownTags.HTTP_CLIENT_IP_ID, clientIp); + clientIpTagged = true; } } if (peerIp != null) { if (peerIp.indexOf(':') > 0) { - span.setTag(Tags.PEER_HOST_IPV6, peerIp); + span.setTag(KnownTags.PEER_IPV6_ID, peerIp); } else { - span.setTag(Tags.PEER_HOST_IPV4, peerIp); + span.setTag(KnownTags.PEER_IPV4_ID, peerIp); } if (shouldTagIps) { - span.setTag(Tags.NETWORK_CLIENT_IP, peerIp); + span.setTag(KnownTags.NETWORK_CLIENT_IP_ID, peerIp); + if (OTEL_SEMANTICS + && !clientIpTagged + && span.getTag(KnownTags.HTTP_CLIENT_IP_OTEL_NAME) == null) { + span.setTag(KnownTags.HTTP_CLIENT_IP_ID, peerIp); + } } } if (shouldStashIps && (peerIp != null || inferredAddressStr != null)) { @@ -484,6 +496,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) { diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/UriBasedClientDecorator.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/UriBasedClientDecorator.java index 0ab130eab19..1f04456c0d6 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/UriBasedClientDecorator.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/UriBasedClientDecorator.java @@ -8,19 +8,23 @@ import datadog.trace.api.Config; import datadog.trace.api.datastreams.DataStreamsContext; import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.bootstrap.instrumentation.api.OtelHttpSemantics; import datadog.trace.bootstrap.instrumentation.api.Tags; import java.net.URI; import javax.annotation.Nonnull; public abstract class UriBasedClientDecorator extends ClientDecorator { + private static final boolean OTEL_SEMANTICS = Config.get().isTraceOtelSemanticsEnabled(); + public void onURI(@Nonnull final AgentSpan span, @Nonnull final URI uri) { String host = uri.getHost(); - int port = uri.getPort(); if (null != host && !host.isEmpty()) { span.setTag(Tags.PEER_HOSTNAME, host); if (Config.get().isHttpClientSplitByDomain() && host.charAt(0) >= 'A') { span.setServiceName(host, component()); } + // Port tagging must follow domain splitting so split-by-tags keeps its precedence. + int port = OTEL_SEMANTICS ? OtelHttpSemantics.serverPort(uri) : uri.getPort(); if (port > 0) { setPeerPort(span, port); } diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/UrlConnectionDecorator.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/UrlConnectionDecorator.java index a3c8be6dd5b..e606481c3e4 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/UrlConnectionDecorator.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/UrlConnectionDecorator.java @@ -1,12 +1,15 @@ package datadog.trace.bootstrap.instrumentation.decorator; +import datadog.trace.api.Config; +import datadog.trace.api.KnownTags; import datadog.trace.api.cache.DDCache; import datadog.trace.api.cache.DDCaches; import datadog.trace.api.naming.SpanNaming; import datadog.trace.bootstrap.instrumentation.api.AgentSpan; import datadog.trace.bootstrap.instrumentation.api.InternalSpanTypes; -import datadog.trace.bootstrap.instrumentation.api.Tags; +import datadog.trace.bootstrap.instrumentation.api.TagExtractor; import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; +import datadog.trace.bootstrap.instrumentation.api.http.OtelClientUrl; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; @@ -16,6 +19,8 @@ import org.slf4j.LoggerFactory; public class UrlConnectionDecorator extends UriBasedClientDecorator { + private static final TagExtractor URL_TAGS = + Config.get().isTraceOtelSemanticsEnabled() ? OtelClientUrl.INSTANCE : new FullUrl(); private static final DDCache CACHE = DDCaches.newFixedSizeCache(16); private static final Function ADDER = @@ -51,7 +56,7 @@ protected String service() { @Override public void onURI(@Nonnull AgentSpan span, @Nonnull URI uri) { super.onURI(span, uri); - span.setTag(Tags.HTTP_URL, uri.toString()); + span.setTagsFrom(uri, URL_TAGS); } public void onURL(@Nonnull final AgentSpan span, @Nonnull final URL url) { @@ -65,4 +70,12 @@ public void onURL(@Nonnull final AgentSpan span, @Nonnull final URL url) { public CharSequence operationName(@Nonnull final String protocol) { return CACHE.computeIfAbsent(protocol, ADDER); } + + private static final class FullUrl implements TagExtractor { + @Override + public void extract(URI uri, AgentSpan span) { + // URLConnection historically records the original URL, including its query and fragment. + span.setTag(KnownTags.HTTP_URL_ID, uri.toString()); + } + } } diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/http/HttpResourceDecorator.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/http/HttpResourceDecorator.java index 72ca66a520f..efc215b114a 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/http/HttpResourceDecorator.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/http/HttpResourceDecorator.java @@ -1,10 +1,13 @@ package datadog.trace.bootstrap.instrumentation.decorator.http; import datadog.trace.api.Config; +import datadog.trace.api.DDSpanTypes; +import datadog.trace.api.KnownTags; +import datadog.trace.api.Pair; 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; import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; @@ -12,18 +15,40 @@ 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 final boolean hasClientPathMappings = + !Config.get().getHttpClientPathResourceNameMapping().isEmpty(); private HttpResourceDecorator() {} public final void withClientPath(AgentSpan span, CharSequence method, CharSequence path) { - HttpResourceNames.setForClient(span, method, path, false); + if (OTEL_SEMANTICS + && span.getSpanType() != null + && DDSpanTypes.HTTP_CLIENT.contentEquals(span.getSpanType())) { + if (hasClientPathMappings && path != null) { + Pair resourceName = + HttpResourceNames.computeForClient( + OtelHttpSemantics.spanNameMethod(method), path, false); + if (resourceName.getRight() == ResourceNamePriorities.HTTP_CLIENT_CONFIG_PATTERN_MATCH) { + span.setResourceName(resourceName.getLeft(), resourceName.getRight()); + return; + } + } + OtelHttpSemantics.setMethodResourceName(span, method); + } 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; @@ -43,8 +68,12 @@ public final void withRoute( if (encoded) { routeTag = URIUtils.decode(route.toString()); } - span.setTag(Tags.HTTP_ROUTE, routeTag); - if (Config.get().isHttpServerRouteBasedNaming()) { + span.setTag(KnownTags.HTTP_ROUTE_ID, routeTag); + 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); } diff --git a/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/decorator/HttpClientPathMappingOtelSemanticsForkedTest.java b/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/decorator/HttpClientPathMappingOtelSemanticsForkedTest.java new file mode 100644 index 00000000000..222eea79879 --- /dev/null +++ b/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/decorator/HttpClientPathMappingOtelSemanticsForkedTest.java @@ -0,0 +1,91 @@ +package datadog.trace.bootstrap.instrumentation.decorator; + +import static datadog.trace.api.config.GeneralConfig.TRACE_OTEL_SEMANTICS_ENABLED; +import static datadog.trace.api.config.TracerConfig.TRACE_HTTP_CLIENT_PATH_RESOURCE_NAME_MAPPING; +import static datadog.trace.bootstrap.instrumentation.api.ResourceNamePriorities.HTTP_CLIENT_CONFIG_PATTERN_MATCH; +import static datadog.trace.bootstrap.instrumentation.api.ResourceNamePriorities.MANUAL_INSTRUMENTATION; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import datadog.trace.api.KnownTags; +import datadog.trace.bootstrap.instrumentation.decorator.HttpDecoratorsOtelSemanticsForkedTest.ClientRequest; +import datadog.trace.bootstrap.instrumentation.decorator.HttpDecoratorsOtelSemanticsForkedTest.TestClientDecorator; +import datadog.trace.common.writer.ListWriter; +import datadog.trace.core.CoreTracer; +import datadog.trace.core.DDSpan; +import datadog.trace.test.junit.utils.config.WithConfig; +import java.net.URI; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.tabletest.junit.TableTest; + +@WithConfig(key = TRACE_OTEL_SEMANTICS_ENABLED, value = "true") +@WithConfig( + key = TRACE_HTTP_CLIENT_PATH_RESOURCE_NAME_MAPPING, + value = "/orders/*:/orders/{id},/raw/*:*") +class HttpClientPathMappingOtelSemanticsForkedTest { + private CoreTracer tracer; + + @BeforeEach + void setUp() { + tracer = CoreTracer.builder().writer(new ListWriter()).build(); + } + + @AfterEach + void tearDown() { + tracer.close(); + } + + @TableTest({ + "Scenario | Method | Path | Expected resource | Expected method", + "Unmapped path | GET | /health/123 | GET | GET ", + "Mapped path | GET | /orders/123 | GET /orders/{id} | GET ", + "Mapped extension method | BREW | /orders/123 | HTTP /orders/{id} | _OTHER ", + "Unmapped extension method | BREW | /health/123 | HTTP | _OTHER ", + "Mapped lowercase method | get | /orders/123 | HTTP /orders/{id} | _OTHER ", + "Mapped missing method | | /orders/123 | HTTP /orders/{id} | ", + "Keep original path | GET | /raw/123 | GET /raw/123 | GET " + }) + void explicitPathMappingsOverrideOtelClientNames( + String method, String path, String expectedResource, String expectedMethod) { + DDSpan span = (DDSpan) tracer.startSpan("test", "http.request"); + TestClientDecorator decorator = new TestClientDecorator(); + + decorator.afterStart(span); + decorator.onRequest(span, new ClientRequest(method, URI.create("https://example.com" + path))); + span.finish(); + + assertEquals(expectedResource, span.getResourceName().toString()); + assertEquals(expectedMethod, span.getTag(KnownTags.HTTP_METHOD_OTEL_NAME)); + } + + @Test + @WithConfig(key = "trace.URLAsResourceNameRule.enabled", value = "false") + void explicitPathMappingsApplyWithoutUrlResourceNameRule() { + DDSpan span = (DDSpan) tracer.startSpan("test", "http.request"); + TestClientDecorator decorator = new TestClientDecorator(); + + decorator.afterStart(span); + decorator.onRequest( + span, new ClientRequest("GET", URI.create("https://example.com/orders/123"))); + span.finish(); + + assertEquals("GET /orders/{id}", span.getResourceName().toString()); + assertEquals(HTTP_CLIENT_CONFIG_PATTERN_MATCH, span.getResourceNamePriority()); + } + + @Test + void explicitPathMappingsPreserveManualResourceNames() { + DDSpan span = (DDSpan) tracer.startSpan("test", "http.request"); + span.setResourceName("custom operation", MANUAL_INSTRUMENTATION); + TestClientDecorator decorator = new TestClientDecorator(); + + decorator.afterStart(span); + decorator.onRequest( + span, new ClientRequest("GET", URI.create("https://example.com/orders/123"))); + span.finish(); + + assertEquals("custom operation", span.getResourceName().toString()); + assertEquals(MANUAL_INSTRUMENTATION, span.getResourceNamePriority()); + } +} diff --git a/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/decorator/HttpDecoratorsDatadogSemanticsForkedTest.java b/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/decorator/HttpDecoratorsDatadogSemanticsForkedTest.java new file mode 100644 index 00000000000..8180770eeda --- /dev/null +++ b/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/decorator/HttpDecoratorsDatadogSemanticsForkedTest.java @@ -0,0 +1,134 @@ +package datadog.trace.bootstrap.instrumentation.decorator; + +import static datadog.trace.api.config.GeneralConfig.TRACE_OTEL_SEMANTICS_ENABLED; +import static datadog.trace.api.config.TraceInstrumentationConfig.HTTP_CLIENT_HOST_SPLIT_BY_DOMAIN; +import static datadog.trace.api.config.TracerConfig.SPLIT_BY_TAGS; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import datadog.trace.api.DDTraceId; +import datadog.trace.bootstrap.instrumentation.api.AgentPropagation; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.bootstrap.instrumentation.api.TagContext; +import datadog.trace.bootstrap.instrumentation.api.Tags; +import datadog.trace.bootstrap.instrumentation.api.URIDataAdapter; +import datadog.trace.bootstrap.instrumentation.api.URIDefaultDataAdapter; +import datadog.trace.bootstrap.instrumentation.decorator.HttpDecoratorsOtelSemanticsForkedTest.TestClientDecorator; +import datadog.trace.common.writer.ListWriter; +import datadog.trace.core.CoreTracer; +import datadog.trace.core.DDSpan; +import datadog.trace.test.junit.utils.config.WithConfig; +import java.net.URI; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +@WithConfig(key = TRACE_OTEL_SEMANTICS_ENABLED, value = "false") +class HttpDecoratorsDatadogSemanticsForkedTest { + private CoreTracer tracer; + + @BeforeEach + void setUp() { + tracer = CoreTracer.builder().writer(new ListWriter()).build(); + } + + @AfterEach + void tearDown() { + tracer.close(); + } + + @Test + @WithConfig(key = HTTP_CLIENT_HOST_SPLIT_BY_DOMAIN, value = "true") + @WithConfig(key = SPLIT_BY_TAGS, value = "peer.port") + void portServiceSplitTakesPrecedenceOverDomainSplit() { + DDSpan span = (DDSpan) tracer.startSpan("test", "http.request"); + + new TestClientDecorator().onURI(span, URI.create("http://example.com:8080/orders")); + span.finish(); + + assertEquals("8080", span.getServiceName()); + } + + @Test + void forwardedProtocolDoesNotTriggerHostHeaderLookup() { + TestServerDecorator decorator = new TestServerDecorator(); + DDSpan span = (DDSpan) tracer.startSpan("test", "http.request"); + TagContext.HttpHeaders headers = new TagContext.HttpHeaders(); + headers.xForwardedProto = "https"; + headers.xForwardedHost = "public.example.com"; + TagContext extracted = new TagContext(null, null, headers, null, 0, null, null, DDTraceId.ZERO); + + decorator.onRequest( + span, + null, + URI.create("http://internal.example.com:8080/orders"), + AgentSpan.fromSpanContext(extracted)); + span.finish(); + + assertEquals(0, decorator.headerLookups); + assertEquals("http://internal.example.com:8080/orders", span.getTag(Tags.HTTP_URL).toString()); + assertEquals("public.example.com", span.getTag(Tags.HTTP_HOSTNAME)); + assertNull(span.getTag("url.scheme")); + } + + private static final class TestServerDecorator + extends HttpServerDecorator { + private int headerLookups; + + @Override + protected String[] instrumentationNames() { + return new String[] {"test-http-server"}; + } + + @Override + protected CharSequence component() { + return "test-http-server"; + } + + @Override + protected AgentPropagation.ContextVisitor getter() { + return null; + } + + @Override + protected AgentPropagation.ContextVisitor responseGetter() { + return null; + } + + @Override + public CharSequence spanName() { + return "http.request"; + } + + @Override + protected String method(URI request) { + return "GET"; + } + + @Override + protected URIDataAdapter url(URI request) { + return new URIDefaultDataAdapter(request); + } + + @Override + protected String getRequestHeader(URI request, String key) { + headerLookups++; + return "Host".equalsIgnoreCase(key) ? "public.example.com" : null; + } + + @Override + protected String peerHostIP(Void connection) { + return null; + } + + @Override + protected int peerPort(Void connection) { + return 0; + } + + @Override + protected int status(Integer response) { + return response; + } + } +} diff --git a/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/decorator/HttpDecoratorsOtelSemanticsForkedTest.java b/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/decorator/HttpDecoratorsOtelSemanticsForkedTest.java new file mode 100644 index 00000000000..6d547bbe816 --- /dev/null +++ b/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/decorator/HttpDecoratorsOtelSemanticsForkedTest.java @@ -0,0 +1,870 @@ +package datadog.trace.bootstrap.instrumentation.decorator; + +import static datadog.context.Context.root; +import static datadog.trace.api.config.GeneralConfig.TRACE_OTEL_SEMANTICS_ENABLED; +import static datadog.trace.api.config.TraceInstrumentationConfig.HTTP_CLIENT_TAG_QUERY_STRING; +import static datadog.trace.api.config.TraceInstrumentationConfig.HTTP_SERVER_RAW_QUERY_STRING; +import static datadog.trace.api.config.TraceInstrumentationConfig.HTTP_SERVER_TAG_QUERY_STRING; +import static datadog.trace.api.config.TracerConfig.CLIENT_IP_ENABLED; +import static datadog.trace.bootstrap.instrumentation.api.ContextVisitors.stringValuesMap; +import static datadog.trace.bootstrap.instrumentation.api.ResourceNamePriorities.HTTP_PATH_NORMALIZER; +import static datadog.trace.bootstrap.instrumentation.api.ResourceNamePriorities.MANUAL_INSTRUMENTATION; +import static datadog.trace.bootstrap.instrumentation.decorator.http.HttpResourceDecorator.HTTP_RESOURCE_DECORATOR; +import static datadog.trace.core.CoreSpan.OWN_PROPAGATION_TAGS; +import static datadog.trace.core.MetadataConsumer.NO_OP; +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.spy; + +import datadog.appsec.api.blocking.BlockingException; +import datadog.json.JsonMapper; +import datadog.json.JsonWriter; +import datadog.trace.api.Config; +import datadog.trace.api.DDTraceId; +import datadog.trace.api.KnownTags; +import datadog.trace.api.gateway.InferredProxySpan; +import datadog.trace.bootstrap.ActiveSubsystems; +import datadog.trace.bootstrap.instrumentation.api.AgentPropagation; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.bootstrap.instrumentation.api.AgentSpanContext; +import datadog.trace.bootstrap.instrumentation.api.AgentTracer; +import datadog.trace.bootstrap.instrumentation.api.TagContext; +import datadog.trace.bootstrap.instrumentation.api.Tags; +import datadog.trace.bootstrap.instrumentation.api.URIDataAdapter; +import datadog.trace.bootstrap.instrumentation.api.URIDefaultDataAdapter; +import datadog.trace.bootstrap.instrumentation.api.UnparseableURIDataAdapter; +import datadog.trace.common.writer.ListWriter; +import datadog.trace.core.CoreTracer; +import datadog.trace.core.DDSpan; +import datadog.trace.core.otlp.trace.OtlpTraceJson; +import datadog.trace.core.propagation.HttpCodec; +import datadog.trace.test.junit.utils.config.WithConfig; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.URI; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.tabletest.junit.TableTest; + +@WithConfig(key = TRACE_OTEL_SEMANTICS_ENABLED, value = "true") +@WithConfig(key = HTTP_CLIENT_TAG_QUERY_STRING, value = "false") +@WithConfig(key = HTTP_SERVER_TAG_QUERY_STRING, value = "false") +class HttpDecoratorsOtelSemanticsForkedTest { + private CoreTracer tracer; + + @BeforeEach + void setUp() { + tracer = CoreTracer.builder().writer(new ListWriter()).build(); + } + + @AfterEach + void tearDown() { + tracer.close(); + } + + @Test + void clientDecoratorCapturesOtelHttpValues() throws Exception { + TestClientDecorator decorator = new TestClientDecorator(); + DDSpan span = (DDSpan) tracer.startSpan("test", "http.request"); + ClientRequest request = + new ClientRequest( + "BREW", new URI("https://user:password@example.com/orders?token=secret#fragment")); + + decorator.afterStart(span); + decorator.onRequest(span, request); + decorator.onResponse(span, 503); + span.processTagsAndBaggage(NO_OP, OWN_PROPAGATION_TAGS); + + assertEquals("HTTP", span.getResourceName().toString()); + assertEquals("_OTHER", span.getTag(KnownTags.HTTP_METHOD_OTEL_NAME)); + assertEquals("BREW", span.getTag(KnownTags.HTTP_REQUEST_METHOD_ORIGINAL_NAME)); + assertEquals( + "https://REDACTED:REDACTED@example.com/orders#fragment", + span.getTag(KnownTags.HTTP_URL_OTEL_NAME).toString()); + assertEquals("example.com", span.getTag(Tags.PEER_HOSTNAME)); + assertEquals(443, span.getTag(Tags.PEER_PORT)); + assertEquals(503, span.getHttpStatusCode()); + assertEquals("503", span.getTag("error.type")); + assertTrue(span.isError()); + } + + @TableTest({ + "Scenario | Method | URL | Span type | Expected resource", + "No URL | GET | | http | GET ", + "Extension method | BREW | | http | HTTP ", + "Missing method | | | http | HTTP ", + "Opaque URL | GET | mailto:test@test.com | http | GET ", + "Other span type | GET | | custom | GET ", + "Missing span type | GET | | | GET ", + "HTTP client path | GET | https://example.com/orders | http | GET ", + "Other span type path | GET | https://example.com/orders | custom | GET /orders ", + "Missing span type path | GET | https://example.com/orders | | GET /orders " + }) + void clientResourceNamePreservesSpanTypeBehavior( + String method, String url, String spanType, String expectedResource) { + TestClientDecorator decorator = new TestClientDecorator(); + DDSpan span = (DDSpan) tracer.startSpan("test", "http.request"); + + decorator.afterStart(span); + span.setSpanType(spanType); + decorator.onRequest(span, new ClientRequest(method, url == null ? null : URI.create(url))); + + assertEquals(expectedResource, span.getResourceName().toString()); + assertEquals(HTTP_PATH_NORMALIZER, span.getResourceNamePriority()); + } + + @TableTest({ + "Scenario | Fail extraction", + "URL extraction fails | true ", + "URL decoration fails | false " + }) + void clientResourceNameSurvivesUrlFailures(boolean failExtraction) { + TestClientDecorator decorator = spy(new TestClientDecorator()); + DDSpan span = (DDSpan) tracer.startSpan("test", "http.request"); + URI uri = URI.create("https://example.com/orders"); + ClientRequest request = new ClientRequest("GET", uri); + RuntimeException failure = new IllegalArgumentException("Invalid URL"); + if (failExtraction) { + doThrow(failure).when(decorator).url(request); + } else { + doThrow(failure).when(decorator).onURI(span, uri); + } + + decorator.afterStart(span); + decorator.onRequest(span, request); + + assertEquals("GET", span.getResourceName().toString()); + assertEquals(HTTP_PATH_NORMALIZER, span.getResourceNamePriority()); + } + + @Test + void clientResourceNameSurvivesBlockingException() { + TestClientDecorator decorator = spy(new TestClientDecorator()); + DDSpan span = (DDSpan) tracer.startSpan("test", "http.request"); + URI uri = URI.create("https://example.com/orders"); + ClientRequest request = new ClientRequest("GET", uri); + BlockingException failure = new BlockingException("Blocked request"); + doThrow(failure).when(decorator).onURI(span, uri); + + decorator.afterStart(span); + + assertSame( + failure, assertThrows(BlockingException.class, () -> decorator.onRequest(span, request))); + assertEquals("GET", span.getResourceName().toString()); + assertEquals(HTTP_PATH_NORMALIZER, span.getResourceNamePriority()); + } + + @Test + void clientResourceNameWithoutUrlPreservesManualName() { + TestClientDecorator decorator = new TestClientDecorator(); + DDSpan span = (DDSpan) tracer.startSpan("test", "http.request"); + span.setResourceName("custom operation", MANUAL_INSTRUMENTATION); + + decorator.afterStart(span); + decorator.onRequest(span, new ClientRequest("GET", null)); + + assertEquals("custom operation", span.getResourceName().toString()); + assertEquals(MANUAL_INSTRUMENTATION, span.getResourceNamePriority()); + } + + @TableTest({ + "Scenario | URL ", + "No URL | ", + "With URL | https://example.com/orders" + }) + @WithConfig(key = "trace.URLAsResourceNameRule.enabled", value = "false") + void clientResourceNamingCanBeDisabled(String url) { + TestClientDecorator decorator = spy(new TestClientDecorator()); + doReturn(false).when(decorator).shouldSetResourceName(); + DDSpan span = (DDSpan) tracer.startSpan("test", "http.request"); + span.setResourceName("custom operation"); + + decorator.afterStart(span); + decorator.onRequest(span, new ClientRequest("GET", url == null ? null : URI.create(url))); + + assertEquals("custom operation", span.getResourceName().toString()); + } + + @Test + void urlConnectionDecoratorRespectsQueryStringOptOut() throws Exception { + DDSpan span = (DDSpan) tracer.startSpan("test", "urlconnection.request"); + + UrlConnectionDecorator.DECORATE.afterStart(span); + UrlConnectionDecorator.DECORATE.onURI( + span, new URI("https://example.com/orders?token=secret#fragment")); + span.processTagsAndBaggage(NO_OP, OWN_PROPAGATION_TAGS); + + assertEquals( + "https://example.com/orders#fragment", + span.getTag(KnownTags.HTTP_URL_OTEL_NAME).toString()); + } + + @Test + @WithConfig(key = HTTP_CLIENT_TAG_QUERY_STRING, value = "true") + void clientDecoratorCapturesAndObfuscatesQueryWhenEnabled() throws Exception { + TestClientDecorator decorator = new TestClientDecorator(); + DDSpan span = (DDSpan) tracer.startSpan("test", "http.request"); + URI uri = new URI("https://user:password@example.com/orders?token=secret#fragment"); + + decorator.afterStart(span); + decorator.onRequest(span, new ClientRequest("GET", uri)); + + assertEquals("token=secret", span.getTag(KnownTags.HTTP_QUERY_STRING_NAME)); + + span.processTagsAndBaggage(NO_OP, OWN_PROPAGATION_TAGS); + + assertEquals( + "https://REDACTED:REDACTED@example.com/orders?#fragment", + span.getTag(KnownTags.HTTP_URL_OTEL_NAME).toString()); + } + + @Test + @WithConfig(key = HTTP_CLIENT_TAG_QUERY_STRING, value = "true") + void urlConnectionDecoratorCapturesAndObfuscatesQueryWhenEnabled() throws Exception { + DDSpan span = (DDSpan) tracer.startSpan("test", "urlconnection.request"); + URI uri = new URI("https://user:password@example.com/orders?token=secret#fragment"); + + UrlConnectionDecorator.DECORATE.afterStart(span); + UrlConnectionDecorator.DECORATE.onURI(span, uri); + + assertEquals("token=secret", span.getTag(KnownTags.HTTP_QUERY_STRING_NAME)); + + span.processTagsAndBaggage(NO_OP, OWN_PROPAGATION_TAGS); + + assertEquals( + "https://REDACTED:REDACTED@example.com/orders?#fragment", + span.getTag(KnownTags.HTTP_URL_OTEL_NAME).toString()); + } + + @Test + void serverDecoratorCapturesOtelHttpValuesAndRouteName() throws Exception { + TestServerDecorator decorator = new TestServerDecorator(); + DDSpan span = (DDSpan) tracer.startSpan("test", "servlet.request"); + ServerRequest request = + new ServerRequest("GET", new URI("https://example.com:8443/users/123?view=full")); + + decorator.afterStart(span); + decorator.onRequest(span, new Connection("192.0.2.10", 32100), request, root()); + HTTP_RESOURCE_DECORATOR.withRoute(span, request.method, "/users/{id}"); + decorator.onResponse(span, 503); + span.processTagsAndBaggage(NO_OP, OWN_PROPAGATION_TAGS); + + assertEquals("GET /users/{id}", span.getResourceName().toString()); + assertEquals("GET", span.getTag(KnownTags.HTTP_METHOD_OTEL_NAME)); + assertEquals("/users/123", span.getTag(KnownTags.URL_PATH_NAME)); + assertEquals("https", span.getTag(KnownTags.URL_SCHEME_NAME)); + assertNull(span.getTag(KnownTags.HTTP_QUERY_STRING_NAME)); + assertEquals("example.com", span.getTag(KnownTags.HTTP_HOSTNAME_NAME)); + assertEquals(8443, span.getTag(KnownTags.SERVER_PORT_NAME)); + assertNull(span.getTag(Tags.HTTP_CLIENT_IP)); + assertNull(span.getTag(Tags.NETWORK_CLIENT_IP)); + assertEquals(503, span.getHttpStatusCode()); + assertEquals("503", span.getTag("error.type")); + assertTrue(span.isError()); + assertFalse(span.getTags().containsKey("http.url")); + } + + @Test + @WithConfig(key = HTTP_SERVER_TAG_QUERY_STRING, value = "true") + void serverDecoratorCapturesAndObfuscatesQueryWhenEnabled() throws Exception { + TestServerDecorator decorator = new TestServerDecorator(); + DDSpan span = (DDSpan) tracer.startSpan("test", "servlet.request"); + ServerRequest request = + new ServerRequest("GET", new URI("https://example.com/orders?token=secret&view=full")); + + decorator.afterStart(span); + decorator.onRequest(span, new Connection("192.0.2.10", 32100), request, root()); + + assertEquals("token=secret&view=full", span.getTag(KnownTags.HTTP_QUERY_STRING_NAME)); + + span.processTagsAndBaggage(NO_OP, OWN_PROPAGATION_TAGS); + + assertEquals("&view=full", span.getTag(KnownTags.HTTP_QUERY_STRING_NAME)); + assertEquals("/orders", span.getTag(KnownTags.URL_PATH_NAME)); + } + + @TableTest({ + "Scenario | Request URI | Expected Path", + "query | /bad%path?token=secret&view=full | /bad%path ", + "encoded marker | /bad%3F%path?token=secret | /bad%3F%path ", + "query and fragment | /bad%path?token=secret&view=full#fragment | /bad%path ", + "fragment only | /bad%path#fragment | /bad%path ", + "fragment question | /bad%path#fragment?token=secret | /bad%path ", + "empty query fragment | /bad%path?#fragment | /bad%path ", + "encoded markers | /bad%23%path?token=secret&view=full%23details#fragment | /bad%23%path ", + "empty query | /bad%path? | /bad%path ", + "no query | /bad%path | /bad%path " + }) + void invalidServerUrlRespectsQueryStringOptOut(String requestUri, String expectedPath) { + TestServerDecorator decorator = new TestServerDecorator(); + DDSpan span = (DDSpan) tracer.startSpan("test", "servlet.request"); + ServerRequest request = + new ServerRequest("GET", new UnparseableURIDataAdapter(requestUri), null); + + decorator.afterStart(span); + decorator.onRequest(span, new Connection("192.0.2.10", 32100), request, root()); + span.processTagsAndBaggage(NO_OP, OWN_PROPAGATION_TAGS); + + assertEquals(expectedPath, span.getTag(KnownTags.URL_PATH_NAME)); + assertNull(span.getTag(KnownTags.HTTP_QUERY_STRING_NAME)); + assertNull(span.getTag(Tags.HTTP_URL)); + } + + @TableTest({ + "Scenario | Request URI | Expected Path | Expected Query | Obfuscated Query ", + "malformed path | /bad%path?token=secret&view=full | /bad%path | token=secret&view=full | &view=full ", + "malformed query | /orders?token=secret&view=%GG | /orders | token=secret&view=%GG | &view=%GG ", + "extra marker | /bad%path?token=secret&view=full?details | /bad%path | token=secret&view=full?details | &view=full?details ", + "query and fragment | /bad%path?token=secret&view=full#fragment | /bad%path | token=secret&view=full | &view=full ", + "fragment only | /bad%path#fragment | /bad%path | | ", + "fragment question | /bad%path#fragment?token=secret | /bad%path | | ", + "empty query fragment | /bad%path?#fragment | /bad%path | | ", + "encoded markers | /bad%23%path?token=secret&view=full%23details#fragment | /bad%23%path | token=secret&view=full%23details | &view=full%23details", + "empty query | /bad%path? | /bad%path | | ", + "no query | /bad%path | /bad%path | | " + }) + @WithConfig(key = HTTP_SERVER_TAG_QUERY_STRING, value = "true") + void invalidServerUrlSeparatesAndObfuscatesQueryWhenEnabled( + String requestUri, String expectedPath, String expectedQuery, String obfuscatedQuery) { + TestServerDecorator decorator = new TestServerDecorator(); + DDSpan span = (DDSpan) tracer.startSpan("test", "servlet.request"); + ServerRequest request = + new ServerRequest("GET", new UnparseableURIDataAdapter(requestUri), null); + + decorator.afterStart(span); + decorator.onRequest(span, new Connection("192.0.2.10", 32100), request, root()); + + assertEquals(expectedPath, span.getTag(KnownTags.URL_PATH_NAME)); + assertEquals(expectedQuery, span.getTag(KnownTags.HTTP_QUERY_STRING_NAME)); + + span.processTagsAndBaggage(NO_OP, OWN_PROPAGATION_TAGS); + + assertEquals(expectedPath, span.getTag(KnownTags.URL_PATH_NAME)); + assertEquals(obfuscatedQuery, span.getTag(KnownTags.HTTP_QUERY_STRING_NAME)); + assertNull(span.getTag(Tags.HTTP_URL)); + } + + @Test + @WithConfig(key = HTTP_SERVER_TAG_QUERY_STRING, value = "true") + @WithConfig(key = HTTP_SERVER_RAW_QUERY_STRING, value = "false") + void invalidServerUrlDecodesQueryWhenConfigured() { + TestServerDecorator decorator = new TestServerDecorator(); + DDSpan span = (DDSpan) tracer.startSpan("test", "servlet.request"); + ServerRequest request = + new ServerRequest( + "GET", + new UnparseableURIDataAdapter( + "/bad%path?t%6fken=secret&view=full%3Fdetails%23section#fragment?token=fragment-secret"), + null); + + decorator.afterStart(span); + decorator.onRequest(span, new Connection("192.0.2.10", 32100), request, root()); + + assertEquals( + "token=secret&view=full?details#section", span.getTag(KnownTags.HTTP_QUERY_STRING_NAME)); + + span.processTagsAndBaggage(NO_OP, OWN_PROPAGATION_TAGS); + + assertEquals("/bad%path", span.getTag(KnownTags.URL_PATH_NAME)); + assertEquals( + "&view=full?details#section", span.getTag(KnownTags.HTTP_QUERY_STRING_NAME)); + } + + @Test + void serverDecoratorOmitsClientIpsAndForwardedHeadersByDefault() throws Exception { + TestServerDecorator decorator = new TestServerDecorator(); + DDSpan span = (DDSpan) tracer.startSpan("test", "servlet.request"); + ServerRequest request = new ServerRequest("GET", new URI("https://example.com/")); + TagContext.HttpHeaders headers = new TagContext.HttpHeaders(); + headers.xForwardedFor = "203.0.113.7"; + headers.forwarded = "for=203.0.113.7"; + TagContext extracted = new TagContext(null, null, headers, null, 0, null, null, DDTraceId.ZERO); + + decorator.onRequest( + span, new Connection("192.0.2.10", 32100), request, AgentSpan.fromSpanContext(extracted)); + + assertNull(span.getTag(Tags.HTTP_CLIENT_IP)); + assertNull(span.getTag(Tags.NETWORK_CLIENT_IP)); + assertNull(span.getTag(Tags.HTTP_FORWARDED_IP)); + assertNull(span.getTag(Tags.HTTP_FORWARDED)); + } + + @TableTest({ + "Scenario | Forwarded Ip | Expected Client Ip", + "peer address | | 192.0.2.10 ", + "forwarded address | 203.0.113.7 | 203.0.113.7 " + }) + @WithConfig(key = CLIENT_IP_ENABLED, value = "true") + void serverDecoratorCapturesClientIpsWithOptIn(String forwardedIp, String expectedClientIp) + throws Exception { + TestServerDecorator decorator = new TestServerDecorator(); + DDSpan span = (DDSpan) tracer.startSpan("test", "servlet.request"); + ServerRequest request = new ServerRequest("GET", new URI("https://example.com/")); + TagContext.HttpHeaders headers = new TagContext.HttpHeaders(); + headers.xForwardedFor = forwardedIp; + headers.forwarded = forwardedIp == null ? null : "for=" + forwardedIp; + TagContext extracted = new TagContext(null, null, headers, null, 0, null, null, DDTraceId.ZERO); + + decorator.onRequest( + span, + new Connection("192.0.2.10", 32100), + request, + forwardedIp == null ? root() : AgentSpan.fromSpanContext(extracted)); + + assertEquals(expectedClientIp, span.getTag(KnownTags.HTTP_CLIENT_IP_OTEL_NAME)); + assertEquals("192.0.2.10", span.getTag(Tags.NETWORK_CLIENT_IP)); + assertEquals(forwardedIp, span.getTag(Tags.HTTP_FORWARDED_IP)); + assertEquals(headers.forwarded, span.getTag(Tags.HTTP_FORWARDED)); + } + + @TableTest({ + "Scenario | Root Client Ip | Expected Client Ip", + "root address | 203.0.113.7 | 203.0.113.7 ", + "peer fallback | | 192.0.2.10 " + }) + @WithConfig(key = CLIENT_IP_ENABLED, value = "true") + void serverDecoratorInheritsClientIpFromLocalRoot(String rootClientIp, String expectedClientIp) { + TestServerDecorator decorator = new TestServerDecorator(); + DDSpan rootSpan = (DDSpan) tracer.startSpan("test", "servlet.request"); + rootSpan.setTag(Tags.HTTP_CLIENT_IP, rootClientIp); + DDSpan span = (DDSpan) tracer.startSpan("test", "servlet.dispatch", rootSpan.spanContext()); + ServerRequest request = new ServerRequest("GET", URI.create("https://example.com/")); + + decorator.onRequest(span, new Connection("192.0.2.10", 32100), request, root()); + + assertEquals(expectedClientIp, span.getTag(Tags.HTTP_CLIENT_IP)); + assertEquals("192.0.2.10", span.getTag(Tags.NETWORK_CLIENT_IP)); + assertEquals(rootClientIp, rootSpan.getTag(Tags.HTTP_CLIENT_IP)); + } + + @Test + @WithConfig(key = CLIENT_IP_ENABLED, value = "true") + void serverDecoratorPreservesExistingClientIpAcrossRepeatedDecoration() { + TestServerDecorator decorator = new TestServerDecorator(); + DDSpan span = (DDSpan) tracer.startSpan("test", "servlet.request"); + span.setTag(Tags.HTTP_CLIENT_IP, "203.0.113.7"); + ServerRequest request = new ServerRequest("GET", URI.create("https://example.com/")); + + decorator.onRequest(span, new Connection("192.0.2.10", 32100), request, root()); + + assertEquals("203.0.113.7", span.getTag(Tags.HTTP_CLIENT_IP)); + + decorator.onRequest(span, new Connection("192.0.2.11", 32101), request, root()); + + assertEquals("203.0.113.7", span.getTag(Tags.HTTP_CLIENT_IP)); + assertEquals("192.0.2.11", span.getTag(Tags.NETWORK_CLIENT_IP)); + } + + @TableTest({ + "Scenario | Request URI | Expected Path", + "relative path | orders | /orders ", + "empty path | '' | / ", + "absolute path | /orders | /orders " + }) + void serverDecoratorNormalizesUrlPath(String requestUri, String expectedPath) { + TestServerDecorator decorator = new TestServerDecorator(); + DDSpan span = (DDSpan) tracer.startSpan("test", "servlet.request"); + ServerRequest request = new ServerRequest("GET", URI.create(requestUri)); + + decorator.onRequest(span, new Connection("192.0.2.10", 32100), request, root()); + + assertEquals(expectedPath, span.getTag(KnownTags.URL_PATH_NAME)); + } + + @Test + void serverDecoratorUsesForwardedOrigin() throws Exception { + TestServerDecorator decorator = new TestServerDecorator(); + DDSpan span = (DDSpan) tracer.startSpan("test", "servlet.request"); + ServerRequest request = + new ServerRequest("GET", new URI("http://internal.example.com:8080/users/123")); + TagContext.HttpHeaders headers = new TagContext.HttpHeaders(); + headers.xForwardedProto = "https, http"; + headers.xForwardedHost = "public.example.com:8443, internal.example.com:8080"; + TagContext extracted = new TagContext(null, null, headers, null, 0, null, null, DDTraceId.ZERO); + + decorator.afterStart(span); + decorator.onRequest( + span, new Connection("192.0.2.10", 32100), request, AgentSpan.fromSpanContext(extracted)); + + assertEquals("https", span.getTag(KnownTags.URL_SCHEME_NAME)); + assertEquals("public.example.com", span.getTag(KnownTags.HTTP_HOSTNAME_NAME)); + assertEquals(8443, span.getTag(KnownTags.SERVER_PORT_NAME)); + } + + @Test + void serverDecoratorUsesForwardedPortWithIpv6Host() throws Exception { + TestServerDecorator decorator = new TestServerDecorator(); + DDSpan span = (DDSpan) tracer.startSpan("test", "servlet.request"); + ServerRequest request = + new ServerRequest("GET", new URI("http://internal.example.com:8080/users/123")); + TagContext.HttpHeaders headers = new TagContext.HttpHeaders(); + headers.xForwardedProto = "https"; + headers.xForwardedHost = "[2001:db8::1]"; + headers.xForwardedPort = "9443"; + TagContext extracted = new TagContext(null, null, headers, null, 0, null, null, DDTraceId.ZERO); + + decorator.afterStart(span); + decorator.onRequest( + span, new Connection("192.0.2.10", 32100), request, AgentSpan.fromSpanContext(extracted)); + + assertEquals("https", span.getTag(KnownTags.URL_SCHEME_NAME)); + assertEquals("2001:db8::1", span.getTag(KnownTags.HTTP_HOSTNAME_NAME)); + assertEquals(9443, span.getTag(KnownTags.SERVER_PORT_NAME)); + } + + @TableTest({ + "Scenario | Request URI | Host header | Expected port", + "Backend default port | http://internal.example.com:80/orders | public.example.com | 443 ", + "Backend custom port | http://internal.example.com:8080/orders | public.example.com | 443 ", + "Missing Host header | http://internal.example.com:8080/orders | | 443 ", + "Explicit public port | http://internal.example.com:8080/orders | public.example.com:9443 | 9443 ", + "Explicit IPv6 port | http://internal.example.com:8080/orders | '[2001:db8::1]:8443' | 8443 ", + "Already resolved | https://public.example.com:9443/orders | public.example.com | 9443 " + }) + void serverDecoratorUsesOriginPortWithOnlyForwardedScheme( + String requestUri, String hostHeader, int expectedPort) { + TestServerDecorator decorator = new TestServerDecorator(); + DDSpan span = (DDSpan) tracer.startSpan("test", "servlet.request"); + ServerRequest request = new ServerRequest("GET", URI.create(requestUri), hostHeader); + TagContext.HttpHeaders headers = new TagContext.HttpHeaders(); + headers.xForwardedProto = "https, http"; + TagContext extracted = new TagContext(null, null, headers, null, 0, null, null, DDTraceId.ZERO); + + decorator.afterStart(span); + decorator.onRequest( + span, new Connection("192.0.2.10", 32100), request, AgentSpan.fromSpanContext(extracted)); + + assertEquals("https", span.getTag(KnownTags.URL_SCHEME_NAME)); + assertEquals(expectedPort, span.getTag(KnownTags.SERVER_PORT_NAME)); + } + + @ParameterizedTest + @ValueSource(strings = {"elasticsearch", "opensearch"}) + void databaseRestClientsKeepEndpointResourceNames(String spanType) { + DDSpan span = (DDSpan) tracer.startSpan("test", "database.query"); + span.setSpanType(spanType); + span.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_CLIENT); + + // Both REST client decorators use this helper to name their database spans. + HTTP_RESOURCE_DECORATOR.withClientPath(span, "GET", "/index/_search"); + span.finish(); + + assertEquals("GET /index/_search", span.getResourceName().toString()); + } + + @TableTest({ + "Scenario | URL | Expected port", + "HTTP default | http://example.com/orders | 80 ", + "HTTPS default | https://example.com/orders | 443 ", + "Explicit port | https://example.com:8443/orders | 8443 " + }) + void clientDestinationPortUsesPeerIdentity(String url, int expectedPort) { + DDSpan span = (DDSpan) tracer.startSpan("test", "http.request"); + TestClientDecorator decorator = new TestClientDecorator(); + + decorator.afterStart(span); + decorator.onRequest(span, new ClientRequest("GET", URI.create(url))); + span.finish(); + + assertEquals(expectedPort, span.getTag(Tags.PEER_PORT)); + } + + @Test + void clientConnectionDoesNotExportDuplicateServerPorts() throws IOException { + DDSpan span = (DDSpan) tracer.startSpan("test", "http.request"); + TestClientDecorator decorator = new TestClientDecorator(); + decorator.afterStart(span); + decorator.onRequest( + span, new ClientRequest("GET", URI.create("https://example.com:8443/orders"))); + // Netty decorates the connection after the request, including when it connects to a proxy. + decorator.onPeerConnection(span, InetSocketAddress.createUnresolved("proxy.example.com", 3128)); + span.finish(); + + List ports = exportedAttributeValues(span, "server.port"); + + assertEquals(singletonList(String.valueOf(span.getTag(Tags.PEER_PORT))), ports); + } + + /** + * The query is decoded before obfuscation so that a percent-encoded parameter name cannot hide a + * secret from QueryObfuscator, which matches literal names. The trade-off is that url.full does + * not round-trips: encoded delimiters in a value come back decoded, so {@code view=a%26b%3Dc} + * reads back as two parameters. This matches the query that non-OpenTelemetry client spans + * already carry in http.url. + */ + @ParameterizedTest + @ValueSource(booleans = {false, true}) + @WithConfig(key = HTTP_CLIENT_TAG_QUERY_STRING, value = "true") + void clientQueriesAreDecodedBeforeObfuscating(boolean urlConnection) { + DDSpan span = (DDSpan) tracer.startSpan("test", "http.request"); + URI uri = + URI.create( + "https://user:password@example.com/orders" + + "?t%6fken=secret&view=a%26b%3Dc&space=hello%20world&plus=%2B#fragment"); + + if (urlConnection) { + UrlConnectionDecorator.DECORATE.afterStart(span); + UrlConnectionDecorator.DECORATE.onURI(span, uri); + } else { + TestClientDecorator decorator = new TestClientDecorator(); + decorator.afterStart(span); + decorator.onRequest(span, new ClientRequest("GET", uri)); + } + span.processTagsAndBaggage(NO_OP, OWN_PROPAGATION_TAGS); + + // The encoded 'token' name is redacted, at the cost of decoding the safe parameters. + assertEquals( + "https://REDACTED:REDACTED@example.com/orders" + + "?&view=a&b=c&space=hello world&plus=+#fragment", + span.getTag(KnownTags.HTTP_URL_OTEL_NAME).toString()); + } + + @TableTest({ + "Scenario | Propagation headers ", + "Datadog headers | [x-datadog-trace-id: '1', x-datadog-parent-id: '2'] ", + "W3C headers | [traceparent: '00-00000000000000000000000000000001-0000000000000002-01']" + }) + @WithConfig(key = "appsec.enabled", value = "false") + @WithConfig(key = "ai_guard.enabled", value = "false") + @WithConfig(key = CLIENT_IP_ENABLED, value = "false") + @WithConfig(key = "trace.propagation.style.extract", value = "datadog,tracecontext") + void serverOriginUsesHeadersFromRealPropagationWithoutIpOptIn( + Map propagationHeaders) { + Map headers = new HashMap<>(propagationHeaders); + headers.put("x-forwarded-proto", "https, http"); + headers.put("x-forwarded-host", "public.example.com:8443, internal.example.com:8080"); + headers.put("x-forwarded-port", "9443, 8080"); + headers.put("x-forwarded-for", "203.0.113.7"); + boolean originalAppSecActive = ActiveSubsystems.APPSEC_ACTIVE; + ActiveSubsystems.APPSEC_ACTIVE = false; + HttpCodec.Extractor extractor = + HttpCodec.createExtractor(Config.get(), tracer::captureTraceConfig); + try { + TagContext extracted = extractor.extract(headers, stringValuesMap()); + assertNotNull(extracted); + assertNull(extracted.getXForwardedFor()); + DDSpan span = (DDSpan) tracer.startSpan("test", "servlet.request", extracted); + TestServerDecorator decorator = new TestServerDecorator(); + + decorator.afterStart(span); + decorator.onRequest( + span, + new Connection("192.0.2.10", 32100), + new ServerRequest("GET", URI.create("http://internal.example.com:8080/orders")), + AgentSpan.fromSpanContext(extracted)); + span.finish(); + + assertEquals("https", span.getTag(KnownTags.URL_SCHEME_NAME)); + assertEquals("public.example.com", span.getTag(KnownTags.HTTP_HOSTNAME_NAME)); + assertEquals(9443, span.getTag(KnownTags.SERVER_PORT_NAME)); + assertNull(span.getTag(KnownTags.HTTP_CLIENT_IP_OTEL_NAME)); + assertNull(span.getTag(Tags.NETWORK_CLIENT_IP)); + assertNull(span.getTag(Tags.HTTP_FORWARDED_IP)); + } finally { + extractor.cleanup(); + ActiveSubsystems.APPSEC_ACTIVE = originalAppSecActive; + } + } + + @TableTest({ + "Scenario | Proxy system | Domain | Expected port", + "API Gateway HTTPS | aws-apigateway | api.example.com | 443 ", + "HTTP API HTTPS | aws-httpapi | api.example.com | 443 ", + "API Gateway without host | aws-apigateway | | ", + "HTTP API without host | aws-httpapi | | " + }) + void inferredProxyUsesKnownHttpsPort(String proxySystem, String domain, Integer expectedPort) { + Map headers = new HashMap<>(); + headers.put("x-dd-proxy", proxySystem); + headers.put("x-dd-proxy-request-time-ms", Long.toString(System.currentTimeMillis())); + headers.put("x-dd-proxy-httpmethod", "GET"); + headers.put("x-dd-proxy-path", "/orders"); + if (domain != null) { + headers.put("x-dd-proxy-domain-name", domain); + } + InferredProxySpan inferred = InferredProxySpan.fromHeaders(headers); + AgentTracer.TracerAPI originalTracer = AgentTracer.get(); + AgentTracer.forceRegister(tracer); + try { + AgentSpanContext parent = inferred.start(null); + assertNotNull(parent); + DDSpan child = (DDSpan) tracer.startSpan("test", "servlet.request", parent); + AgentSpan proxy = child.getLocalRootSpan(); + child.finish(); + + assertEquals(domain, proxy.getTag(KnownTags.HTTP_HOSTNAME_NAME)); + assertEquals(domain == null ? null : "https", proxy.getTag(KnownTags.URL_SCHEME_NAME)); + assertEquals(expectedPort, proxy.getTag(KnownTags.SERVER_PORT_NAME)); + } finally { + inferred.finish(); + AgentTracer.forceRegister(originalTracer); + } + } + + private static List exportedAttributeValues(DDSpan span, String key) throws IOException { + Map exportedSpan; + try (JsonWriter writer = new JsonWriter()) { + OtlpTraceJson.writeSpan(writer, span, new OtlpTraceJson.MetaWriter(writer), emptyList()); + exportedSpan = JsonMapper.fromJsonToMap(writer.toString()); + } + List values = new ArrayList<>(); + for (Object item : (List) exportedSpan.get("attributes")) { + Map attribute = (Map) item; + if (key.equals(attribute.get("key"))) { + values.add(((Map) attribute.get("value")).values().iterator().next()); + } + } + return values; + } + + static final class ClientRequest { + private final String method; + private final URI uri; + + ClientRequest(String method, URI uri) { + this.method = method; + this.uri = uri; + } + } + + static class TestClientDecorator extends HttpClientDecorator { + @Override + protected String[] instrumentationNames() { + return new String[] {"test-http-client"}; + } + + @Override + protected CharSequence component() { + return "test-http-client"; + } + + @Override + protected String method(ClientRequest request) { + return request.method; + } + + @Override + protected URI url(ClientRequest request) { + return request.uri; + } + + @Override + protected int status(Integer response) { + return response; + } + + @Override + protected String getRequestHeader(ClientRequest request, String headerName) { + return null; + } + + @Override + protected String getResponseHeader(Integer response, String headerName) { + return null; + } + } + + private static final class ServerRequest { + private final String method; + private final URIDataAdapter uri; + private final String hostHeader; + + private ServerRequest(String method, URI uri) { + this(method, uri, null); + } + + private ServerRequest(String method, URI uri, String hostHeader) { + this(method, new URIDefaultDataAdapter(uri), hostHeader); + } + + private ServerRequest(String method, URIDataAdapter uri, String hostHeader) { + this.method = method; + this.uri = uri; + this.hostHeader = hostHeader; + } + } + + private static final class Connection { + private final String host; + private final int port; + + private Connection(String host, int port) { + this.host = host; + this.port = port; + } + } + + private static final class TestServerDecorator + extends HttpServerDecorator { + @Override + protected String[] instrumentationNames() { + return new String[] {"test-http-server"}; + } + + @Override + protected CharSequence component() { + return "test-http-server"; + } + + @Override + protected AgentPropagation.ContextVisitor getter() { + return null; + } + + @Override + protected AgentPropagation.ContextVisitor responseGetter() { + return null; + } + + @Override + public CharSequence spanName() { + return "http.request"; + } + + @Override + protected String method(ServerRequest request) { + return request.method; + } + + @Override + protected URIDataAdapter url(ServerRequest request) { + return request.uri; + } + + @Override + protected String getRequestHeader(ServerRequest request, String key) { + return "Host".equalsIgnoreCase(key) ? request.hostHeader : null; + } + + @Override + protected String peerHostIP(Connection connection) { + return connection.host; + } + + @Override + protected int peerPort(Connection connection) { + return connection.port; + } + + @Override + protected int status(Integer response) { + return response; + } + } +} diff --git a/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/decorator/NameKeyedAgentSpan.java b/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/decorator/NameKeyedAgentSpan.java index 435ef32cdb5..61d7e851080 100644 --- a/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/decorator/NameKeyedAgentSpan.java +++ b/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/decorator/NameKeyedAgentSpan.java @@ -2,6 +2,7 @@ import datadog.trace.api.KnownTagCodec; import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.bootstrap.instrumentation.api.TagExtractor; /** * An {@link AgentSpan} to mock in tests whose id-keyed {@code setTag} overloads are {@code final} @@ -10,6 +11,11 @@ * a tag by name or by {@code KnownTags} id. */ public abstract class NameKeyedAgentSpan implements AgentSpan { + @Override + public final AgentSpan setTagsFrom(T source, TagExtractor extractor) { + return AgentSpan.super.setTagsFrom(source, extractor); + } + @Override public final AgentSpan setTag(long tagId, boolean value) { return setTag(KnownTagCodec.nameOf(tagId), value); diff --git a/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/decorator/UrlConnectionDecoratorUrlTest.java b/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/decorator/UrlConnectionDecoratorUrlTest.java new file mode 100644 index 00000000000..e621e1bdf2a --- /dev/null +++ b/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/decorator/UrlConnectionDecoratorUrlTest.java @@ -0,0 +1,30 @@ +package datadog.trace.bootstrap.instrumentation.decorator; + +import static datadog.trace.api.config.GeneralConfig.TRACE_OTEL_SEMANTICS_ENABLED; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.CALLS_REAL_METHODS; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +import datadog.trace.api.KnownTags; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.test.junit.utils.config.WithConfig; +import java.net.URI; +import org.junit.jupiter.api.Test; + +@WithConfig(key = TRACE_OTEL_SEMANTICS_ENABLED, value = "false") +class UrlConnectionDecoratorUrlTest { + @Test + @WithConfig(key = "http.client.tag.query-string", value = "false") + void datadogUrlConnectionKeepsOriginalUrl() { + AgentSpan span = mock(AgentSpan.class, CALLS_REAL_METHODS); + String url = "http://u:p@host:80/a?q=1#fragment"; + + UrlConnectionDecorator.DECORATE.onURI(span, URI.create(url)); + + verify(span).setTag(KnownTags.HTTP_URL_ID, url); + verify(span, never()).setTag(eq(KnownTags.HTTP_QUERY_STRING_ID), anyString()); + } +} diff --git a/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/sink/HstsMissingHeaderModuleImpl.java b/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/sink/HstsMissingHeaderModuleImpl.java index c96247c8870..53ffd454c82 100644 --- a/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/sink/HstsMissingHeaderModuleImpl.java +++ b/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/sink/HstsMissingHeaderModuleImpl.java @@ -5,6 +5,7 @@ import com.datadog.iast.model.Location; import com.datadog.iast.model.Vulnerability; import com.datadog.iast.model.VulnerabilityType; +import datadog.trace.api.KnownTags; import datadog.trace.api.gateway.IGSpanInfo; import datadog.trace.api.iast.IastContext; import datadog.trace.api.iast.sink.HstsMissingHeaderModule; @@ -38,6 +39,7 @@ public void onRequestEnd(final IastContext ctx, final IGSpanInfo igSpanInfo) { try { Map tags = igSpanInfo.getTags(); String urlString = (String) tags.get("http.url"); + String scheme = (String) tags.get(KnownTags.URL_SCHEME_NAME); Integer httpStatus = (Integer) tags.get("http.status_code"); if (isIgnorableResponseCode(httpStatus)) { return; @@ -45,7 +47,7 @@ public void onRequestEnd(final IastContext ctx, final IGSpanInfo igSpanInfo) { if (!isHtmlResponse(iastRequestContext.getContentType())) { return; } - if (!isHttps(urlString, iastRequestContext.getxForwardedProto())) { + if (!isHttps(urlString, scheme, iastRequestContext.getxForwardedProto())) { return; } final AgentSpan span = (AgentSpan) igSpanInfo; @@ -68,11 +70,15 @@ static boolean isValidMaxAge(@Nullable final String value) { return Integer.parseInt(matcher.group(1)) > 0; } - static boolean isHttps(@Nullable final String urlString, @Nullable final String forwardedFor) { - if (urlString == null) { + static boolean isHttps( + @Nullable final String urlString, + @Nullable final String scheme, + @Nullable final String forwardedFor) { + if (urlString == null && scheme == null) { return false; } - if (urlString.toLowerCase(Locale.ROOT).startsWith("https://")) { + if ("https".equalsIgnoreCase(scheme) + || (urlString != null && urlString.toLowerCase(Locale.ROOT).startsWith("https://"))) { return true; } if (forwardedFor == null) { diff --git a/dd-java-agent/agent-iast/src/test/java/com/datadog/iast/sink/HstsMissingHeaderModuleUrlSchemeTest.java b/dd-java-agent/agent-iast/src/test/java/com/datadog/iast/sink/HstsMissingHeaderModuleUrlSchemeTest.java new file mode 100644 index 00000000000..7d147fb2056 --- /dev/null +++ b/dd-java-agent/agent-iast/src/test/java/com/datadog/iast/sink/HstsMissingHeaderModuleUrlSchemeTest.java @@ -0,0 +1,66 @@ +package com.datadog.iast.sink; + +import static com.datadog.iast.model.VulnerabilityType.HSTS_HEADER_MISSING; +import static com.datadog.iast.overhead.Operations.REPORT_VULNERABILITY; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.datadog.iast.Dependencies; +import com.datadog.iast.IastRequestContext; +import com.datadog.iast.Reporter; +import com.datadog.iast.overhead.OverheadController; +import datadog.trace.api.KnownTags; +import datadog.trace.api.TagMap; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.bootstrap.instrumentation.api.Tags; +import org.junit.jupiter.api.Test; +import org.tabletest.junit.TableTest; + +class HstsMissingHeaderModuleUrlSchemeTest { + + @TableTest({ + "Scenario | url | scheme | forwardedProto | expected", + "Legacy HTTPS | https://localhost/a | | | true ", + "Legacy HTTP | http://localhost/a | | | false ", + "Legacy forwarded HTTPS | http://localhost/a | | https | true ", + "OTel HTTPS | | https | | true ", + "OTel uppercase HTTPS | | HTTPS | | true ", + "OTel HTTP | | http | | false ", + "OTel forwarded HTTPS | | http | https | true ", + "OTel HTTPS forwarded HTTP | | https | http | true ", + "No URL attributes | | | | false ", + "No URL attributes forwarded | | | https | false " + }) + void detectsHttps(String url, String scheme, String forwardedProto, boolean expected) { + assertEquals(expected, HstsMissingHeaderModuleImpl.isHttps(url, scheme, forwardedProto)); + } + + @Test + void reportsMissingHstsUsingUrlSchemeWithoutLegacyUrl() { + Dependencies dependencies = mock(Dependencies.class); + Reporter reporter = mock(Reporter.class); + OverheadController overheadController = mock(OverheadController.class); + when(dependencies.getReporter()).thenReturn(reporter); + when(dependencies.getOverheadController()).thenReturn(overheadController); + + IastRequestContext context = mock(IastRequestContext.class); + when(context.getContentType()).thenReturn("text/html"); + + TagMap tags = TagMap.create(); + tags.put(KnownTags.URL_SCHEME_NAME, "https"); + tags.put(Tags.HTTP_STATUS, 200); + AgentSpan span = mock(AgentSpan.class); + when(span.getTags()).thenReturn(tags); + when(overheadController.consumeQuota(REPORT_VULNERABILITY, span, HSTS_HEADER_MISSING)) + .thenReturn(true); + + new HstsMissingHeaderModuleImpl(dependencies).onRequestEnd(context, span); + + verify(reporter) + .report(eq(span), argThat(vulnerability -> vulnerability.getType() == HSTS_HEADER_MISSING)); + } +} diff --git a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/trace/OtelConventions.java b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/trace/OtelConventions.java index 117c7ab77d8..f849dd47cb1 100644 --- a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/trace/OtelConventions.java +++ b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/trace/OtelConventions.java @@ -1,9 +1,6 @@ package datadog.opentelemetry.shim.trace; import static datadog.trace.api.DDTags.ANALYTICS_SAMPLE_RATE; -import static datadog.trace.api.DDTags.ERROR_MSG; -import static datadog.trace.api.DDTags.ERROR_STACK; -import static datadog.trace.api.DDTags.ERROR_TYPE; import static datadog.trace.bootstrap.instrumentation.api.Tags.SPAN_KIND; import static datadog.trace.bootstrap.instrumentation.api.Tags.SPAN_KIND_CLIENT; import static datadog.trace.bootstrap.instrumentation.api.Tags.SPAN_KIND_CONSUMER; @@ -19,6 +16,7 @@ import static java.util.Locale.ROOT; import datadog.trace.api.KnownTagCodec; +import datadog.trace.api.KnownTags; import datadog.trace.bootstrap.instrumentation.api.AgentSpan; import datadog.trace.bootstrap.instrumentation.api.SpanAttributes; import datadog.trace.bootstrap.instrumentation.api.Tags; @@ -171,16 +169,16 @@ public static void applySpanEventExceptionAttributes( // Handle exception message Object value = attributes.computeIfAbsent(EXCEPTION_MESSAGE_ATTRIBUTE, k -> exception.getMessage()); - span.setTag(ERROR_MSG, value); + span.setTag(KnownTags.ERROR_MESSAGE_ID, value); // Handle exception type value = attributes.computeIfAbsent(EXCEPTION_TYPE_ATTRIBUTE, k -> exception.getClass().getName()); - span.setTag(ERROR_TYPE, value); + span.setTag(KnownTags.ERROR_TYPE_ID, value); // Handle exception stacktrace value = attributes.computeIfAbsent( EXCEPTION_STACK_TRACE_ATTRIBUTE, k -> stringifyErrorStack(exception)); - span.setTag(ERROR_STACK, value); + span.setTag(KnownTags.ERROR_STACK_ID, value); } static String stringifyErrorStack(Throwable error) { diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/gateway/GatewayBridge.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/gateway/GatewayBridge.java index e5aa16cb6f0..ed0bfd6ed66 100644 --- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/gateway/GatewayBridge.java +++ b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/gateway/GatewayBridge.java @@ -8,6 +8,7 @@ import static com.datadog.appsec.gateway.AppSecRequestContext.REQUEST_HEADERS_ALLOW_LIST; import static com.datadog.appsec.gateway.AppSecRequestContext.RESPONSE_HEADERS_ALLOW_LIST; import static datadog.trace.api.telemetry.LogCollector.SEND_TELEMETRY; +import static datadog.trace.bootstrap.instrumentation.api.OtelHttpSemantics.urlOrPath; import com.datadog.appsec.AppSecSystem; import com.datadog.appsec.api.security.ApiSecurityDownstreamSampler; @@ -1114,13 +1115,12 @@ private NoopFlow onRequestEnded(RequestContext ctx_, IGSpanInfo spanInfo) { private boolean maybeSampleForApiSecurity( AppSecRequestContext ctx, IGSpanInfo spanInfo, Map tags) { log.debug("Checking API Security for end of request handler on span: {}", spanInfo.getSpanId()); - // API Security sampling requires http.route tag or http.url for endpoint inference. + // API Security sampling requires a route or a URL path for endpoint inference. final Object route = tags.get(Tags.HTTP_ROUTE); if (route != null) { ctx.setRoute(route.toString()); } - // Pass http.url to enable endpoint inference when route is absent - final Object url = tags.get(Tags.HTTP_URL); + Object url = urlOrPath(tags); if (url != null) { ctx.setHttpUrl(url.toString()); } diff --git a/dd-java-agent/appsec/src/test/java/com/datadog/appsec/gateway/GatewayBridgeOtelSemanticsForkedTest.java b/dd-java-agent/appsec/src/test/java/com/datadog/appsec/gateway/GatewayBridgeOtelSemanticsForkedTest.java new file mode 100644 index 00000000000..9d164946721 --- /dev/null +++ b/dd-java-agent/appsec/src/test/java/com/datadog/appsec/gateway/GatewayBridgeOtelSemanticsForkedTest.java @@ -0,0 +1,147 @@ +package com.datadog.appsec.gateway; + +import static datadog.context.Context.root; +import static datadog.trace.api.config.GeneralConfig.TRACE_OTEL_SEMANTICS_ENABLED; +import static datadog.trace.api.gateway.Events.EVENTS; +import static datadog.trace.bootstrap.instrumentation.decorator.http.HttpResourceDecorator.HTTP_RESOURCE_DECORATOR; +import static java.util.Collections.emptyList; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.datadog.appsec.api.security.ApiSecuritySampler; +import com.datadog.appsec.event.EventProducerService; +import datadog.trace.api.gateway.InstrumentationGateway; +import datadog.trace.api.gateway.RequestContext; +import datadog.trace.api.gateway.RequestContextSlot; +import datadog.trace.bootstrap.instrumentation.api.AgentPropagation; +import datadog.trace.bootstrap.instrumentation.api.Tags; +import datadog.trace.bootstrap.instrumentation.api.URIDataAdapter; +import datadog.trace.bootstrap.instrumentation.api.URIDefaultDataAdapter; +import datadog.trace.bootstrap.instrumentation.decorator.HttpServerDecorator; +import datadog.trace.common.writer.ListWriter; +import datadog.trace.core.CoreTracer; +import datadog.trace.core.DDSpan; +import datadog.trace.test.junit.utils.config.WithConfig; +import java.net.URI; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.tabletest.junit.TableTest; + +@WithConfig(key = TRACE_OTEL_SEMANTICS_ENABLED, value = "true") +class GatewayBridgeOtelSemanticsForkedTest { + private CoreTracer tracer; + + @BeforeEach + void setUp() { + tracer = CoreTracer.builder().writer(new ListWriter()).build(); + } + + @AfterEach + void tearDown() { + tracer.close(); + } + + @TableTest({ + "Scenario | Path | Route ", + "Without route | /orders/123 | ", + "With route | /orders/123 | /orders/{id}" + }) + void apiSecurityReceivesEndpointDataFromOtelServerSpans(String path, String route) { + TestServerDecorator decorator = new TestServerDecorator(); + DDSpan span = (DDSpan) tracer.startSpan("test", "http.request"); + decorator.afterStart(span); + decorator.onRequest(span, null, URI.create("https://example.com" + path), root()); + if (route != null) { + HTTP_RESOURCE_DECORATOR.withRoute(span, "GET", route); + } + assertEquals(route, span.getTag(Tags.HTTP_ROUTE)); + + InstrumentationGateway gateway = new InstrumentationGateway(); + EventProducerService producer = mock(EventProducerService.class); + when(producer.allSubscribedDataAddresses()).thenReturn(emptyList()); + ApiSecuritySampler sampler = mock(ApiSecuritySampler.class); + GatewayBridge bridge = + new GatewayBridge( + gateway.getSubscriptionService(RequestContextSlot.APPSEC), + producer, + () -> sampler, + emptyList()); + bridge.init(); + AppSecRequestContext appSecContext = new AppSecRequestContext(); + RequestContext requestContext = mock(RequestContext.class); + when(requestContext.getData(RequestContextSlot.APPSEC)).thenReturn(appSecContext); + + try { + gateway + .getCallbackProvider(RequestContextSlot.APPSEC) + .getCallback(EVENTS.requestEnded()) + .apply(requestContext, span); + + verify(sampler).preSampleRequest(appSecContext, "test-http-server"); + assertEquals(route, appSecContext.getRoute()); + if (route == null) { + assertNotNull(appSecContext.getHttpUrl()); + assertEquals(path, URI.create(appSecContext.getHttpUrl()).getPath()); + } + } finally { + span.finish(); + appSecContext.close(); + } + } + + private static final class TestServerDecorator + extends HttpServerDecorator { + @Override + protected String[] instrumentationNames() { + return new String[] {"test-http-server"}; + } + + @Override + protected CharSequence component() { + return "test-http-server"; + } + + @Override + public CharSequence spanName() { + return "http.request"; + } + + @Override + protected AgentPropagation.ContextVisitor getter() { + return null; + } + + @Override + protected AgentPropagation.ContextVisitor responseGetter() { + return null; + } + + @Override + protected String method(URI request) { + return "GET"; + } + + @Override + protected URIDataAdapter url(URI request) { + return new URIDefaultDataAdapter(request); + } + + @Override + protected String peerHostIP(Void connection) { + return null; + } + + @Override + protected int peerPort(Void connection) { + return 0; + } + + @Override + protected int status(Integer response) { + return response; + } + } +} diff --git a/dd-java-agent/instrumentation-testing/src/main/groovy/datadog/trace/agent/test/base/HttpClientTest.groovy b/dd-java-agent/instrumentation-testing/src/main/groovy/datadog/trace/agent/test/base/HttpClientTest.groovy index e787feeda1f..af4103543e3 100644 --- a/dd-java-agent/instrumentation-testing/src/main/groovy/datadog/trace/agent/test/base/HttpClientTest.groovy +++ b/dd-java-agent/instrumentation-testing/src/main/groovy/datadog/trace/agent/test/base/HttpClientTest.groovy @@ -5,8 +5,10 @@ import datadog.trace.agent.test.asserts.TagsAssert import datadog.trace.agent.test.asserts.TraceAssert import datadog.trace.agent.test.naming.VersionedNamingTestBase import datadog.trace.agent.test.server.http.HttpProxy +import datadog.trace.api.Config import datadog.trace.api.DDSpanTypes import datadog.trace.api.DDTags +import datadog.trace.api.KnownTags import datadog.trace.api.appsec.HttpClientRequest import datadog.trace.api.appsec.HttpClientResponse import datadog.trace.api.config.TracerConfig @@ -16,6 +18,7 @@ import datadog.trace.api.gateway.Flow import datadog.trace.api.gateway.RequestContext import datadog.trace.api.gateway.RequestContextSlot import datadog.trace.bootstrap.instrumentation.api.AgentTracer +import datadog.trace.bootstrap.instrumentation.api.OtelHttpSemantics import datadog.trace.bootstrap.instrumentation.api.TagContext import datadog.trace.bootstrap.instrumentation.api.Tags import datadog.trace.bootstrap.instrumentation.api.URIUtils @@ -990,11 +993,15 @@ abstract class HttpClientTest extends VersionedNamingTestBase { boolean ignorePeer = false, Map extraTags = null) { + def otelSemantics = Config.get().isTraceOtelSemanticsEnabled() def expectedQuery = tagQueryString ? uri.query : null def expectedUrl = URIUtils.buildURL(uri.scheme, uri.host, uri.port, uri.path) if (expectedQuery != null && !expectedQuery.empty) { expectedUrl = "$expectedUrl?$expectedQuery" } + if (otelSemantics && emitsUrlFragment() && uri.rawFragment != null) { + expectedUrl = "$expectedUrl#$uri.rawFragment" + } trace.span { if (parentSpan == null) { parent() @@ -1005,9 +1012,9 @@ abstract class HttpClientTest extends VersionedNamingTestBase { serviceName uri.host } operationName operation() - resourceName "$method $uri.path" + resourceName otelSemantics ? OtelHttpSemantics.spanNameMethod(method) : "$method $uri.path" spanType DDSpanTypes.HTTP_CLIENT - errored error + errored otelSemantics && status != null && status >= 400 ? true : error measured true tags { "$Tags.COMPONENT" component @@ -1019,14 +1026,19 @@ abstract class HttpClientTest extends VersionedNamingTestBase { it == null || it == "127.0.0.1" || ignorePeer } // Optional "$Tags.PEER_PORT" { - it == null || it == uri.port || it == proxy.port || it == 443 || ignorePeer + otelSemantics + ? it == OtelHttpSemantics.serverPort(uri) + : (it == null || it == uri.port || it == proxy.port || it == 443 || ignorePeer) } "$Tags.HTTP_URL" expectedUrl "$Tags.HTTP_METHOD" method if (status) { "$Tags.HTTP_STATUS" status } - if (tagQueryString) { + if (otelSemantics) { + "$KnownTags.HTTP_QUERY_STRING_NAME" null + "$DDTags.HTTP_FRAGMENT" null + } else if (tagQueryString) { "$DDTags.HTTP_QUERY" expectedQuery "$DDTags.HTTP_FRAGMENT" { it == null || it == uri.fragment @@ -1041,9 +1053,15 @@ abstract class HttpClientTest extends VersionedNamingTestBase { } if (exception) { this.assertErrorTags(it, exception) + } else if (otelSemantics && status != null && status >= 400) { + "$DDTags.ERROR_TYPE" status.toString() + } + if (otelSemantics) { + defaultTagsNoPeerService() + } else { + peerServiceFrom(Tags.PEER_HOSTNAME) + defaultTags() } - peerServiceFrom(Tags.PEER_HOSTNAME) - defaultTags() if (extraTags) { it.addTags(extraTags) } @@ -1095,6 +1113,10 @@ abstract class HttpClientTest extends VersionedNamingTestBase { true } + boolean emitsUrlFragment() { + true + } + boolean testAppSecClientRequest() { false } diff --git a/dd-java-agent/instrumentation-testing/src/main/groovy/datadog/trace/agent/test/base/HttpServerTest.groovy b/dd-java-agent/instrumentation-testing/src/main/groovy/datadog/trace/agent/test/base/HttpServerTest.groovy index 9d0b67e2c71..a0e751f1b35 100644 --- a/dd-java-agent/instrumentation-testing/src/main/groovy/datadog/trace/agent/test/base/HttpServerTest.groovy +++ b/dd-java-agent/instrumentation-testing/src/main/groovy/datadog/trace/agent/test/base/HttpServerTest.groovy @@ -11,6 +11,7 @@ import datadog.trace.agent.test.utils.OkHttpUtils import datadog.trace.api.Config import datadog.trace.api.DDSpanTypes import datadog.trace.api.DDTags +import datadog.trace.api.KnownTags import datadog.trace.api.ProductActivation import datadog.trace.api.appsec.AppSecContext import datadog.trace.api.config.GeneralConfig @@ -35,6 +36,7 @@ import datadog.trace.api.telemetry.EndpointCollector import datadog.trace.bootstrap.blocking.BlockingActionHelper import datadog.trace.bootstrap.instrumentation.api.AgentTracer import datadog.trace.bootstrap.instrumentation.api.InstrumentationTags +import datadog.trace.bootstrap.instrumentation.api.OtelHttpSemantics import datadog.trace.bootstrap.instrumentation.api.SpanAttributes import datadog.trace.bootstrap.instrumentation.api.SpanLink import datadog.trace.bootstrap.instrumentation.api.Tags @@ -201,7 +203,32 @@ abstract class HttpServerTest extends WithHttpServer { abstract String expectedOperationName() - String expectedResourceName(ServerEndpoint endpoint, String method, URI address) { + Serializable expectedResourceName(ServerEndpoint endpoint, String method, URI address) { + if (Config.get().isTraceOtelSemanticsEnabled()) { + def route = expectedServerSpanRoute(endpoint) + def nameMethod = OtelHttpSemantics.spanNameMethod(method) + if (route == null) { + return nameMethod + } + if (route instanceof CharSequence) { + return "$nameMethod $route" + } + return { + String resourceName -> + def prefix = "$nameMethod " + if (!resourceName.startsWith(prefix)) { + return false + } + def resourceRoute = resourceName.substring(prefix.length()) + if (route instanceof Class) { + return route.isInstance(resourceRoute) + } + if (route instanceof Closure) { + return route.call(resourceRoute) + } + return false + } + } if (endpoint.status == 404 && (changesAll404s() || endpoint.path == "/not-found")) { return "404" } else if (endpoint.hasPathParam) { @@ -243,7 +270,8 @@ abstract class HttpServerTest extends WithHttpServer { it == null || it == EXCEPTION.body }, "error.type" : { - it == null || it == Exception.name + it == null || it == Exception.name || + (Config.get().isTraceOtelSemanticsEnabled() && it == endpoint.status.toString()) }, "error.stack" : { it == null || it instanceof String @@ -894,12 +922,13 @@ abstract class HttpServerTest extends WithHttpServer { } } } - recordedBaggageTags == [ + def expectedBaggageTags = Config.get().isInjectBaggageAsTagsEnabled() ? [ "baggage.user.id" : "test-user", "baggage.session.id": "test-session", "baggage.account.id": "test-account" // "baggage.language" should NOT be present since it's not in default config - ] + ] : [:] + recordedBaggageTags == expectedBaggageTags and: if (isDataStreamsEnabled()) { @@ -2511,11 +2540,10 @@ abstract class HttpServerTest extends WithHttpServer { trace.span { operationName operation - if (handshake.getTag(Tags.HTTP_ROUTE) != null) { - resourceName "websocket ${handshake.getTag(Tags.HTTP_ROUTE) as String}" - } else { - resourceName "websocket ${URI.create(handshake.getTag(Tags.HTTP_URL) as String).path}" - } + def handshakeResource = handshake.resourceName.toString() + def separator = handshakeResource.indexOf(' ') + def websocketResource = separator < 0 ? handshakeResource : handshakeResource.substring(separator + 1) + resourceName "websocket $websocketResource" if (traceStarter && Config.get().isWebsocketMessagesSeparateTraces()) { parent() } else { @@ -2670,15 +2698,19 @@ abstract class HttpServerTest extends WithHttpServer { boolean hasForwardedIP = hasForwardedIP() def expectedExtraServerTags = expectedExtraServerTags(endpoint) def expectedStatus = expectedStatus(endpoint) + def expectedIsErrored = expectedErrored(endpoint) def expectedQueryTag = expectedQueryTag(endpoint) def expectedUrl = expectedUrl(endpoint, address) def expectedIntegrationName = expectedIntegrationName() + def otelSemantics = Config.get().isTraceOtelSemanticsEnabled() + def requestUrl = endpoint.resolve(address) + def expectedPath = Config.get().isHttpServerRawResource() && supportsRaw() ? requestUrl.rawPath : requestUrl.path trace.span { serviceName expectedServiceName() operationName operation() resourceName expectedResourceName(endpoint, method, address) spanType DDSpanTypes.HTTP_SERVER - errored expectedErrored(endpoint) + errored expectedIsErrored if (parentID != null) { traceId traceID parentSpanId parentID @@ -2706,8 +2738,16 @@ abstract class HttpServerTest extends WithHttpServer { "$Tags.NETWORK_CLIENT_IP" null "$Tags.HTTP_CLIENT_IP" clientIp } - "$Tags.HTTP_HOSTNAME" address.host - "$Tags.HTTP_URL" "$expectedUrl" + if (otelSemantics) { + "$Tags.HTTP_URL" null + "$KnownTags.HTTP_HOSTNAME_NAME" address.host + "$KnownTags.SERVER_PORT_NAME" requestUrl.port + "$KnownTags.URL_PATH_NAME" expectedPath + "$KnownTags.URL_SCHEME_NAME" requestUrl.scheme + } else { + "$Tags.HTTP_HOSTNAME" address.host + "$Tags.HTTP_URL" "$expectedUrl" + } "$Tags.HTTP_METHOD" method "$Tags.HTTP_STATUS" expectedStatus "$Tags.HTTP_USER_AGENT" String @@ -2723,6 +2763,10 @@ abstract class HttpServerTest extends WithHttpServer { if (null != expectedExtraErrorInformation) { addTags(expectedExtraErrorInformation) } + if (otelSemantics && expectedIsErrored && + (expectedExtraErrorInformation == null || !expectedExtraErrorInformation.containsKey(DDTags.ERROR_TYPE))) { + "$DDTags.ERROR_TYPE" endpoint.status.toString() + } if (endpoint.query) { "$DDTags.HTTP_QUERY" expectedQueryTag } diff --git a/dd-java-agent/instrumentation-testing/src/main/groovy/datadog/trace/agent/test/naming/NamingConventions.groovy b/dd-java-agent/instrumentation-testing/src/main/groovy/datadog/trace/agent/test/naming/NamingConventions.groovy index 06f40257318..aefb4e3d6fa 100644 --- a/dd-java-agent/instrumentation-testing/src/main/groovy/datadog/trace/agent/test/naming/NamingConventions.groovy +++ b/dd-java-agent/instrumentation-testing/src/main/groovy/datadog/trace/agent/test/naming/NamingConventions.groovy @@ -1,5 +1,9 @@ package datadog.trace.agent.test.naming +/** Marks a forked HTTP test suite that runs with OpenTelemetry HTTP semantics enabled. */ +interface TestingOtelHttpSemantics { +} + class TestingGenericHttpNamingConventions { trait ClientV0 implements VersionedNamingTest { @Override diff --git a/dd-java-agent/instrumentation-testing/src/main/groovy/datadog/trace/agent/test/naming/VersionedNamingTestBase.groovy b/dd-java-agent/instrumentation-testing/src/main/groovy/datadog/trace/agent/test/naming/VersionedNamingTestBase.groovy index 5bc5f1a9eaf..3ed5da2290a 100644 --- a/dd-java-agent/instrumentation-testing/src/main/groovy/datadog/trace/agent/test/naming/VersionedNamingTestBase.groovy +++ b/dd-java-agent/instrumentation-testing/src/main/groovy/datadog/trace/agent/test/naming/VersionedNamingTestBase.groovy @@ -1,6 +1,7 @@ package datadog.trace.agent.test.naming import datadog.trace.agent.test.InstrumentationSpecification +import datadog.trace.api.config.GeneralConfig import datadog.trace.api.config.TracerConfig abstract class VersionedNamingTestBase extends InstrumentationSpecification implements VersionedNamingTest { @@ -10,5 +11,8 @@ abstract class VersionedNamingTestBase extends InstrumentationSpecification impl protected void configurePreAgent() { super.configurePreAgent() injectSysConfig(TracerConfig.TRACE_SPAN_ATTRIBUTE_SCHEMA, "v" + version()) + if (this instanceof TestingOtelHttpSemantics) { + injectSysConfig(GeneralConfig.TRACE_OTEL_SEMANTICS_ENABLED, "true") + } } } diff --git a/dd-java-agent/instrumentation/akka/akka-http/akka-http-10.0/src/baseTest/groovy/AkkaHttpServerInstrumentationTest.groovy b/dd-java-agent/instrumentation/akka/akka-http/akka-http-10.0/src/baseTest/groovy/AkkaHttpServerInstrumentationTest.groovy index 2d807d5e215..c4619379863 100644 --- a/dd-java-agent/instrumentation/akka/akka-http/akka-http-10.0/src/baseTest/groovy/AkkaHttpServerInstrumentationTest.groovy +++ b/dd-java-agent/instrumentation/akka/akka-http/akka-http-10.0/src/baseTest/groovy/AkkaHttpServerInstrumentationTest.groovy @@ -1,6 +1,7 @@ import datadog.trace.agent.test.base.HttpServer import datadog.trace.agent.test.base.HttpServerTest import datadog.trace.agent.test.naming.TestingGenericHttpNamingConventions +import datadog.trace.agent.test.naming.TestingOtelHttpSemantics import datadog.trace.api.config.TraceInstrumentationConfig import datadog.trace.test.util.ThreadUtils import datadog.trace.instrumentation.akkahttp.AkkaHttpServerDecorator @@ -275,6 +276,9 @@ class AkkaHttpServerInstrumentationSyncV0ForkedTest extends AkkaHttpServerInstru class AkkaHttpServerInstrumentationSyncV1ForkedTest extends AkkaHttpServerInstrumentationSyncTest implements TestingGenericHttpNamingConventions.ServerV1 { } +class AkkaHttpServerInstrumentationSyncOtelSemanticsForkedTest extends AkkaHttpServerInstrumentationSyncV0ForkedTest implements TestingOtelHttpSemantics { +} + class AkkaHttpServerInstrumentationAsyncTest extends AkkaHttpServerInstrumentationTest { @Override HttpServer server() { diff --git a/dd-java-agent/instrumentation/apache-httpclient/apache-httpasyncclient-4.0/src/test/groovy/ApacheHttpAsyncClientTest.groovy b/dd-java-agent/instrumentation/apache-httpclient/apache-httpasyncclient-4.0/src/test/groovy/ApacheHttpAsyncClientTest.groovy index bc57def64cf..96fe31e3a6a 100644 --- a/dd-java-agent/instrumentation/apache-httpclient/apache-httpasyncclient-4.0/src/test/groovy/ApacheHttpAsyncClientTest.groovy +++ b/dd-java-agent/instrumentation/apache-httpclient/apache-httpasyncclient-4.0/src/test/groovy/ApacheHttpAsyncClientTest.groovy @@ -1,6 +1,7 @@ import datadog.trace.agent.test.asserts.TraceAssert import datadog.trace.agent.test.base.HttpClientTest import datadog.trace.agent.test.naming.TestingGenericHttpNamingConventions +import datadog.trace.agent.test.naming.TestingOtelHttpSemantics import datadog.trace.instrumentation.apachehttpasyncclient.ApacheHttpAsyncClientDecorator import org.apache.http.HttpHost import org.apache.http.HttpResponse @@ -100,6 +101,9 @@ class ApacheHttpAsyncClientV0Test extends ApacheHttpAsyncClientTest implements T class ApacheHttpAsyncClientV1ForkedTest extends ApacheHttpAsyncClientTest implements TestingGenericHttpNamingConventions.ClientV1 { } +class ApacheHttpAsyncClientOtelSemanticsForkedTest extends ApacheHttpAsyncClientTest implements TestingGenericHttpNamingConventions.ClientV0, TestingOtelHttpSemantics { +} + class ApacheHttpAsyncClientHostRequestTest extends ApacheHttpAsyncClientV0Test { def relativizeUri(URI uri) { diff --git a/dd-java-agent/instrumentation/apache-httpclient/apache-httpclient-4.0/src/test/groovy/ApacheHttpClientTest.groovy b/dd-java-agent/instrumentation/apache-httpclient/apache-httpclient-4.0/src/test/groovy/ApacheHttpClientTest.groovy index 1d34b88381e..684303bd16b 100644 --- a/dd-java-agent/instrumentation/apache-httpclient/apache-httpclient-4.0/src/test/groovy/ApacheHttpClientTest.groovy +++ b/dd-java-agent/instrumentation/apache-httpclient/apache-httpclient-4.0/src/test/groovy/ApacheHttpClientTest.groovy @@ -1,5 +1,6 @@ import datadog.trace.agent.test.base.HttpClientTest import datadog.trace.agent.test.naming.TestingGenericHttpNamingConventions +import datadog.trace.agent.test.naming.TestingOtelHttpSemantics import datadog.trace.instrumentation.apachehttpclient.ApacheHttpClientDecorator import org.apache.http.HttpHost import org.apache.http.HttpRequest @@ -195,3 +196,7 @@ class ApacheClientUriRequestResponseHandlerContextV0ForkedTest extends ApacheCli @Timeout(5) class ApacheClientUriRequestResponseHandlerContextV1ForkedTest extends ApacheClientUriRequestResponseHandlerContext implements TestingGenericHttpNamingConventions.ClientV1 { } + +@Timeout(5) +class ApacheClientOtelSemanticsForkedTest extends ApacheClientUriRequestResponseHandlerContext implements TestingOtelHttpSemantics { +} diff --git a/dd-java-agent/instrumentation/axway-api-7.5/src/main/java/datadog/trace/instrumentation/axway/AxwayHTTPPluginDecorator.java b/dd-java-agent/instrumentation/axway-api-7.5/src/main/java/datadog/trace/instrumentation/axway/AxwayHTTPPluginDecorator.java index 10ce28bdd1b..a87104464d8 100644 --- a/dd-java-agent/instrumentation/axway-api-7.5/src/main/java/datadog/trace/instrumentation/axway/AxwayHTTPPluginDecorator.java +++ b/dd-java-agent/instrumentation/axway-api-7.5/src/main/java/datadog/trace/instrumentation/axway/AxwayHTTPPluginDecorator.java @@ -2,12 +2,17 @@ import static java.lang.invoke.MethodType.methodType; +import datadog.trace.api.KnownTags; import datadog.trace.bootstrap.instrumentation.api.AgentPropagation; import datadog.trace.bootstrap.instrumentation.api.AgentSpan; -import datadog.trace.bootstrap.instrumentation.api.Tags; +import datadog.trace.bootstrap.instrumentation.api.OtelHttpSemantics; +import datadog.trace.bootstrap.instrumentation.api.TagExtractor; import datadog.trace.bootstrap.instrumentation.api.URIDataAdapter; import datadog.trace.bootstrap.instrumentation.api.URIDefaultDataAdapter; import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; +import datadog.trace.bootstrap.instrumentation.api.http.DatadogServerUrl; +import datadog.trace.bootstrap.instrumentation.api.http.HttpServerUrl; +import datadog.trace.bootstrap.instrumentation.api.http.OtelServerUrl; import datadog.trace.bootstrap.instrumentation.decorator.HttpServerDecorator; import de.thetaphi.forbiddenapis.SuppressForbidden; import java.lang.invoke.MethodHandle; @@ -22,6 +27,8 @@ // request = is com.vordel.circuit.net.State, connection = com.vordel.dwe.http.ServerTransaction public class AxwayHTTPPluginDecorator extends HttpServerDecorator { private static final Logger log = LoggerFactory.getLogger(AxwayHTTPPluginDecorator.class); + private static final TagExtractor URL_TAGS = + OTEL_SEMANTICS ? OtelServerUrl.INSTANCE : DatadogServerUrl.URL_ONLY; public static final CharSequence AXWAY_TRY_TRANSACTION = UTF8BytesString.create("axway.trytransaction"); @@ -198,10 +205,21 @@ protected int status(final Object serverTransaction) { */ public void onTransaction(AgentSpan span, Object stateInstance) { if (span != null) { - setStringTagFromStateField(span, Tags.PEER_HOSTNAME, stateInstance, hostField_mh); - setStringTagFromStateField(span, Tags.PEER_PORT, stateInstance, portField_mh); - setStringTagFromStateField(span, Tags.HTTP_METHOD, stateInstance, methodField_mh); - setURLTagFromUriStateField(span, stateInstance); + String method = getStringFromStateField(stateInstance, methodField_mh); + String host = getStringFromStateField(stateInstance, hostField_mh); + String port = getStringFromStateField(stateInstance, portField_mh); + if (OTEL_SEMANTICS) { + OtelHttpSemantics.setRequestMethod(span, method); + OtelHttpSemantics.setMethodResourceName(span, method); + } else { + span.setTag(KnownTags.PEER_HOSTNAME_ID, host); + span.setTag(KnownTags.PEER_PORT_ID, port); + span.setTag(KnownTags.HTTP_METHOD_ID, method); + } + span.setTagsFrom( + HttpServerUrl.fromTransaction( + getStringFromStateField(stateInstance, uriField_mh), host, port), + URL_TAGS); } } @@ -233,28 +251,13 @@ private static InetSocketAddress getRemoteAddr(Object obj) { return new InetSocketAddress(0); } - private static void setStringTagFromStateField( - AgentSpan span, String tag, Object stateInstance, MethodHandle mh) { - String v = ""; + private static String getStringFromStateField(Object stateInstance, MethodHandle mh) { try { - v = (String) mh.invoke(stateInstance); + Object value = mh.invoke(stateInstance); + return value == null ? null : value.toString(); } catch (Throwable e) { - log.debug( - "Can't invoke '{}' on instance '{}'; ; Tag '{}' not set.", mh, stateInstance, tag, e); - } - span.setTag(tag, v); - } - - private static void setURLTagFromUriStateField(AgentSpan span, Object stateInstance) { - try { - span.setTag(Tags.HTTP_URL, uriField_mh.invoke(stateInstance).toString()); - } catch (Throwable e) { - log.debug( - "Can't invoke '{}' on instance '{}'; Tag '{}' not set.", - uriField_mh, - stateInstance, - Tags.HTTP_URL, - e); + log.debug("Can't invoke '{}' on instance '{}'; value not read.", mh, stateInstance, e); + return null; } } } diff --git a/dd-java-agent/instrumentation/google-http-client-1.19/src/test/groovy/GoogleHttpClientTest.groovy b/dd-java-agent/instrumentation/google-http-client-1.19/src/test/groovy/GoogleHttpClientTest.groovy index 5efcd6d7acb..0b606c320f5 100644 --- a/dd-java-agent/instrumentation/google-http-client-1.19/src/test/groovy/GoogleHttpClientTest.groovy +++ b/dd-java-agent/instrumentation/google-http-client-1.19/src/test/groovy/GoogleHttpClientTest.groovy @@ -1,6 +1,7 @@ import com.google.api.client.http.HttpRequest import com.google.api.client.http.HttpResponse import datadog.trace.agent.test.naming.TestingGenericHttpNamingConventions +import datadog.trace.agent.test.naming.TestingOtelHttpSemantics import spock.lang.Timeout abstract class GoogleHttpClientTest extends AbstractGoogleHttpClientTest { @@ -18,3 +19,7 @@ class GoogleHttpClientV0ForkedTest extends GoogleHttpClientTest implements Testi @Timeout(5) class GoogleHttpClientV1ForkedTest extends GoogleHttpClientTest implements TestingGenericHttpNamingConventions.ClientV1 { } + +@Timeout(5) +class GoogleHttpClientOtelSemanticsForkedTest extends GoogleHttpClientTest implements TestingGenericHttpNamingConventions.ClientV0, TestingOtelHttpSemantics { +} diff --git a/dd-java-agent/instrumentation/java/java-net/java-net-1.8/src/test/groovy/HttpUrlConnectionTest.groovy b/dd-java-agent/instrumentation/java/java-net/java-net-1.8/src/test/groovy/HttpUrlConnectionTest.groovy index 52d45844a33..7222b55e90a 100644 --- a/dd-java-agent/instrumentation/java/java-net/java-net-1.8/src/test/groovy/HttpUrlConnectionTest.groovy +++ b/dd-java-agent/instrumentation/java/java-net/java-net-1.8/src/test/groovy/HttpUrlConnectionTest.groovy @@ -4,6 +4,7 @@ import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activeSpan import datadog.trace.agent.test.base.HttpClientTest import datadog.trace.agent.test.naming.TestingGenericHttpNamingConventions +import datadog.trace.agent.test.naming.TestingOtelHttpSemantics import datadog.trace.api.DDSpanTypes import datadog.trace.bootstrap.instrumentation.api.Tags import datadog.trace.bootstrap.instrumentation.httpurlconnection.HttpUrlConnectionDecorator @@ -380,3 +381,6 @@ class HttpUrlConnectionV0ForkedTest extends HttpUrlConnectionTest implements Tes class HttpUrlConnectionV1ForkedTest extends HttpUrlConnectionTest implements TestingGenericHttpNamingConventions.ClientV1 { } + +class HttpUrlConnectionOtelSemanticsForkedTest extends HttpUrlConnectionTest implements TestingGenericHttpNamingConventions.ClientV0, TestingOtelHttpSemantics { +} diff --git a/dd-java-agent/instrumentation/jersey/jersey-2.0/build.gradle b/dd-java-agent/instrumentation/jersey/jersey-2.0/build.gradle index 0bb3e609294..9e7a5ce57fc 100644 --- a/dd-java-agent/instrumentation/jersey/jersey-2.0/build.gradle +++ b/dd-java-agent/instrumentation/jersey/jersey-2.0/build.gradle @@ -19,6 +19,7 @@ testJvmConstraints { // there are tests with jersey2 and grizzly on the grizzly-http-2.3.20 module addTestSuiteForDir('jersey2JettyTest', 'jersey2JettyTest') +addForkedTestTask('jersey2JettyTest') addTestSuiteForDir('jersey3JettyTest', 'jersey3JettyTest') addTestSuiteForDir('latestDepTest', 'test') diff --git a/dd-java-agent/instrumentation/jersey/jersey-2.0/src/jersey2JettyTest/groovy/datadog/trace/instrumentation/jersey2/Jersey2JettyTest.groovy b/dd-java-agent/instrumentation/jersey/jersey-2.0/src/jersey2JettyTest/groovy/datadog/trace/instrumentation/jersey2/Jersey2JettyTest.groovy index b2d8dd20751..cdff4d99d26 100644 --- a/dd-java-agent/instrumentation/jersey/jersey-2.0/src/jersey2JettyTest/groovy/datadog/trace/instrumentation/jersey2/Jersey2JettyTest.groovy +++ b/dd-java-agent/instrumentation/jersey/jersey-2.0/src/jersey2JettyTest/groovy/datadog/trace/instrumentation/jersey2/Jersey2JettyTest.groovy @@ -2,6 +2,7 @@ package datadog.trace.instrumentation.jersey2 import datadog.trace.agent.test.base.HttpServer import datadog.trace.agent.test.base.HttpServerTest +import datadog.trace.agent.test.naming.TestingOtelHttpSemantics import javax.ws.rs.NotFoundException import javax.ws.rs.core.Response @@ -89,3 +90,6 @@ class Jersey2JettyTest extends HttpServerTest { } } } + +class Jersey2OtelSemanticsForkedTest extends Jersey2JettyTest implements TestingOtelHttpSemantics { +} diff --git a/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-11.0/src/test/groovy/Jetty11Test.groovy b/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-11.0/src/test/groovy/Jetty11Test.groovy index fd80aa18479..0a7aeca9982 100644 --- a/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-11.0/src/test/groovy/Jetty11Test.groovy +++ b/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-11.0/src/test/groovy/Jetty11Test.groovy @@ -1,6 +1,7 @@ import datadog.trace.agent.test.base.HttpServer import datadog.trace.agent.test.base.HttpServerTest import datadog.trace.agent.test.naming.TestingGenericHttpNamingConventions +import datadog.trace.agent.test.naming.TestingOtelHttpSemantics import datadog.trace.instrumentation.servlet5.HtmlRumServlet import datadog.trace.instrumentation.servlet5.TestServlet5 import datadog.trace.instrumentation.servlet5.XmlRumServlet @@ -162,6 +163,9 @@ class Jetty11V1ForkedTest extends Jetty11Test implements TestingGenericHttpNamin } } +class Jetty11OtelSemanticsForkedTest extends Jetty11Test implements TestingGenericHttpNamingConventions.ServerV0, TestingOtelHttpSemantics { +} + class JettyRumInjectionForkedTest extends Jetty11V0ForkedTest { @Override boolean testRumInjection() { diff --git a/dd-java-agent/instrumentation/netty/netty-4.1/src/test/groovy/Netty41ServerTest.groovy b/dd-java-agent/instrumentation/netty/netty-4.1/src/test/groovy/Netty41ServerTest.groovy index 1c4831d0ce5..3faf3c13257 100644 --- a/dd-java-agent/instrumentation/netty/netty-4.1/src/test/groovy/Netty41ServerTest.groovy +++ b/dd-java-agent/instrumentation/netty/netty-4.1/src/test/groovy/Netty41ServerTest.groovy @@ -4,6 +4,7 @@ import datadog.trace.agent.test.base.HttpServerTest import datadog.trace.agent.test.base.WebsocketClient import datadog.trace.agent.test.base.WebsocketServer import datadog.trace.agent.test.naming.TestingNettyHttpNamingConventions +import datadog.trace.agent.test.naming.TestingOtelHttpSemantics import datadog.trace.bootstrap.instrumentation.api.URIUtils import datadog.trace.instrumentation.netty41.server.NettyHttpServerDecorator import io.netty.bootstrap.ServerBootstrap @@ -335,6 +336,9 @@ class Netty41ServerV0Test extends Netty41ServerTest implements TestingNettyHttpN class Netty41ServerV1ForkedTest extends Netty41ServerTest implements TestingNettyHttpNamingConventions.ServerV1 { } +class Netty41ServerOtelSemanticsForkedTest extends Netty41ServerTest implements TestingNettyHttpNamingConventions.ServerV0, TestingOtelHttpSemantics { +} + class WsEndpoint { static volatile ChannelHandlerContext activeSession @@ -351,4 +355,3 @@ class WsEndpoint { } } - diff --git a/dd-java-agent/instrumentation/okhttp/okhttp-3.0/src/test/groovy/OkHttp3Test.groovy b/dd-java-agent/instrumentation/okhttp/okhttp-3.0/src/test/groovy/OkHttp3Test.groovy index 2aed1c0682a..3b414f4185c 100644 --- a/dd-java-agent/instrumentation/okhttp/okhttp-3.0/src/test/groovy/OkHttp3Test.groovy +++ b/dd-java-agent/instrumentation/okhttp/okhttp-3.0/src/test/groovy/OkHttp3Test.groovy @@ -1,5 +1,6 @@ import datadog.trace.agent.test.base.HttpClientTest import datadog.trace.agent.test.naming.TestingGenericHttpNamingConventions +import datadog.trace.agent.test.naming.TestingOtelHttpSemantics import datadog.trace.instrumentation.okhttp3.OkHttpClientDecorator import okhttp3.* import okhttp3.internal.http.HttpMethod @@ -109,3 +110,22 @@ class OkHttp3V0ForkedTest extends OkHttp3Test { @Timeout(5) class OkHttp3V1ForkedTest extends OkHttp3Test implements TestingGenericHttpNamingConventions.ClientV1 { } + +@Timeout(5) +class OkHttp3OtelSemanticsForkedTest extends OkHttp3Test implements TestingOtelHttpSemantics { + + @Override + int version() { + return 0 + } + + @Override + String service() { + return null + } + + @Override + String operation() { + return "okhttp.request" + } +} diff --git a/dd-java-agent/instrumentation/play-ws/play-ws-2.1/src/test/groovy/PlayWSClientTest.groovy b/dd-java-agent/instrumentation/play-ws/play-ws-2.1/src/test/groovy/PlayWSClientTest.groovy index 72a5b41be3a..dae498722e2 100644 --- a/dd-java-agent/instrumentation/play-ws/play-ws-2.1/src/test/groovy/PlayWSClientTest.groovy +++ b/dd-java-agent/instrumentation/play-ws/play-ws-2.1/src/test/groovy/PlayWSClientTest.groovy @@ -1,3 +1,4 @@ +import datadog.trace.agent.test.naming.TestingOtelHttpSemantics import play.libs.ws.StandaloneWSClient import play.libs.ws.StandaloneWSRequest import play.libs.ws.StandaloneWSResponse @@ -8,6 +9,7 @@ import scala.concurrent.ExecutionContext import scala.concurrent.Future import scala.concurrent.duration.Duration import spock.lang.Shared +import spock.lang.Timeout import java.util.concurrent.TimeUnit @@ -129,3 +131,7 @@ class PlayScalaStreamedWSClientTest extends PlayWSClientTestBase { wsClient?.close() } } + +@Timeout(5) +class PlayWSClientOtelSemanticsForkedTest extends PlayJavaWSClientTest implements TestingOtelHttpSemantics { +} diff --git a/dd-java-agent/instrumentation/play-ws/play-ws-common/src/test/groovy/PlayWSClientTestBase.groovy b/dd-java-agent/instrumentation/play-ws/play-ws-common/src/test/groovy/PlayWSClientTestBase.groovy index f5c0ffbb471..820a8886812 100644 --- a/dd-java-agent/instrumentation/play-ws/play-ws-common/src/test/groovy/PlayWSClientTestBase.groovy +++ b/dd-java-agent/instrumentation/play-ws/play-ws-common/src/test/groovy/PlayWSClientTestBase.groovy @@ -57,6 +57,11 @@ abstract class PlayWSClientTestBase extends HttpClientTest { return false } + @Override + boolean emitsUrlFragment() { + return false + } + @Override int version() { return 0 diff --git a/dd-java-agent/instrumentation/play/play-2.6/src/test/groovy/datadog/trace/instrumentation/play26/server/PlayServerTest.groovy b/dd-java-agent/instrumentation/play/play-2.6/src/test/groovy/datadog/trace/instrumentation/play26/server/PlayServerTest.groovy index 5f42d474c10..a12670a1285 100644 --- a/dd-java-agent/instrumentation/play/play-2.6/src/test/groovy/datadog/trace/instrumentation/play26/server/PlayServerTest.groovy +++ b/dd-java-agent/instrumentation/play/play-2.6/src/test/groovy/datadog/trace/instrumentation/play26/server/PlayServerTest.groovy @@ -1,5 +1,7 @@ package datadog.trace.instrumentation.play26.server +import datadog.trace.agent.test.naming.TestingOtelHttpSemantics + class PlayServerTest extends AbstractPlayServerTest { // Disabling involving Appsec / Instrumentation Gateway, they are tested in play-appsec-2.x modules @@ -48,3 +50,6 @@ class PlayServerTest extends AbstractPlayServerTest { false } } + +class PlayServerOtelSemanticsForkedTest extends PlayServerTest implements TestingOtelHttpSemantics { +} diff --git a/dd-java-agent/instrumentation/play/play-2.6/src/testFixtures/groovy/datadog/trace/instrumentation/play26/server/AbstractPlayServerTest.groovy b/dd-java-agent/instrumentation/play/play-2.6/src/testFixtures/groovy/datadog/trace/instrumentation/play26/server/AbstractPlayServerTest.groovy index 8edf9005759..59af26913d0 100644 --- a/dd-java-agent/instrumentation/play/play-2.6/src/testFixtures/groovy/datadog/trace/instrumentation/play26/server/AbstractPlayServerTest.groovy +++ b/dd-java-agent/instrumentation/play/play-2.6/src/testFixtures/groovy/datadog/trace/instrumentation/play26/server/AbstractPlayServerTest.groovy @@ -3,8 +3,10 @@ package datadog.trace.instrumentation.play26.server import datadog.trace.agent.test.asserts.TraceAssert import datadog.trace.agent.test.base.HttpServer import datadog.trace.agent.test.base.HttpServerTest +import datadog.trace.api.Config import datadog.trace.api.DDSpanTypes import datadog.trace.api.DDTags +import datadog.trace.api.KnownTags import datadog.trace.bootstrap.instrumentation.api.Tags import datadog.trace.instrumentation.play26.PlayHttpServerDecorator import groovy.transform.CompileStatic @@ -125,6 +127,9 @@ class AbstractPlayServerTest extends HttpServerTest { @Override void handlerSpan(TraceAssert trace, ServerEndpoint endpoint = SUCCESS) { def expectedQueryTag = expectedQueryTag(endpoint) + def otelSemantics = Config.get().isTraceOtelSemanticsEnabled() + def requestUrl = endpoint.resolve(address) + def expectedPath = Config.get().isHttpServerRawResource() && supportsRaw() ? requestUrl.rawPath : requestUrl.path trace.span { serviceName expectedServiceName() operationName "play.request" @@ -137,8 +142,16 @@ class AbstractPlayServerTest extends HttpServerTest { "$Tags.PEER_HOST_IPV4" '127.0.0.1' "$Tags.HTTP_CLIENT_IP" (endpoint == FORWARDED ? endpoint.body : '127.0.0.1') "$Tags.NETWORK_CLIENT_IP" '127.0.0.1' - "$Tags.HTTP_URL" String - "$Tags.HTTP_HOSTNAME" address.host + if (otelSemantics) { + "$Tags.HTTP_URL" null + "$Tags.HTTP_HOSTNAME" address.host + "$KnownTags.SERVER_PORT_NAME" address.port + "$KnownTags.URL_PATH_NAME" expectedPath + "$KnownTags.URL_SCHEME_NAME" address.scheme + } else { + "$Tags.HTTP_URL" String + "$Tags.HTTP_HOSTNAME" address.host + } "$Tags.HTTP_METHOD" String // BUG // "$Tags.HTTP_ROUTE" String @@ -146,7 +159,11 @@ class AbstractPlayServerTest extends HttpServerTest { errorTags(endpoint == CUSTOM_EXCEPTION ? TestHttpErrorHandler.CustomRuntimeException : RuntimeException, endpoint.body) } if (endpoint.query) { - "$DDTags.HTTP_QUERY" expectedQueryTag + if (otelSemantics) { + "$KnownTags.HTTP_QUERY_STRING_NAME" expectedQueryTag + } else { + "$DDTags.HTTP_QUERY" expectedQueryTag + } } defaultTags() } diff --git a/dd-java-agent/instrumentation/ratpack-1.5/src/test/groovy/server/RatpackHttpServerTest.groovy b/dd-java-agent/instrumentation/ratpack-1.5/src/test/groovy/server/RatpackHttpServerTest.groovy index a0ede81d695..a47684fcfef 100644 --- a/dd-java-agent/instrumentation/ratpack-1.5/src/test/groovy/server/RatpackHttpServerTest.groovy +++ b/dd-java-agent/instrumentation/ratpack-1.5/src/test/groovy/server/RatpackHttpServerTest.groovy @@ -3,8 +3,11 @@ package server import datadog.trace.agent.test.asserts.TraceAssert import datadog.trace.agent.test.base.HttpServer import datadog.trace.agent.test.base.HttpServerTest +import datadog.trace.agent.test.naming.TestingOtelHttpSemantics +import datadog.trace.api.Config import datadog.trace.api.DDSpanTypes import datadog.trace.api.DDTags +import datadog.trace.api.KnownTags import datadog.trace.bootstrap.instrumentation.api.Tags import datadog.trace.instrumentation.netty41.server.NettyHttpServerDecorator import datadog.trace.instrumentation.ratpack.RatpackServerDecorator @@ -33,15 +36,6 @@ class RatpackHttpServerTest extends HttpServerTest { "netty.request" } - @Override - String expectedResourceName(ServerEndpoint endpoint, String method, URI address) { - if (endpoint == PATH_PARAM) { - 'GET /path/:id/param' - } else { - super.expectedResourceName(endpoint, method, address) - } - } - @Override Map expectedIGPathParams() { [id: '123'] @@ -89,7 +83,7 @@ class RatpackHttpServerTest extends HttpServerTest { @Override String testPathParam() { - true + '/path/:id/param' } @Override @@ -122,7 +116,7 @@ class RatpackHttpServerTest extends HttpServerTest { @Override Serializable expectedServerSpanRoute(ServerEndpoint endpoint) { - return String + return endpoint == PATH_PARAM ? testPathParam() : endpoint.rawPath } @Override @@ -138,6 +132,9 @@ class RatpackHttpServerTest extends HttpServerTest { @Override void handlerSpan(TraceAssert trace, ServerEndpoint endpoint = SUCCESS) { + def otelSemantics = Config.get().isTraceOtelSemanticsEnabled() + def requestUrl = endpoint.resolve(address) + def expectedRoute = expectedServerSpanRoute(endpoint) trace.span { serviceName expectedServiceName() operationName "ratpack.handler" @@ -149,21 +146,34 @@ class RatpackHttpServerTest extends HttpServerTest { "$Tags.SPAN_KIND" Tags.SPAN_KIND_SERVER "$Tags.PEER_HOST_IPV4" "127.0.0.1" // This span ignores "x-forwards-from". "$Tags.PEER_PORT" Integer - "$Tags.HTTP_URL" String - "$Tags.HTTP_HOSTNAME" "${address.host}" + if (otelSemantics) { + "$Tags.HTTP_URL" null + "$Tags.HTTP_HOSTNAME" address.host + "$KnownTags.SERVER_PORT_NAME" address.port + "$KnownTags.URL_PATH_NAME" requestUrl.rawPath + "$KnownTags.URL_SCHEME_NAME" address.scheme + } else { + "$Tags.HTTP_URL" String + "$Tags.HTTP_HOSTNAME" "${address.host}" + } "$Tags.HTTP_METHOD" String "$Tags.HTTP_STATUS" Integer - "$Tags.HTTP_ROUTE" String + "$Tags.HTTP_ROUTE" expectedRoute "$Tags.HTTP_CLIENT_IP" (endpoint == FORWARDED ? endpoint.body : '127.0.0.1') "$Tags.NETWORK_CLIENT_IP" '127.0.0.1' if (endpoint == EXCEPTION) { errorTags(Exception, EXCEPTION.body) + } else if (otelSemantics && endpoint == ERROR) { + "$DDTags.ERROR_TYPE" endpoint.status.toString() } if (endpoint.query) { - "$DDTags.HTTP_QUERY" endpoint.rawQuery + tag(DDTags.HTTP_QUERY, endpoint.rawQuery) } defaultTags() } } } } + +class RatpackHttpServerOtelSemanticsForkedTest extends RatpackHttpServerTest implements TestingOtelHttpSemantics { +} diff --git a/dd-java-agent/instrumentation/rs/jax-rs/jax-rs-annotations/jax-rs-annotations-2.0/src/test/groovy/JaxRsAnnotations2InstrumentationTest.groovy b/dd-java-agent/instrumentation/rs/jax-rs/jax-rs-annotations/jax-rs-annotations-2.0/src/test/groovy/JaxRsAnnotations2InstrumentationTest.groovy index fe542a6baac..5d1e4c107c6 100644 --- a/dd-java-agent/instrumentation/rs/jax-rs/jax-rs-annotations/jax-rs-annotations-2.0/src/test/groovy/JaxRsAnnotations2InstrumentationTest.groovy +++ b/dd-java-agent/instrumentation/rs/jax-rs/jax-rs-annotations/jax-rs-annotations-2.0/src/test/groovy/JaxRsAnnotations2InstrumentationTest.groovy @@ -1,4 +1,6 @@ import datadog.trace.agent.test.InstrumentationSpecification +import datadog.trace.api.Config +import datadog.trace.api.config.GeneralConfig import datadog.trace.api.config.TraceInstrumentationConfig import datadog.trace.bootstrap.instrumentation.api.Tags import datadog.trace.instrumentation.jaxrs2.JaxRsAnnotationsDecorator @@ -54,7 +56,7 @@ class JaxRsAnnotations2InstrumentationTest extends InstrumentationSpecification trace(2) { span { operationName "test" - resourceName name + resourceName(Config.get().isTraceOtelSemanticsEnabled() ? otelName : name) parent() tags { "$Tags.COMPONENT" "jax-rs" @@ -77,65 +79,65 @@ class JaxRsAnnotations2InstrumentationTest extends InstrumentationSpecification } where: - name | obj - "/a" | new Jax() { + name | otelName | obj + "/a" | "HTTP /a" | new Jax() { @Path("/a") void call() { } } - "GET /b" | new Jax() { + "GET /b" | "GET /b" | new Jax() { @GET @Path("/b") void call() { } } - "POST /interface/c" | new InterfaceWithPath() { + "POST /interface/c" | "POST /interface/c" | new InterfaceWithPath() { @POST @Path("/c") void call() { } } - "HEAD /interface" | new InterfaceWithPath() { + "HEAD /interface" | "HEAD /interface" | new InterfaceWithPath() { @HEAD void call() { } } - "POST /abstract/d" | new AbstractClassWithPath() { + "POST /abstract/d" | "POST /abstract/d" | new AbstractClassWithPath() { @POST @Path("/d") void call() { } } - "PATCH /interface" | new InterfaceWithPath() { + "PATCH /interface" | "PATCH /interface" | new InterfaceWithPath() { @PATCH void call() { } } - "CUSTOM /interface" | new InterfaceWithPath() { + "CUSTOM /interface" | "HTTP /interface" | new InterfaceWithPath() { @CustomMethod void call() { } } - "PUT /abstract" | new AbstractClassWithPath() { + "PUT /abstract" | "PUT /abstract" | new AbstractClassWithPath() { @PUT void call() { } } - "OPTIONS /child/e" | new ChildClassWithPath() { + "OPTIONS /child/e" | "OPTIONS /child/e" | new ChildClassWithPath() { @OPTIONS @Path("/e") void call() { } } - "DELETE /child/call" | new ChildClassWithPath() { + "DELETE /child/call" | "DELETE /child/call" | new ChildClassWithPath() { @DELETE void call() { } } - "POST /child/call" | new ChildClassWithPath() - "GET /child/call" | new JavaInterfaces.ChildClassOnInterface() + "POST /child/call" | "POST /child/call" | new ChildClassWithPath() + "GET /child/call" | "GET /child/call" | new JavaInterfaces.ChildClassOnInterface() // TODO: uncomment when we drop support for Java 7 - // "GET /child/invoke" | new JavaInterfaces.DefaultChildClassOnInterface() + // "GET /child/invoke" | "GET /child/invoke" | new JavaInterfaces.DefaultChildClassOnInterface() className = JaxRsAnnotationsDecorator.DECORATE.className(obj.class) } @@ -188,3 +190,11 @@ class JaxRsAnnotations2InstrumentationTest extends InstrumentationSpecification } } } + +class JaxRsAnnotations2OtelSemanticsForkedTest extends JaxRsAnnotations2InstrumentationTest { + @Override + protected void configurePreAgent() { + super.configurePreAgent() + injectSysConfig(GeneralConfig.TRACE_OTEL_SEMANTICS_ENABLED, "true") + } +} diff --git a/dd-java-agent/instrumentation/rs/jax-rs/jax-rs-client/jax-rs-client-2.0/src/test/groovy/JaxRsClientAsyncTest.groovy b/dd-java-agent/instrumentation/rs/jax-rs/jax-rs-client/jax-rs-client-2.0/src/test/groovy/JaxRsClientAsyncTest.groovy index 0af25e9b290..01c31059cf1 100644 --- a/dd-java-agent/instrumentation/rs/jax-rs/jax-rs-client/jax-rs-client-2.0/src/test/groovy/JaxRsClientAsyncTest.groovy +++ b/dd-java-agent/instrumentation/rs/jax-rs/jax-rs-client/jax-rs-client-2.0/src/test/groovy/JaxRsClientAsyncTest.groovy @@ -1,6 +1,7 @@ import datadog.environment.JavaVirtualMachine import datadog.trace.agent.test.base.HttpClientTest import datadog.trace.agent.test.naming.TestingGenericHttpNamingConventions +import datadog.trace.agent.test.naming.TestingOtelHttpSemantics import datadog.trace.instrumentation.jaxrs.JaxRsClientDecorator import org.apache.cxf.jaxrs.client.spec.ClientBuilderImpl import org.glassfish.jersey.client.ClientConfig @@ -94,6 +95,10 @@ class JerseyClientAsyncTest extends JaxRsClientAsyncTest { } } +@Timeout(5) +class JerseyClientOtelSemanticsForkedTest extends JerseyClientAsyncTest implements TestingOtelHttpSemantics { +} + abstract class ResteasyClientAsyncTest extends JaxRsClientAsyncTest { @Override diff --git a/dd-java-agent/instrumentation/rs/jax-rs/jax-rs-client/jax-rs-client-2.0/src/test/groovy/JaxRsClientTest.groovy b/dd-java-agent/instrumentation/rs/jax-rs/jax-rs-client/jax-rs-client-2.0/src/test/groovy/JaxRsClientTest.groovy index 78660157452..964231d9015 100644 --- a/dd-java-agent/instrumentation/rs/jax-rs/jax-rs-client/jax-rs-client-2.0/src/test/groovy/JaxRsClientTest.groovy +++ b/dd-java-agent/instrumentation/rs/jax-rs/jax-rs-client/jax-rs-client-2.0/src/test/groovy/JaxRsClientTest.groovy @@ -1,6 +1,7 @@ import datadog.environment.JavaVirtualMachine import datadog.trace.agent.test.base.HttpClientTest import datadog.trace.agent.test.naming.TestingGenericHttpNamingConventions +import datadog.trace.agent.test.naming.TestingOtelHttpSemantics import datadog.trace.instrumentation.jaxrs.JaxRsClientDecorator import datadog.trace.test.util.Flaky import org.apache.cxf.jaxrs.client.spec.ClientBuilderImpl @@ -101,6 +102,10 @@ class ResteasyClientV0ForkedTest extends ResteasyClientTest { class ResteasyClientV1ForkedTest extends ResteasyClientTest implements TestingGenericHttpNamingConventions.ClientV1 { } +@Timeout(5) +class JaxRsClientOtelSemanticsForkedTest extends ResteasyClientTest implements TestingOtelHttpSemantics { +} + @Timeout(5) @IgnoreIf({ // TODO Java 17: This version of apache-cxf doesn't work on Java 17 diff --git a/dd-java-agent/instrumentation/servlet/javax-servlet/javax-servlet-3.0/src/test/groovy/JettyServlet3Test.groovy b/dd-java-agent/instrumentation/servlet/javax-servlet/javax-servlet-3.0/src/test/groovy/JettyServlet3Test.groovy index 1a4ef433990..34fb9c4bc46 100644 --- a/dd-java-agent/instrumentation/servlet/javax-servlet/javax-servlet-3.0/src/test/groovy/JettyServlet3Test.groovy +++ b/dd-java-agent/instrumentation/servlet/javax-servlet/javax-servlet-3.0/src/test/groovy/JettyServlet3Test.groovy @@ -1,6 +1,7 @@ import datadog.trace.agent.test.asserts.TraceAssert import datadog.trace.agent.test.base.HttpServer import datadog.trace.agent.test.naming.TestingGenericHttpNamingConventions +import datadog.trace.agent.test.naming.TestingOtelHttpSemantics import datadog.trace.bootstrap.instrumentation.api.AgentSpan import datadog.trace.bootstrap.instrumentation.api.Tags import datadog.trace.instrumentation.servlet3.AsyncDispatcherDecorator @@ -254,6 +255,9 @@ class JettyServlet3SyncRumInjectionForkedTest extends JettyServlet3TestSync { class JettyServlet3SyncV1ForkedTest extends JettyServlet3TestSync implements TestingGenericHttpNamingConventions.ServerV1 { } +class JettyServlet3SyncOtelSemanticsForkedTest extends JettyServlet3TestSync implements TestingOtelHttpSemantics { +} + class JettyServlet3TestAsync extends JettyServlet3Test { @Override diff --git a/dd-java-agent/instrumentation/spring/spring-webflux/spring-webflux-6.0/build.gradle b/dd-java-agent/instrumentation/spring/spring-webflux/spring-webflux-6.0/build.gradle index e71fca0e23c..a3b7ca53114 100644 --- a/dd-java-agent/instrumentation/spring/spring-webflux/spring-webflux-6.0/build.gradle +++ b/dd-java-agent/instrumentation/spring/spring-webflux/spring-webflux-6.0/build.gradle @@ -10,6 +10,7 @@ testJvmConstraints { // test that webflux5 instrumentation works for webflux6 too addTestSuite('iastTest') addTestSuite('bootTest') +addForkedTestTask('bootTest') addTestSuiteForDir('latestDepTest', 'test') addTestSuiteExtendingForDir('latestDepForkedTest', 'latestDepTest', 'test') addTestSuiteExtendingForDir('latestDepBootTest', 'latestDepTest', 'bootTest') diff --git a/dd-java-agent/instrumentation/spring/spring-webflux/spring-webflux-6.0/src/bootTest/groovy/SpringWebfluxTest.groovy b/dd-java-agent/instrumentation/spring/spring-webflux/spring-webflux-6.0/src/bootTest/groovy/SpringWebfluxTest.groovy index 61b73aed5eb..7aade15fdf7 100644 --- a/dd-java-agent/instrumentation/spring/spring-webflux/spring-webflux-6.0/src/bootTest/groovy/SpringWebfluxTest.groovy +++ b/dd-java-agent/instrumentation/spring/spring-webflux/spring-webflux-6.0/src/bootTest/groovy/SpringWebfluxTest.groovy @@ -10,8 +10,13 @@ import static datadog.trace.api.config.TraceInstrumentationConfig.TRACE_ANNOTATI import datadog.trace.agent.test.InstrumentationSpecification import datadog.trace.agent.test.asserts.TraceAssert +import datadog.trace.agent.test.asserts.TagsAssert +import datadog.trace.agent.test.naming.TestingOtelHttpSemantics +import datadog.trace.api.Config import datadog.trace.api.DDSpanTypes import datadog.trace.api.DDTags +import datadog.trace.api.KnownTags +import datadog.trace.api.config.GeneralConfig import datadog.trace.bootstrap.instrumentation.api.Tags import datadog.trace.bootstrap.instrumentation.api.URIUtils import datadog.trace.core.DDSpan @@ -55,6 +60,9 @@ class SpringWebfluxHttp11Test extends InstrumentationSpecification { protected void configurePreAgent() { super.configurePreAgent() injectSysConfig(TRACE_ANNOTATION_ASYNC, "true") + if (this instanceof TestingOtelHttpSemantics) { + injectSysConfig(GeneralConfig.TRACE_OTEL_SEMANTICS_ENABLED, "true") + } } def "Basic GET test #testName"() { @@ -84,8 +92,7 @@ class SpringWebfluxHttp11Test extends InstrumentationSpecification { "$Tags.SPAN_KIND" Tags.SPAN_KIND_SERVER "$Tags.PEER_HOST_IPV4" "127.0.0.1" "$Tags.PEER_PORT" Integer - "$Tags.HTTP_URL" url - "$Tags.HTTP_HOSTNAME" "localhost" + this.serverUrlTags(it, url) "$Tags.HTTP_METHOD" "GET" "$Tags.HTTP_STATUS" 200 "$Tags.HTTP_USER_AGENT" String @@ -135,6 +142,7 @@ class SpringWebfluxHttp11Test extends InstrumentationSpecification { "annotation API without parameters" | "/foo" | "/foo" | "getFooModel" | new FooModel(0L, "DEFAULT").toString() "annotation API with one parameter" | "/foo/1" | "/foo/{id}" | "getFooModel" | new FooModel(1L, "pass").toString() + "annotation API with query" | "/foo/1?view=full" | "/foo/{id}" | "getFooModel" | new FooModel(1L, "pass").toString() "annotation API with two parameters" | "/foo/2/world" | "/foo/{id}/{name}" | "getFooModel" | new FooModel(2L, "world").toString() "annotation API delayed response" | "/foo-delayed" | "/foo-delayed" | "getFooDelayed" | new FooModel(3L, "delayed").toString() } @@ -166,8 +174,7 @@ class SpringWebfluxHttp11Test extends InstrumentationSpecification { "$Tags.SPAN_KIND" Tags.SPAN_KIND_SERVER "$Tags.PEER_HOST_IPV4" "127.0.0.1" "$Tags.PEER_PORT" Integer - "$Tags.HTTP_URL" url - "$Tags.HTTP_HOSTNAME" "localhost" + this.serverUrlTags(it, url) "$Tags.HTTP_METHOD" "GET" "$Tags.HTTP_STATUS" 200 "$Tags.HTTP_USER_AGENT" String @@ -300,7 +307,7 @@ class SpringWebfluxHttp11Test extends InstrumentationSpecification { } trace(2) { span { - resourceName "404" + resourceName Config.get().isTraceOtelSemanticsEnabled() ? "GET" : "404" operationName "netty.request" spanType DDSpanTypes.HTTP_SERVER childOf(traceParent) @@ -309,8 +316,7 @@ class SpringWebfluxHttp11Test extends InstrumentationSpecification { "$Tags.SPAN_KIND" Tags.SPAN_KIND_SERVER "$Tags.PEER_HOST_IPV4" "127.0.0.1" "$Tags.PEER_PORT" Integer - "$Tags.HTTP_URL" url - "$Tags.HTTP_HOSTNAME" "localhost" + this.serverUrlTags(it, url) "$Tags.HTTP_METHOD" "GET" "$Tags.HTTP_STATUS" 404 "$Tags.HTTP_USER_AGENT" String @@ -366,8 +372,7 @@ class SpringWebfluxHttp11Test extends InstrumentationSpecification { "$Tags.SPAN_KIND" Tags.SPAN_KIND_SERVER "$Tags.PEER_HOST_IPV4" "127.0.0.1" "$Tags.PEER_PORT" Integer - "$Tags.HTTP_URL" url - "$Tags.HTTP_HOSTNAME" "localhost" + this.serverUrlTags(it, url) "$Tags.HTTP_METHOD" "POST" "$Tags.HTTP_STATUS" 202 "$Tags.HTTP_USER_AGENT" String @@ -433,10 +438,12 @@ class SpringWebfluxHttp11Test extends InstrumentationSpecification { "$Tags.SPAN_KIND" Tags.SPAN_KIND_SERVER "$Tags.PEER_HOST_IPV4" "127.0.0.1" "$Tags.PEER_PORT" Integer - "$Tags.HTTP_URL" url - "$Tags.HTTP_HOSTNAME" "localhost" + this.serverUrlTags(it, url) "$Tags.HTTP_METHOD" "GET" "$Tags.HTTP_STATUS" 500 + if (Config.get().isTraceOtelSemanticsEnabled()) { + "$DDTags.ERROR_TYPE" "500" + } "$Tags.HTTP_USER_AGENT" String "$Tags.HTTP_CLIENT_IP" "127.0.0.1" "$Tags.NETWORK_CLIENT_IP" "127.0.0.1" @@ -524,8 +531,7 @@ class SpringWebfluxHttp11Test extends InstrumentationSpecification { "$Tags.SPAN_KIND" Tags.SPAN_KIND_SERVER "$Tags.PEER_HOST_IPV4" "127.0.0.1" "$Tags.PEER_PORT" Integer - "$Tags.HTTP_URL" url - "$Tags.HTTP_HOSTNAME" "localhost" + this.serverUrlTags(it, url) "$Tags.HTTP_METHOD" "GET" "$Tags.HTTP_STATUS" 307 "$Tags.HTTP_USER_AGENT" String @@ -570,8 +576,7 @@ class SpringWebfluxHttp11Test extends InstrumentationSpecification { "$Tags.SPAN_KIND" Tags.SPAN_KIND_SERVER "$Tags.PEER_HOST_IPV4" "127.0.0.1" "$Tags.PEER_PORT" Integer - "$Tags.HTTP_URL" finalUrl - "$Tags.HTTP_HOSTNAME" "localhost" + this.serverUrlTags(it, finalUrl) "$Tags.HTTP_METHOD" "GET" "$Tags.HTTP_STATUS" 200 "$Tags.HTTP_USER_AGENT" String @@ -630,8 +635,7 @@ class SpringWebfluxHttp11Test extends InstrumentationSpecification { "$Tags.SPAN_KIND" Tags.SPAN_KIND_SERVER "$Tags.PEER_HOST_IPV4" "127.0.0.1" "$Tags.PEER_PORT" Integer - "$Tags.HTTP_URL" url - "$Tags.HTTP_HOSTNAME" "localhost" + this.serverUrlTags(it, url) "$Tags.HTTP_METHOD" "GET" "$Tags.HTTP_STATUS" 200 "$Tags.HTTP_USER_AGENT" String @@ -692,7 +696,7 @@ class SpringWebfluxHttp11Test extends InstrumentationSpecification { def traceParent sortSpansByStart() trace(2) { - clientSpan(it, null, "http.request", "spring-webflux-client", "GET", URI.create(url), null, false, null, false, + clientSpan(it, null, "http.request", "spring-webflux-client", "GET", URI.create(url), null, false, false, ["message": "The subscription was cancelled", "event": "cancelled"]) traceParent = clientSpan(it, span(0), "netty.client.request", "netty-client", "GET", URI.create(url), null) } @@ -707,8 +711,7 @@ class SpringWebfluxHttp11Test extends InstrumentationSpecification { "$Tags.SPAN_KIND" Tags.SPAN_KIND_SERVER "$Tags.PEER_HOST_IPV4" "127.0.0.1" "$Tags.PEER_PORT" Integer - "$Tags.HTTP_URL" url - "$Tags.HTTP_HOSTNAME" "localhost" + this.serverUrlTags(it, url) "$Tags.HTTP_METHOD" "GET" "$Tags.HTTP_USER_AGENT" String "$Tags.HTTP_CLIENT_IP" "127.0.0.1" @@ -762,8 +765,7 @@ class SpringWebfluxHttp11Test extends InstrumentationSpecification { "$Tags.SPAN_KIND" Tags.SPAN_KIND_SERVER "$Tags.PEER_HOST_IPV4" "127.0.0.1" "$Tags.PEER_PORT" Integer - "$Tags.HTTP_URL" url - "$Tags.HTTP_HOSTNAME" "localhost" + this.serverUrlTags(it, url) "$Tags.HTTP_METHOD" "GET" "$Tags.HTTP_STATUS" 101 "$Tags.HTTP_USER_AGENT" String @@ -802,6 +804,21 @@ class SpringWebfluxHttp11Test extends InstrumentationSpecification { } + void serverUrlTags(TagsAssert tags, String url) { + def uri = URI.create(url) + if (Config.get().isTraceOtelSemanticsEnabled()) { + tags.tag(KnownTags.HTTP_HOSTNAME_NAME, uri.host) + tags.tag(KnownTags.SERVER_PORT_NAME, uri.port) + tags.tag(KnownTags.URL_PATH_NAME, uri.path) + tags.tag(KnownTags.URL_SCHEME_NAME, uri.scheme) + tags.tag(KnownTags.HTTP_QUERY_STRING_NAME, uri.rawQuery) + } else { + tags.tag(Tags.HTTP_URL, url) + tags.tag(Tags.HTTP_HOSTNAME, uri.host) + tags.tag(DDTags.HTTP_QUERY, uri.query) + } + } + def clientSpan( TraceAssert trace, Object parentSpan, @@ -811,12 +828,13 @@ class SpringWebfluxHttp11Test extends InstrumentationSpecification { URI uri, Integer status = 200, boolean error = false, - Throwable exception = null, - boolean tagQueryString = false, + boolean tagQueryString = Config.get().isHttpClientTagQueryString(), Map extraTags = null) { def ret - def expectedQuery = tagQueryString ? uri.query : null + def otelSemantics = Config.get().isTraceOtelSemanticsEnabled() + def statusError = otelSemantics && status != null && status >= 400 + def expectedQuery = otelSemantics || tagQueryString ? uri.query : null def expectedUrl = URIUtils.buildURL(uri.scheme, uri.host, uri.port, uri.path) if (expectedQuery != null && !expectedQuery.empty) { expectedUrl = "$expectedUrl?$expectedQuery" @@ -829,26 +847,36 @@ class SpringWebfluxHttp11Test extends InstrumentationSpecification { childOf((DDSpan) parentSpan) } operationName operation + if (otelSemantics) { + resourceName method + } spanType DDSpanTypes.HTTP_CLIENT - errored error + errored error || statusError measured true tags { "$Tags.COMPONENT" component "$Tags.SPAN_KIND" Tags.SPAN_KIND_CLIENT - "$Tags.PEER_HOSTNAME" uri.host - "$Tags.PEER_HOST_IPV4" { it == null || it == "127.0.0.1" } // Optional - "$Tags.PEER_PORT" { it == null || it == uri.port } + if (!otelSemantics || component == "netty-client") { + // Netty also decorates the underlying socket connection. + "$Tags.PEER_HOSTNAME" uri.host + "$Tags.PEER_HOST_IPV4" { it == null || it == "127.0.0.1" } // Optional + "$Tags.PEER_PORT" { it == null || it == uri.port } + } + if (otelSemantics) { + "$Tags.PEER_HOSTNAME" uri.host + "$Tags.PEER_PORT" uri.port + } "$Tags.HTTP_URL" expectedUrl "$Tags.HTTP_METHOD" method if (status) { "$Tags.HTTP_STATUS" status } - if (tagQueryString) { + if (!otelSemantics && tagQueryString) { "$DDTags.HTTP_QUERY" expectedQuery "$DDTags.HTTP_FRAGMENT" { it == null || it == uri.fragment } // Optional } - if (exception) { - errorTags(exception.class, exception.message) + if (statusError) { + "$DDTags.ERROR_TYPE" status.toString() } peerServiceFrom(Tags.PEER_HOSTNAME) defaultTags() @@ -861,6 +889,9 @@ class SpringWebfluxHttp11Test extends InstrumentationSpecification { } } +class SpringWebfluxOtelSemanticsForkedTest extends SpringWebfluxHttp11Test implements TestingOtelHttpSemantics { +} + class SpringWebfluxHttp2UpgradeTest extends SpringWebfluxHttp11Test { @Override protected HttpClient buildClient() { diff --git a/dd-java-agent/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/src/test/groovy/datadog/trace/instrumentation/springweb6/boot/SpringBootBasedTest.groovy b/dd-java-agent/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/src/test/groovy/datadog/trace/instrumentation/springweb6/boot/SpringBootBasedTest.groovy index 885831440f5..f9077e78710 100644 --- a/dd-java-agent/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/src/test/groovy/datadog/trace/instrumentation/springweb6/boot/SpringBootBasedTest.groovy +++ b/dd-java-agent/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/src/test/groovy/datadog/trace/instrumentation/springweb6/boot/SpringBootBasedTest.groovy @@ -5,6 +5,8 @@ import datadog.trace.agent.test.asserts.TraceAssert import datadog.trace.agent.test.base.HttpServer import datadog.trace.agent.test.base.HttpServerTest import datadog.trace.agent.test.base.WebsocketServer +import datadog.trace.agent.test.naming.TestingOtelHttpSemantics +import datadog.trace.api.Config import datadog.trace.api.ConfigDefaults import datadog.trace.api.DDSpanTypes import datadog.trace.api.DDTags @@ -244,6 +246,9 @@ class SpringBootBasedTest extends HttpServerTest @Override String expectedResourceName(ServerEndpoint endpoint, String method, URI address) { + if (Config.get().isTraceOtelSemanticsEnabled()) { + return super.expectedResourceName(endpoint, method, address) + } if (endpoint.status == 404 && endpoint.path == "/not-found") { return "404" } else if (endpoint.hasPathParam) { @@ -512,6 +517,9 @@ class SpringBootRumInjectionForkedTest extends SpringBootBasedTest { } } +class SpringBootOtelSemanticsForkedTest extends SpringBootBasedTest implements TestingOtelHttpSemantics { +} + /** * Runs the full suite with the opt-in legacy route-naming filter enabled, to verify that path still * produces the same traces (including APMS-8174). The default path (getHandler + ControllerAdvice) diff --git a/dd-java-agent/instrumentation/tomcat/tomcat-5.5/src/test/groovy/TomcatServletTest.groovy b/dd-java-agent/instrumentation/tomcat/tomcat-5.5/src/test/groovy/TomcatServletTest.groovy index 3e507f1ae71..48fd61bbf32 100644 --- a/dd-java-agent/instrumentation/tomcat/tomcat-5.5/src/test/groovy/TomcatServletTest.groovy +++ b/dd-java-agent/instrumentation/tomcat/tomcat-5.5/src/test/groovy/TomcatServletTest.groovy @@ -13,6 +13,7 @@ import com.google.common.io.Files import datadog.trace.agent.test.asserts.TraceAssert import datadog.trace.agent.test.base.HttpServer import datadog.trace.agent.test.naming.TestingGenericHttpNamingConventions +import datadog.trace.agent.test.naming.TestingOtelHttpSemantics import datadog.trace.api.DDTags import datadog.trace.instrumentation.servlet3.TestServlet3 import org.apache.catalina.Context @@ -344,3 +345,6 @@ class TomcatServletV1ForkedTest extends TomcatServletTest implements TestingGene true } } + +class TomcatServletOtelSemanticsForkedTest extends TomcatServletTest implements TestingGenericHttpNamingConventions.ServerV0, TestingOtelHttpSemantics { +} diff --git a/dd-java-agent/instrumentation/undertow/undertow-2.2/src/test/groovy/UndertowTest.groovy b/dd-java-agent/instrumentation/undertow/undertow-2.2/src/test/groovy/UndertowTest.groovy index c369cce9e43..1c76ffa68bc 100644 --- a/dd-java-agent/instrumentation/undertow/undertow-2.2/src/test/groovy/UndertowTest.groovy +++ b/dd-java-agent/instrumentation/undertow/undertow-2.2/src/test/groovy/UndertowTest.groovy @@ -2,6 +2,7 @@ import datadog.appsec.api.blocking.Blocking import datadog.appsec.api.blocking.BlockingException import datadog.trace.agent.test.base.HttpServer import datadog.trace.agent.test.base.HttpServerTest +import datadog.trace.agent.test.naming.TestingOtelHttpSemantics import io.undertow.Handlers import io.undertow.Undertow import io.undertow.UndertowOptions @@ -209,3 +210,6 @@ class UndertowTest extends HttpServerTest { } } } + +class UndertowOtelSemanticsForkedTest extends UndertowTest implements TestingOtelHttpSemantics { +} diff --git a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-4.0/src/test/groovy/server/VertxHttpServerForkedTest.groovy b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-4.0/src/test/groovy/server/VertxHttpServerForkedTest.groovy index 1385b59833e..4e4c815a5c1 100644 --- a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-4.0/src/test/groovy/server/VertxHttpServerForkedTest.groovy +++ b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-4.0/src/test/groovy/server/VertxHttpServerForkedTest.groovy @@ -12,7 +12,10 @@ import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.SUCCES import datadog.trace.agent.test.asserts.TraceAssert import datadog.trace.agent.test.base.HttpServer import datadog.trace.agent.test.base.HttpServerTest +import datadog.trace.agent.test.naming.TestingNettyHttpNamingConventions +import datadog.trace.agent.test.naming.TestingOtelHttpSemantics import datadog.trace.api.Config +import datadog.trace.api.DDTags import okhttp3.MediaType import okhttp3.MultipartBody import okhttp3.RequestBody @@ -229,6 +232,8 @@ class VertxHttpServerForkedTest extends HttpServerTest { "$Tags.HTTP_STATUS" Integer if (endpoint == EXCEPTION && this.testExceptionTag()) { errorTags(RuntimeException, EXCEPTION.body) + } else if (Config.get().isTraceOtelSemanticsEnabled() && endpoint == ERROR) { + "$DDTags.ERROR_TYPE" endpoint.status.toString() } defaultTags() } @@ -242,3 +247,6 @@ class VertxHttpServerWorkerForkedTest extends VertxHttpServerForkedTest { return new VertxServer(verticle(), routerBasePath(), true) } } + +class VertxHttpServerOtelSemanticsForkedTest extends VertxHttpServerForkedTest implements TestingNettyHttpNamingConventions.ServerV0, TestingOtelHttpSemantics { +} diff --git a/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java b/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java index bf5bab30d05..e5eab1da02b 100644 --- a/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java +++ b/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java @@ -16,6 +16,7 @@ public final class ConfigDefaults { static final BitSet DEFAULT_HTTP_SERVER_ERROR_STATUSES; static final BitSet DEFAULT_HTTP_CLIENT_ERROR_STATUSES; + static final BitSet DEFAULT_OTEL_HTTP_CLIENT_ERROR_STATUSES; static final BitSet DEFAULT_GRPC_SERVER_ERROR_STATUSES; static final BitSet DEFAULT_GRPC_CLIENT_ERROR_STATUSES; @@ -26,6 +27,8 @@ public final class ConfigDefaults { DEFAULT_HTTP_SERVER_ERROR_STATUSES.set(500, 600); DEFAULT_HTTP_CLIENT_ERROR_STATUSES = new BitSet(); DEFAULT_HTTP_CLIENT_ERROR_STATUSES.set(400, 500); + DEFAULT_OTEL_HTTP_CLIENT_ERROR_STATUSES = new BitSet(); + DEFAULT_OTEL_HTTP_CLIENT_ERROR_STATUSES.set(400, 600); DEFAULT_GRPC_SERVER_ERROR_STATUSES = new BitSet(); DEFAULT_GRPC_SERVER_ERROR_STATUSES.set(2, 17); DEFAULT_GRPC_CLIENT_ERROR_STATUSES = new BitSet(); @@ -126,6 +129,7 @@ public final class ConfigDefaults { public static final boolean DEFAULT_METRICS_OTEL_EXPERIMENTAL_ENABLED = true; + static final boolean DEFAULT_TRACE_OTEL_SEMANTICS_ENABLED = false; public static final int DEFAULT_OTLP_TRACES_TIMEOUT = 10_000; // ms static final String DEFAULT_OTLP_HTTP_LOGS_ENDPOINT = "v1/logs"; diff --git a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ContextInterpreter.java b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ContextInterpreter.java index a1467a014e1..e4b2905346e 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ContextInterpreter.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ContextInterpreter.java @@ -69,6 +69,7 @@ public abstract class ContextInterpreter implements AgentPropagation.KeyClassifi private final boolean clientIpResolutionEnabled; private final boolean collectClientIp; private final boolean aiGuardEnabled; + private final boolean otelSemanticsEnabled; private boolean collectIpHeaders; private final boolean requestHeaderTagsCommaAllowed; private final int baggageMaxItems; @@ -87,6 +88,7 @@ protected ContextInterpreter(Config config) { this.clientIpResolutionEnabled = config.isTraceClientIpResolverEnabled(); this.collectClientIp = config.isClientIpEnabled(); this.aiGuardEnabled = config.isAiGuardEnabled(); + this.otelSemanticsEnabled = config.isTraceOtelSemanticsEnabled(); this.propagationTagsFactory = PropagationTags.factory(config); this.requestHeaderTagsCommaAllowed = config.isRequestHeaderTagsCommaAllowed(); this.baggageMaxItems = config.getTraceBaggageMaxItems(); @@ -125,7 +127,7 @@ protected final boolean handledForwarding(String key, String value) { } protected final boolean handledXForwarding(String key, String value) { - if (value == null || !collectIpHeaders) { + if (value == null || (!collectIpHeaders && !otelSemanticsEnabled)) { return false; } @@ -137,7 +139,7 @@ protected final boolean handledXForwarding(String key, String value) { getHeaders().xForwardedHost = value; return true; } - if (X_FORWARDED_FOR_KEY.equalsIgnoreCase(key)) { + if (collectIpHeaders && X_FORWARDED_FOR_KEY.equalsIgnoreCase(key)) { getHeaders().xForwardedFor = value; return true; } diff --git a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/HttpEndpointPostProcessor.java b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/HttpEndpointPostProcessor.java index c2e0dd72761..2bb29fe31f4 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/HttpEndpointPostProcessor.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/HttpEndpointPostProcessor.java @@ -1,8 +1,8 @@ package datadog.trace.core.tagprocessor; +import static datadog.trace.bootstrap.instrumentation.api.OtelHttpSemantics.urlOrPath; import static datadog.trace.bootstrap.instrumentation.api.Tags.HTTP_METHOD; import static datadog.trace.bootstrap.instrumentation.api.Tags.HTTP_ROUTE; -import static datadog.trace.bootstrap.instrumentation.api.Tags.HTTP_URL; import datadog.trace.api.TagMap; import datadog.trace.api.endpoint.EndpointResolver; @@ -67,8 +67,9 @@ public void processTags( try { String httpRoute = unsafeTags.getString(HTTP_ROUTE); - String httpUrl = unsafeTags.getString(HTTP_URL); - endpointResolver.resolveEndpoint(unsafeTags, httpRoute, httpUrl); + Object httpUrl = urlOrPath(unsafeTags); + endpointResolver.resolveEndpoint( + unsafeTags, httpRoute, httpUrl == null ? null : httpUrl.toString()); } catch (Throwable t) { log.debug("Error processing HTTP endpoint for span {}", spanContext.getSpanId(), t); } diff --git a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/QueryObfuscator.java b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/QueryObfuscator.java index 6ae813a772f..aab37678c0c 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/QueryObfuscator.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/QueryObfuscator.java @@ -3,6 +3,8 @@ import com.google.re2j.Matcher; import com.google.re2j.Pattern; import com.google.re2j.PatternSyntaxException; +import datadog.trace.api.Config; +import datadog.trace.api.DDSpanTypes; import datadog.trace.api.DDTags; import datadog.trace.api.TagMap; import datadog.trace.api.internal.VisibleForTesting; @@ -58,12 +60,18 @@ public final class QueryObfuscator extends TagsPostProcessor { private final Pattern pattern; private final String replacement; private final boolean failClosed; + private final boolean otelSemanticsEnabled; /** * If regex is null - then used default regex pattern If regex is empty string - then disable * regex. If the regex does not compile, the query string is not reported. */ public QueryObfuscator(String regex) { + this(regex, Config.get().isTraceOtelSemanticsEnabled()); + } + + QueryObfuscator(String regex, boolean otelSemanticsEnabled) { + this.otelSemanticsEnabled = otelSemanticsEnabled; // empty string -> disabled query obfuscation if ("".equals(regex)) { this.pattern = null; @@ -135,8 +143,27 @@ public void processTags( Object url = unsafeTags.getObject(Tags.HTTP_URL); if (url instanceof CharSequence) { - unsafeTags.put(Tags.HTTP_URL, url + "?" + query); + unsafeTags.put(Tags.HTTP_URL, appendQuery(url.toString(), query.toString())); + } + + // HTTP clients carry the query separately only so this processor can scrub it before + // rebuilding url.full. url.query is a server attribute in the OTel HTTP conventions. + if (otelSemanticsEnabled + && spanContext != null + && spanContext.getSpanType() != null + && DDSpanTypes.HTTP_CLIENT.contentEquals(spanContext.getSpanType())) { + unsafeTags.remove(DDTags.HTTP_QUERY); } } } + + private String appendQuery(final String url, final String query) { + // With OTel semantics disabled, captured URLs may contain decoded '#' path characters. + // Preserve the existing query concatenation in that mode. + final int fragment = otelSemanticsEnabled ? url.indexOf('#') : -1; + if (fragment < 0) { + return url + "?" + query; + } + return url.substring(0, fragment) + "?" + query + url.substring(fragment); + } } diff --git a/dd-trace-core/src/main/java/datadog/trace/lambda/LambdaAppSecHandler.java b/dd-trace-core/src/main/java/datadog/trace/lambda/LambdaAppSecHandler.java index 8ed1eb4cb6a..4e0c35dc953 100644 --- a/dd-trace-core/src/main/java/datadog/trace/lambda/LambdaAppSecHandler.java +++ b/dd-trace-core/src/main/java/datadog/trace/lambda/LambdaAppSecHandler.java @@ -334,7 +334,7 @@ static void applyHttpTags(TagContext ctx, LambdaRequestData req, LambdaURIDataAd } if (req.host != null) { - // No query string: QueryObfuscator obfuscates DDTags.HTTP_QUERY and re-appends it here. + // QueryObfuscator sanitizes the separate query tag before appending it to this URL. ctx.putTag( Tags.HTTP_URL, URIUtils.buildURL(url.scheme(), url.host(), url.port(), url.path())); } diff --git a/dd-trace-core/src/test/java/datadog/trace/core/otlp/trace/OtlpTraceJsonCollectorTest.java b/dd-trace-core/src/test/java/datadog/trace/core/otlp/trace/OtlpTraceJsonCollectorTest.java index 384f903efd0..17bc41d85af 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/otlp/trace/OtlpTraceJsonCollectorTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/otlp/trace/OtlpTraceJsonCollectorTest.java @@ -97,6 +97,24 @@ void tagsAreEmittedUnderTheirOpenTelemetryNameWhenOtelSemanticsEnabled() throws assertFalse( attrKeys.contains("http.method"), "renamed tag must not also appear under its Datadog name; got " + attrKeys); + assertTrue( + attrKeys.contains("url.query"), + "renamed tag must use its OpenTelemetry name; got " + attrKeys); + assertFalse( + attrKeys.contains("http.query.string"), + "renamed tag must not also appear under its Datadog name; got " + attrKeys); + assertTrue( + attrKeys.contains("network.peer.address"), + "renamed tag must use its OpenTelemetry name; got " + attrKeys); + assertFalse( + attrKeys.contains("network.client.ip"), + "renamed tag must not also appear under its Datadog name; got " + attrKeys); + assertTrue( + attrKeys.contains("server.address"), + "canonical OpenTelemetry tag must pass through unchanged; got " + attrKeys); + assertTrue( + attrKeys.contains("server.port"), + "canonical OpenTelemetry tag must pass through unchanged; got " + attrKeys); assertTrue( attrKeys.contains("custom.unregistered"), "a tag the registry does not name passes through unchanged; got " + attrKeys); @@ -115,6 +133,26 @@ void tagsAreEmittedUnderTheirDatadogNameWhenOtelSemanticsDisabled() throws IOExc attrKeys.contains("http.request.method"), "tag must not appear under its OpenTelemetry name when OTel semantics are disabled; got " + attrKeys); + assertTrue( + attrKeys.contains("http.query.string"), + "tag must use its Datadog name when OTel semantics are disabled; got " + attrKeys); + assertFalse( + attrKeys.contains("url.query"), + "tag must not appear under its OpenTelemetry name when OTel semantics are disabled; got " + + attrKeys); + assertTrue( + attrKeys.contains("network.client.ip"), + "tag must use its Datadog name when OTel semantics are disabled; got " + attrKeys); + assertFalse( + attrKeys.contains("network.peer.address"), + "tag must not appear under its OpenTelemetry name when OTel semantics are disabled; got " + + attrKeys); + assertTrue( + attrKeys.contains("server.address"), + "canonical tag must pass through unchanged; got " + attrKeys); + assertTrue( + attrKeys.contains("server.port"), + "canonical tag must pass through unchanged; got " + attrKeys); assertTrue( attrKeys.contains("custom.unregistered"), "a tag the registry does not name passes through unchanged; got " + attrKeys); @@ -200,6 +238,29 @@ private static List exportedServerPorts(String kind, Consumer } } + @TableTest({ + "Scenario | Kind ", + "client | client ", + "producer | producer" + }) + void outboundNetworkPeerAddressDoesNotSetClientIp(String kind) throws IOException { + AgentSpan agentSpan = TRACER.startSpan("test", "op.peer-address"); + agentSpan.setTag(SPAN_KIND, kind); + agentSpan.setTag("network.peer.address", "192.0.2.10"); + agentSpan.setSamplingPriority(PrioritySampling.USER_KEEP, SamplingMechanism.DEFAULT); + agentSpan.finish(); + + assertEquals("192.0.2.10", agentSpan.getTag("network.peer.address")); + assertNull(agentSpan.getTag("network.client.ip")); + + OtlpTraceJsonCollector collector = new OtlpTraceJsonCollector(); + collector.addTrace(asList((CoreSpan) agentSpan)); + Set attrKeys = attributeKeys(onlySpan(collector.collectTraces())); + + assertTrue(attrKeys.contains("network.peer.address")); + assertFalse(attrKeys.contains("network.client.ip")); + } + private static Set collectTagsAttributeKeys(boolean otelSemanticsEnabled) throws IOException { Config realConfig = Config.get(); @@ -209,8 +270,13 @@ private static Set collectTagsAttributeKeys(boolean otelSemanticsEnabled configMock.when(Config::get).thenReturn(config); AgentSpan agentSpan = TRACER.startSpan("test", "op.tagged"); + agentSpan.setTag(SPAN_KIND, SPAN_KIND_SERVER); agentSpan.setResourceName("GET /api"); agentSpan.setTag("http.method", "GET"); + agentSpan.setTag("http.query.string", "view=full"); + agentSpan.setTag("network.client.ip", "192.0.2.10"); + agentSpan.setTag("server.address", "example.com"); + agentSpan.setTag("server.port", 443); agentSpan.setTag("custom.unregistered", "value"); agentSpan.setSamplingPriority(PrioritySampling.USER_KEEP, SamplingMechanism.DEFAULT); agentSpan.finish(); diff --git a/dd-trace-core/src/test/java/datadog/trace/core/otlp/trace/OtlpTraceProtoTest.java b/dd-trace-core/src/test/java/datadog/trace/core/otlp/trace/OtlpTraceProtoTest.java index 1402c16c7b3..d4ed0db45cd 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/otlp/trace/OtlpTraceProtoTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/otlp/trace/OtlpTraceProtoTest.java @@ -372,6 +372,11 @@ static Stream cases() { // ── tag types ───────────────────────────────────────────────────────── Arguments.of( "span with string tag", asList(taggedSpan("tagged.op", tags("http.method", "GET")))), + Arguments.of( + "server query uses the configured namespace", + asList( + kindSpan("GET /api/users", SPAN_KIND_SERVER) + .extraTags(tags("http.query.string", "view=full")))), Arguments.of( "span with long tag", asList(taggedSpan("tagged.op", tags("http.status_code", 200L)))), Arguments.of( diff --git a/dd-trace-core/src/test/java/datadog/trace/core/tagprocessor/HttpEndpointPostProcessorTest.java b/dd-trace-core/src/test/java/datadog/trace/core/tagprocessor/HttpEndpointPostProcessorTest.java index e12c1a0dfa6..e6369b97c60 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/tagprocessor/HttpEndpointPostProcessorTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/tagprocessor/HttpEndpointPostProcessorTest.java @@ -7,6 +7,7 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; +import datadog.trace.api.KnownTags; import datadog.trace.api.TagMap; import datadog.trace.api.endpoint.EndpointResolver; import datadog.trace.bootstrap.instrumentation.api.AppendableSpanLinks; @@ -57,6 +58,21 @@ void shouldComputeAndTagHttpEndpointFromUrlWhenRouteIsInvalidWithoutTouchingReso assertEquals("/users/{param:int}/orders/{param:int}", tags.get(Tags.HTTP_ENDPOINT)); } + @Test + void shouldComputeAndTagHttpEndpointFromOtelUrlPath() { + EndpointResolver endpointResolver = new EndpointResolver(true, false); + HttpEndpointPostProcessor processor = new HttpEndpointPostProcessor(endpointResolver); + Map tagInput = new HashMap<>(); + tagInput.put(KnownTags.HTTP_METHOD_OTEL_NAME, "GET"); + tagInput.put(KnownTags.URL_PATH_NAME, "/users/123/orders/456"); + TagMap tags = TagMap.fromMap(tagInput); + + processor.processTags(tags, mockContext, mockSpanLinks); + + verify(mockContext, never()).setResourceName(any(CharSequence.class), anyByte()); + assertEquals("/users/{param:int}/orders/{param:int}", tags.get(Tags.HTTP_ENDPOINT)); + } + @Test void shouldSkipNonHttpSpans() { EndpointResolver endpointResolver = new EndpointResolver(true, false); diff --git a/dd-trace-core/src/test/java/datadog/trace/core/tagprocessor/QueryObfuscatorTest.java b/dd-trace-core/src/test/java/datadog/trace/core/tagprocessor/QueryObfuscatorTest.java index 4e2d837b56b..a30ee794772 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/tagprocessor/QueryObfuscatorTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/tagprocessor/QueryObfuscatorTest.java @@ -3,12 +3,17 @@ import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import com.google.re2j.Pattern; import datadog.trace.api.DDTags; import datadog.trace.api.TagMap; import datadog.trace.bootstrap.instrumentation.api.Tags; +import datadog.trace.bootstrap.instrumentation.api.URIUtils; +import datadog.trace.core.DDSpanContext; import datadog.trace.test.util.DDJavaSpecification; +import java.net.URI; import java.util.LinkedHashMap; import java.util.Map; import org.junit.jupiter.api.Test; @@ -24,6 +29,21 @@ class QueryObfuscatorTest extends DDJavaSpecification { private static final String JWT = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0In0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U"; + @Test + void datadogQueryReconstructionPreservesDecodedPath() { + URI uri = URI.create("http://host/a%23b?q=1"); + TagMap tags = TagMap.create(); + tags.put( + Tags.HTTP_URL, + URIUtils.lazyValidURL(uri.getScheme(), uri.getHost(), uri.getPort(), uri.getPath())); + tags.put(DDTags.HTTP_QUERY, uri.getQuery()); + + new QueryObfuscator(null, false).processTags(tags, null, link -> {}); + + assertEquals("q=1", tags.get(DDTags.HTTP_QUERY)); + assertEquals("http://host/a#b?q=1", tags.get(Tags.HTTP_URL)); + } + @TableTest({ "scenario | query | expectedQuery ", "token | key1=val1&token=a0b21ce2-006f-4cc6-95d5-d7b550698482&key2=val2 | 'key1=val1&&key2=val2'", @@ -65,9 +85,13 @@ void tagsProcessingWithCustomRegexpForEmail(String query, String expectedQuery) } private static TagMap process(QueryObfuscator obfuscator, Object query) { + return process(obfuscator, query, DDTags.HTTP_QUERY); + } + + private static TagMap process(QueryObfuscator obfuscator, Object query, String queryTag) { Map tags = new LinkedHashMap<>(); tags.put(Tags.HTTP_URL, "http://site.com/index"); - tags.put(DDTags.HTTP_QUERY, query); + tags.put(queryTag, query); TagMap unsafeTags = TagMap.fromMap(tags); obfuscator.processTags(unsafeTags, null, link -> {}); @@ -130,16 +154,25 @@ void userCopyOfDefaultPatternAlsoRedactsDelimiter() { assertEquals("a=1&b=2", unsafeTags.getObject(DDTags.HTTP_QUERY)); } - @Test - void invalidRegexDropsQueryString() { - TagMap unsafeTags = process(new QueryObfuscator("(?<=a)b"), "pwd=secret"); + @TableTest({ + "Scenario | OTel semantics", + "Datadog | false ", + "OpenTelemetry | true " + }) + void invalidRegexDropsQueryString(boolean otelSemantics) { + TagMap unsafeTags = process(new QueryObfuscator("(?<=a)b", otelSemantics), "pwd=secret"); assertFalse(unsafeTags.containsKey(DDTags.HTTP_QUERY)); assertEquals("http://site.com/index", unsafeTags.getObject(Tags.HTTP_URL)); } - @Test - void obfuscationFailureDropsQueryString() { + @TableTest({ + "Scenario | OTel semantics | Query tag ", + "Datadog | false | http.query.string", + "OpenTelemetry | true | url.query ", + "Canonical OTel | true | http.query.string" + }) + void obfuscationFailureDropsQueryString(boolean otelSemantics, String queryTag) { CharSequence failing = new CharSequence() { @Override @@ -163,9 +196,11 @@ public String toString() { } }; - TagMap unsafeTags = assertDoesNotThrow(() -> process(new QueryObfuscator(null), failing)); + TagMap unsafeTags = + assertDoesNotThrow( + () -> process(new QueryObfuscator(null, otelSemantics), failing, queryTag)); - assertFalse(unsafeTags.containsKey(DDTags.HTTP_QUERY)); + assertFalse(unsafeTags.containsKey(queryTag)); assertEquals("http://site.com/index", unsafeTags.getObject(Tags.HTTP_URL)); } @@ -178,4 +213,28 @@ void emptyRegexKeepsObfuscationDisabled() { assertEquals(query, unsafeTags.getObject(DDTags.HTTP_QUERY)); assertEquals("http://site.com/index?" + query, unsafeTags.getObject(Tags.HTTP_URL)); } + + @TableTest({ + "Scenario | OTel semantics | Span type | Retain query", + "Datadog server | false | web | true ", + "Datadog client | false | http | true ", + "OTel server | true | web | true ", + "OTel client | true | http | false ", + "No span type | true | | true " + }) + void queryIsRetainedUnlessItOnlyBuildsOtelClientUrlFull( + boolean otelSemantics, String spanType, boolean retainQuery) { + QueryObfuscator obfuscator = new QueryObfuscator(null, otelSemantics); + DDSpanContext spanContext = mock(DDSpanContext.class); + when(spanContext.getSpanType()).thenReturn(spanType); + + TagMap tags = TagMap.create(); + tags.put(Tags.HTTP_URL, "http://site.com/index#fragment"); + tags.put(DDTags.HTTP_QUERY, "token=secret&view=full"); + + obfuscator.processTags(tags, spanContext, link -> {}); + + assertEquals("http://site.com/index?&view=full#fragment", tags.get(Tags.HTTP_URL)); + assertEquals(retainQuery ? "&view=full" : null, tags.get(DDTags.HTTP_QUERY)); + } } diff --git a/dd-trace-core/src/test/java/datadog/trace/lambda/LambdaAppSecHandlerTest.java b/dd-trace-core/src/test/java/datadog/trace/lambda/LambdaAppSecHandlerTest.java index f51ec9e4e78..0d255fe5f3d 100644 --- a/dd-trace-core/src/test/java/datadog/trace/lambda/LambdaAppSecHandlerTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/lambda/LambdaAppSecHandlerTest.java @@ -2562,7 +2562,8 @@ void keepsRepeatedQueryKeysFromTheV2RawQueryString() { // API Gateway comma-joins repeats into queryStringParameters, so rebuilding from that map // would report a=1%2C2 - assertEquals("a=1&a=2", tagsOf(context).get(DDTags.HTTP_QUERY)); + Map tags = tagsOf(context); + assertEquals("a=1&a=2", tags.get(DDTags.HTTP_QUERY)); } @Test diff --git a/dd-trace-core/src/test/java/datadog/trace/lambda/LambdaHttpOtelSemanticsForkedTest.java b/dd-trace-core/src/test/java/datadog/trace/lambda/LambdaHttpOtelSemanticsForkedTest.java new file mode 100644 index 00000000000..f1783d1d4b7 --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/lambda/LambdaHttpOtelSemanticsForkedTest.java @@ -0,0 +1,96 @@ +package datadog.trace.lambda; + +import static datadog.trace.api.config.GeneralConfig.TRACE_OTEL_SEMANTICS_ENABLED; +import static datadog.trace.api.config.TraceInstrumentationConfig.HTTP_SERVER_TAG_QUERY_STRING; +import static datadog.trace.api.config.TraceInstrumentationConfig.OBFUSCATION_QUERY_STRING_REGEXP; +import static datadog.trace.core.CoreSpan.OWN_PROPAGATION_TAGS; +import static datadog.trace.core.MetadataConsumer.NO_OP; +import static datadog.trace.lambda.LambdaEventParser.parseEvent; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import datadog.trace.api.DDSpanTypes; +import datadog.trace.api.DDTags; +import datadog.trace.bootstrap.instrumentation.api.TagContext; +import datadog.trace.bootstrap.instrumentation.api.Tags; +import datadog.trace.common.writer.ListWriter; +import datadog.trace.core.CoreTracer; +import datadog.trace.core.DDSpan; +import datadog.trace.core.tagprocessor.TagsPostProcessorFactory; +import datadog.trace.lambda.LambdaEventParser.LambdaRequestData; +import datadog.trace.test.junit.utils.config.WithConfig; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +@WithConfig(key = TRACE_OTEL_SEMANTICS_ENABLED, value = "true") +@WithConfig(key = HTTP_SERVER_TAG_QUERY_STRING, value = "true") +class LambdaHttpOtelSemanticsForkedTest { + private CoreTracer tracer; + + @BeforeEach + void setUp() { + // Rebuild the chain so each method's obfuscation configuration is used. + TagsPostProcessorFactory.reset(); + tracer = CoreTracer.builder().writer(new ListWriter()).build(); + } + + @AfterEach + void tearDown() { + tracer.close(); + } + + @Test + void lambdaQueryIsObfuscatedWithDefaultPattern() { + assertSanitizedLambdaQuery( + "https://api.example.com/orders?&view=full", "&view=full"); + } + + @Test + @WithConfig(key = OBFUSCATION_QUERY_STRING_REGEXP, value = "token=[^&]+") + void lambdaQueryIsObfuscatedWithCustomPattern() { + assertSanitizedLambdaQuery( + "https://api.example.com/orders?&view=full", "&view=full"); + } + + @Test + @WithConfig(key = OBFUSCATION_QUERY_STRING_REGEXP, value = "(?<=a)b") + void lambdaQueryIsDroppedWhenPatternCannotCompile() { + assertSanitizedLambdaQuery("https://api.example.com/orders", null); + } + + @Test + @WithConfig(key = HTTP_SERVER_TAG_QUERY_STRING, value = "false") + void lambdaOmitsQueryAttributesWhenCaptureIsDisabled() { + TagContext parent = lambdaHttpContext(); + + assertNull(parent.getTags().get(DDTags.HTTP_QUERY)); + assertEquals("https://api.example.com/orders", parent.getTags().get(Tags.HTTP_URL)); + } + + private void assertSanitizedLambdaQuery(String expectedUrl, String expectedQuery) { + TagContext parent = lambdaHttpContext(); + assertEquals("token=secret&view=full", parent.getTags().get(DDTags.HTTP_QUERY)); + DDSpan span = (DDSpan) tracer.startSpan("test", "aws.lambda", parent); + span.setSpanType(DDSpanTypes.HTTP_SERVER); + span.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_SERVER); + span.processTagsAndBaggage(NO_OP, OWN_PROPAGATION_TAGS); + + assertEquals(expectedQuery, span.getTag(DDTags.HTTP_QUERY)); + assertEquals(expectedUrl, span.getTag(Tags.HTTP_URL).toString()); + } + + private TagContext lambdaHttpContext() { + LambdaRequestData request = + parseEvent( + "{\"version\":\"2.0\",\"rawPath\":\"/orders\"," + + "\"rawQueryString\":\"token=secret&view=full\"," + + "\"headers\":{\"host\":\"api.example.com\"}," + + "\"requestContext\":{\"domainName\":\"api.example.com\"," + + "\"http\":{\"method\":\"GET\",\"path\":\"/orders\"}}}"); + TagContext parent = new TagContext(); + LambdaAppSecHandler.applyHttpTags( + parent, request, new LambdaURIDataAdapter(request.rawUri, request.headers, request.host)); + return parent; + } +} diff --git a/internal-api/src/main/java/datadog/trace/api/Config.java b/internal-api/src/main/java/datadog/trace/api/Config.java index 3d81035b6a8..0306b9b8f66 100644 --- a/internal-api/src/main/java/datadog/trace/api/Config.java +++ b/internal-api/src/main/java/datadog/trace/api/Config.java @@ -129,6 +129,7 @@ import static datadog.trace.api.ConfigDefaults.DEFAULT_METRICS_OTEL_EXPERIMENTAL_ENABLED; import static datadog.trace.api.ConfigDefaults.DEFAULT_METRICS_OTEL_INTERVAL; import static datadog.trace.api.ConfigDefaults.DEFAULT_METRICS_OTEL_TIMEOUT; +import static datadog.trace.api.ConfigDefaults.DEFAULT_OTEL_HTTP_CLIENT_ERROR_STATUSES; import static datadog.trace.api.ConfigDefaults.DEFAULT_OTLP_GRPC_PORT; import static datadog.trace.api.ConfigDefaults.DEFAULT_OTLP_HTTP_LOGS_ENDPOINT; import static datadog.trace.api.ConfigDefaults.DEFAULT_OTLP_HTTP_METRICS_ENDPOINT; @@ -188,6 +189,7 @@ import static datadog.trace.api.ConfigDefaults.DEFAULT_TRACE_LONG_RUNNING_ENABLED; import static datadog.trace.api.ConfigDefaults.DEFAULT_TRACE_LONG_RUNNING_FLUSH_INTERVAL; import static datadog.trace.api.ConfigDefaults.DEFAULT_TRACE_LONG_RUNNING_INITIAL_FLUSH_INTERVAL; +import static datadog.trace.api.ConfigDefaults.DEFAULT_TRACE_OTEL_SEMANTICS_ENABLED; import static datadog.trace.api.ConfigDefaults.DEFAULT_TRACE_POST_PROCESSING_TIMEOUT; import static datadog.trace.api.ConfigDefaults.DEFAULT_TRACE_PROPAGATION_BEHAVIOR_EXTRACT; import static datadog.trace.api.ConfigDefaults.DEFAULT_TRACE_PROPAGATION_EXTRACT_FIRST; @@ -1560,11 +1562,33 @@ private Config(final ConfigProvider configProvider, final InstrumenterConfig ins integrationSynapseLegacyOperationName = configProvider.getBoolean(INTEGRATION_SYNAPSE_LEGACY_OPERATION_NAME, false); - traceOtelExporter = configProvider.getString(TRACE_OTEL_EXPORTER); + traceOtelSemanticsEnabled = + configProvider.getBoolean( + TRACE_OTEL_SEMANTICS_ENABLED, DEFAULT_TRACE_OTEL_SEMANTICS_ENABLED); + String configuredTraceOtelExporter = configProvider.getString(TRACE_OTEL_EXPORTER); + if (traceOtelSemanticsEnabled) { + if (configuredTraceOtelExporter != null + && !"otlp".equalsIgnoreCase(configuredTraceOtelExporter)) { + logOverriddenSettingWarning(TRACE_OTEL_EXPORTER, TRACE_OTEL_SEMANTICS_ENABLED, "otlp"); + } + traceOtelExporter = "otlp"; + reportOtelSemanticsOverride(TRACE_OTEL_EXPORTER, traceOtelExporter); + } else { + traceOtelExporter = configuredTraceOtelExporter; + } boolean otlpTracesExporter = isTraceOtlpExporterEnabled(); - writerType = + String configuredWriterType = configProvider.getString( WRITER_TYPE, otlpTracesExporter ? OTLP_WRITER_TYPE : DEFAULT_AGENT_WRITER_TYPE); + if (traceOtelSemanticsEnabled) { + if (!OTLP_WRITER_TYPE.equals(configuredWriterType)) { + logOverriddenSettingWarning(WRITER_TYPE, TRACE_OTEL_SEMANTICS_ENABLED, OTLP_WRITER_TYPE); + } + writerType = OTLP_WRITER_TYPE; + reportOtelSemanticsOverride(WRITER_TYPE, writerType); + } else { + writerType = configuredWriterType; + } boolean isDatadogTraceWriter = !otlpTracesExporter; injectBaggageAsTagsEnabled = configProvider.getBoolean(WRITER_BAGGAGE_INJECT, isDatadogTraceWriter); @@ -1777,14 +1801,32 @@ private Config(final ConfigProvider configProvider, final InstrumenterConfig ins sqsInjectDatadogAttributeEnabled = isInjectDatadogAttributeEnabled(DEFAULT_INJECT_DATADOG_ATTRIBUTE, "sqs"); - spanAttributeSchemaVersion = schemaVersionFromConfig(); + int configuredSpanAttributeSchemaVersion = schemaVersionFromConfig(); + if (traceOtelSemanticsEnabled) { + if (configuredSpanAttributeSchemaVersion != SpanNaming.SCHEMA_MIN_VERSION) { + logOtelSemanticsOverride(TRACE_SPAN_ATTRIBUTE_SCHEMA, "v" + SpanNaming.SCHEMA_MIN_VERSION); + } + spanAttributeSchemaVersion = SpanNaming.SCHEMA_MIN_VERSION; + reportOtelSemanticsOverride(TRACE_SPAN_ATTRIBUTE_SCHEMA, "v" + spanAttributeSchemaVersion); + } else { + spanAttributeSchemaVersion = configuredSpanAttributeSchemaVersion; + } peerHostNameEnabled = configProvider.getBoolean(TRACE_PEER_HOSTNAME_ENABLED, true); // following two only used in v0. // in v1+ defaults are always calculated regardless this feature flag - peerServiceDefaultsEnabled = + boolean configuredPeerServiceDefaultsEnabled = configProvider.getBoolean(TRACE_PEER_SERVICE_DEFAULTS_ENABLED, false); + if (traceOtelSemanticsEnabled) { + if (configuredPeerServiceDefaultsEnabled) { + logOtelSemanticsOverride(TRACE_PEER_SERVICE_DEFAULTS_ENABLED, false); + } + peerServiceDefaultsEnabled = false; + reportOtelSemanticsOverride(TRACE_PEER_SERVICE_DEFAULTS_ENABLED, false); + } else { + peerServiceDefaultsEnabled = configuredPeerServiceDefaultsEnabled; + } peerServiceComponentOverrides = configProvider.getMergedMap(TRACE_PEER_SERVICE_COMPONENT_OVERRIDES); // feature flag to remove fake services in v0 @@ -1816,7 +1858,9 @@ private Config(final ConfigProvider configProvider, final InstrumenterConfig ins httpClientErrorStatuses = configProvider.getIntegerRange( TRACE_HTTP_CLIENT_ERROR_STATUSES, - DEFAULT_HTTP_CLIENT_ERROR_STATUSES, + traceOtelSemanticsEnabled + ? DEFAULT_OTEL_HTTP_CLIENT_ERROR_STATUSES + : DEFAULT_HTTP_CLIENT_ERROR_STATUSES, HTTP_CLIENT_ERROR_STATUSES); httpServerTagQueryString = @@ -2213,7 +2257,6 @@ private Config(final ConfigProvider configProvider, final InstrumenterConfig ins OtlpConfig.Temporality.class, OtlpConfig.Temporality.DELTA); - traceOtelSemanticsEnabled = configProvider.getBoolean(TRACE_OTEL_SEMANTICS_ENABLED, false); // Tri-state default: when unset, SDK-computed OTLP span metrics are emitted iff OTLP trace // export and OTLP metrics export are both enabled. otelTracesSpanMetricsEnabled = @@ -6146,6 +6189,19 @@ private void logOverriddenSettingWarning(String setting, String overridingSettin value); } + private static void reportOtelSemanticsOverride(String setting, Object value) { + ConfigCollector.get().put(setting, value, ConfigOrigin.CALCULATED, Integer.MAX_VALUE); + } + + private static void logOtelSemanticsOverride(String setting, Object value) { + log.warn( + SEND_TELEMETRY, + "Enabling {} overrode {} to {}", + propertyNameToEnvironmentVariableName(TRACE_OTEL_SEMANTICS_ENABLED), + propertyNameToEnvironmentVariableName(setting), + value); + } + private void logOverriddenDeprecatedSettingWarning( String setting, String overridingSetting, Object value) { log.warn( diff --git a/internal-api/src/main/java/datadog/trace/api/gateway/InferredProxySpan.java b/internal-api/src/main/java/datadog/trace/api/gateway/InferredProxySpan.java index 3aa95465e68..2c19b0772e3 100644 --- a/internal-api/src/main/java/datadog/trace/api/gateway/InferredProxySpan.java +++ b/internal-api/src/main/java/datadog/trace/api/gateway/InferredProxySpan.java @@ -1,27 +1,27 @@ package datadog.trace.api.gateway; import static datadog.context.ContextKey.named; -import static datadog.trace.api.DDTags.SPAN_TYPE; import static datadog.trace.bootstrap.instrumentation.api.ErrorPriorities.HTTP_SERVER_DECORATOR; import static datadog.trace.bootstrap.instrumentation.api.ResourceNamePriorities.MANUAL_INSTRUMENTATION; -import static datadog.trace.bootstrap.instrumentation.api.Tags.COMPONENT; -import static datadog.trace.bootstrap.instrumentation.api.Tags.HTTP_METHOD; import static datadog.trace.bootstrap.instrumentation.api.Tags.HTTP_REQUEST_HEADERS_X_DATADOG_ENDPOINT_SCAN; import static datadog.trace.bootstrap.instrumentation.api.Tags.HTTP_REQUEST_HEADERS_X_DATADOG_SECURITY_TEST; -import static datadog.trace.bootstrap.instrumentation.api.Tags.HTTP_ROUTE; -import static datadog.trace.bootstrap.instrumentation.api.Tags.HTTP_URL; import static datadog.trace.bootstrap.instrumentation.api.Tags.HTTP_USER_AGENT; -import static datadog.trace.bootstrap.instrumentation.api.Tags.SPAN_KIND; import static datadog.trace.bootstrap.instrumentation.api.Tags.SPAN_KIND_SERVER; import datadog.context.Context; import datadog.context.ContextKey; import datadog.context.ImplicitContextKeyed; import datadog.trace.api.Config; +import datadog.trace.api.KnownTags; import datadog.trace.api.internal.VisibleForTesting; import datadog.trace.bootstrap.instrumentation.api.AgentSpan; import datadog.trace.bootstrap.instrumentation.api.AgentSpanContext; import datadog.trace.bootstrap.instrumentation.api.AgentTracer; +import datadog.trace.bootstrap.instrumentation.api.OtelHttpSemantics; +import datadog.trace.bootstrap.instrumentation.api.TagExtractor; +import datadog.trace.bootstrap.instrumentation.api.http.DatadogServerUrl; +import datadog.trace.bootstrap.instrumentation.api.http.HttpServerUrl; +import datadog.trace.bootstrap.instrumentation.api.http.OtelServerUrl; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -42,6 +42,9 @@ public class InferredProxySpan implements ImplicitContextKeyed { static final String PROXY_REGION = "x-dd-proxy-region"; static final Map SUPPORTED_PROXIES; static final String INSTRUMENTATION_NAME = "inferred_proxy"; + private static final boolean OTEL_SEMANTICS = Config.get().isTraceOtelSemanticsEnabled(); + private static final TagExtractor URL_TAGS = + OTEL_SEMANTICS ? OtelServerUrl.INSTANCE : DatadogServerUrl.URL_ONLY; static { SUPPORTED_PROXIES = new HashMap<>(); @@ -95,7 +98,6 @@ public AgentSpanContext start(AgentSpanContext extracted) { String path = header(PROXY_PATH); String resourcePath = header(PROXY_RESOURCE_PATH); String domainName = header(PROXY_DOMAIN_NAME); - AgentSpan span = AgentTracer.get().startSpan(INSTRUMENTATION_NAME, proxy, extracted, startTime); // Service: value of x-dd-proxy-domain-name or global config if not found @@ -104,24 +106,25 @@ public AgentSpanContext start(AgentSpanContext extracted) { span.setServiceName(serviceName, INSTRUMENTATION_NAME); // Component: aws-apigateway or aws-httpapi - span.setTag(COMPONENT, proxySystem); + span.setTag(KnownTags.COMPONENT_ID, proxySystem); // Span kind: server - span.setTag(SPAN_KIND, SPAN_KIND_SERVER); + span.setTag(KnownTags.SPAN_KIND_ID, SPAN_KIND_SERVER); // SpanType: web - span.setTag(SPAN_TYPE, "web"); - - // Http.method - value of x-dd-proxy-httpmethod - span.setTag(HTTP_METHOD, httpMethod); + span.setTag(KnownTags.SPAN_TYPE_ID, "web"); - // Http.url - https:// + x-dd-proxy-domain-name + x-dd-proxy-path - span.setTag( - HTTP_URL, - domainName != null && !domainName.isEmpty() ? "https://" + domainName + path : path); + if (OTEL_SEMANTICS) { + OtelHttpSemantics.setRequestMethod(span, httpMethod); + } else { + span.setTag(KnownTags.HTTP_METHOD_ID, httpMethod); + } + span.setTagsFrom(HttpServerUrl.fromInferredProxy(domainName, path), URL_TAGS); - // Http.route - value of x-dd-proxy-resource-path (or x-dd-proxy-path as fallback) - span.setTag(HTTP_ROUTE, resourcePath != null && !resourcePath.isEmpty() ? resourcePath : path); + // A concrete path is not a route under the OTel conventions. Keep the legacy fallback only + // while Datadog semantics are active. + String route = resourcePath != null && !resourcePath.isEmpty() ? resourcePath : null; + span.setTag(KnownTags.HTTP_ROUTE_ID, OTEL_SEMANTICS ? route : route != null ? route : path); // "stage" - value of x-dd-proxy-stage span.setTag("stage", header(STAGE)); @@ -153,12 +156,18 @@ public AgentSpanContext start(AgentSpanContext extracted) { // _dd.inferred_span = 1 (indicates that this is an inferred span) span.setTag("_dd.inferred_span", 1); - // Resource Name: when route available, else - // Prefer x-dd-proxy-resource-path (route) over x-dd-proxy-path (path) + // Resource Name: OTel uses or just ; Datadog keeps the historical + // fallback to when no route is available. // Use MANUAL_INSTRUMENTATION priority to prevent TagInterceptor from overriding - String routeOrPath = resourcePath != null && !resourcePath.isEmpty() ? resourcePath : path; - String resourceName = - httpMethod != null && routeOrPath != null ? httpMethod + " " + routeOrPath : null; + String resourceName; + if (OTEL_SEMANTICS) { + String spanNameMethod = OtelHttpSemantics.spanNameMethod(httpMethod); + resourceName = route != null ? spanNameMethod + " " + route : spanNameMethod; + } else { + String routeOrPath = route != null ? route : path; + resourceName = + httpMethod != null && routeOrPath != null ? httpMethod + " " + routeOrPath : null; + } if (resourceName != null) { span.setResourceName(resourceName, MANUAL_INSTRUMENTATION); } @@ -290,11 +299,14 @@ private void copyTagsFromServiceEntry(AgentSpan serviceEntrySpan) { this.span.setHttpStatusCode(statusCode); boolean isError = Config.get().getHttpServerErrorStatuses().get(statusCode); this.span.setError(isError, HTTP_SERVER_DECORATOR); + if (OTEL_SEMANTICS && isError) { + OtelHttpSemantics.setErrorType(this.span, statusCode); + } } Object userAgent = serviceEntrySpan.getTag(HTTP_USER_AGENT); if (userAgent != null) { - this.span.setTag(HTTP_USER_AGENT, userAgent.toString()); + this.span.setTag(KnownTags.HTTP_USERAGENT_ID, userAgent.toString()); } // Forward the Datadog scan/test markers so the API endpoint reducer can keep diff --git a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/OtelHttpSemantics.java b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/OtelHttpSemantics.java new file mode 100644 index 00000000000..9e8a85ef0dc --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/OtelHttpSemantics.java @@ -0,0 +1,147 @@ +package datadog.trace.bootstrap.instrumentation.api; + +import static datadog.trace.util.IntStringUtils.REJECT_LEADING_PLUS; +import static datadog.trace.util.IntStringUtils.parseNonNegativeInt; + +import datadog.trace.api.DDTags; +import datadog.trace.api.KnownTags; +import datadog.trace.api.cache.RadixTreeCache; +import java.net.URI; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** Shared helpers for emitting OpenTelemetry HTTP semantic conventions. */ +public final class OtelHttpSemantics { + public static final String OTHER_METHOD = "_OTHER"; + + // RFC 9110, PATCH (RFC 5789), and QUERY (HTTPbis draft). Method names are case-sensitive. + private static final Set KNOWN_METHODS = + Collections.unmodifiableSet( + new HashSet<>( + Arrays.asList( + "CONNECT", "DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT", "QUERY", + "TRACE"))); + + private OtelHttpSemantics() {} + + public static void setRequestMethod(final AgentSpan span, final String method) { + if (method != null && !KNOWN_METHODS.contains(method)) { + span.setTag(KnownTags.HTTP_METHOD_ID, OTHER_METHOD); + span.setTag(KnownTags.HTTP_REQUEST_METHOD_ORIGINAL_ID, method); + } else { + span.setTag(KnownTags.HTTP_METHOD_ID, method); + } + } + + public static void setMethodResourceName(final AgentSpan span, final CharSequence method) { + span.setResourceName(spanNameMethod(method), ResourceNamePriorities.HTTP_PATH_NORMALIZER); + } + + public static String spanNameMethod(final CharSequence method) { + final String methodName = method == null ? null : method.toString(); + return KNOWN_METHODS.contains(methodName) ? methodName : "HTTP"; + } + + /** Returns {@code http.url}, falling back to {@code url.path}, without rendering the value. */ + public static Object urlOrPath(final Map tags) { + Object url = tags.get(Tags.HTTP_URL); + return url != null ? url : tags.get(KnownTags.URL_PATH_NAME); + } + + public static String redactedUrl(final URI url) { + final String full = url.toString(); + // URI does not parse user-info for some hosts, including underscores and Unicode names. + final String authority = url.getRawAuthority(); + if (authority == null) { + return full; + } + final int userInfoEnd = authority.lastIndexOf('@'); + if (userInfoEnd <= 0) { + return full; + } + final int colon = authority.indexOf(':'); + final String redacted = colon >= 0 && colon < userInfoEnd ? "REDACTED:REDACTED" : "REDACTED"; + final int userInfoStart = full.indexOf("//") + 2; + return full.substring(0, userInfoStart) + + redacted + + full.substring(userInfoStart + userInfoEnd); + } + + public static String withoutQuery(final String url) { + final int fragment = url.indexOf('#'); + final int query = url.indexOf('?'); + if (query < 0 || (fragment >= 0 && fragment < query)) { + return url; + } + if (fragment < 0) { + return url.substring(0, query); + } + return url.substring(0, query) + url.substring(fragment); + } + + public static void setErrorType(final AgentSpan span, final int status) { + if (span.getTag(DDTags.ERROR_TYPE) == null) { + span.setTag(KnownTags.ERROR_TYPE_ID, RadixTreeCache.HTTP_STATUSES.get(status).toString()); + } + } + + public static int serverPort(final URI url) { + return serverPort(url.getScheme(), url.getPort()); + } + + public static int serverPort(final String scheme, final int port) { + if (port > 0) { + return port; + } + if ("https".equalsIgnoreCase(scheme)) { + return 443; + } + if ("http".equalsIgnoreCase(scheme)) { + return 80; + } + return -1; + } + + public static String forwardedValue(final String value) { + if (value == null) { + return null; + } + final int comma = value.indexOf(','); + final String first = (comma >= 0 ? value.substring(0, comma) : value).trim(); + return first.isEmpty() ? null : first; + } + + /** Returns an explicit authority port, or -1 when absent or invalid. */ + public static int portFromHost(final String host) { + if (host == null) { + return -1; + } + final int colon = host.lastIndexOf(':'); + if (colon <= 0) { + return -1; + } + if (host.charAt(0) == '[') { + if (host.indexOf(']') != colon - 1) { + return -1; + } + } else if (host.indexOf(':') != colon) { + return -1; + } + return parsePort(host, colon + 1, host.length() - colon - 1); + } + + public static int parsePort(final String value) { + return value == null ? -1 : parsePort(value, 0, value.length()); + } + + private static int parsePort(final String value, final int start, final int length) { + if (length == 0 || length > 5) { + return -1; + } + final int port = parseNonNegativeInt(value, start, length, REJECT_LEADING_PLUS); + return port > 0 && port <= 65535 ? port : -1; + } +} diff --git a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/http/DatadogClientUrl.java b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/http/DatadogClientUrl.java new file mode 100644 index 00000000000..5b49066cda6 --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/http/DatadogClientUrl.java @@ -0,0 +1,27 @@ +package datadog.trace.bootstrap.instrumentation.api.http; + +import datadog.trace.api.Config; +import datadog.trace.api.DDTags; +import datadog.trace.api.KnownTags; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.bootstrap.instrumentation.api.TagExtractor; +import datadog.trace.bootstrap.instrumentation.api.URIUtils; +import java.net.URI; + +/** Extracts the Datadog URL tags from an HTTP client URI. */ +public final class DatadogClientUrl implements TagExtractor { + public static final DatadogClientUrl INSTANCE = new DatadogClientUrl(); + + private DatadogClientUrl() {} + + @Override + public void extract(URI uri, AgentSpan span) { + span.setTag( + KnownTags.HTTP_URL_ID, + URIUtils.lazyValidURL(uri.getScheme(), uri.getHost(), uri.getPort(), uri.getPath())); + if (Config.get().isHttpClientTagQueryString()) { + span.setTag(KnownTags.HTTP_QUERY_STRING_ID, uri.getQuery()); + span.setTag(DDTags.HTTP_FRAGMENT, uri.getFragment()); + } + } +} diff --git a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/http/DatadogServerUrl.java b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/http/DatadogServerUrl.java new file mode 100644 index 00000000000..b348db5658a --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/http/DatadogServerUrl.java @@ -0,0 +1,37 @@ +package datadog.trace.bootstrap.instrumentation.api.http; + +import datadog.trace.api.Config; +import datadog.trace.api.DDTags; +import datadog.trace.api.KnownTags; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.bootstrap.instrumentation.api.TagExtractor; + +/** Extracts the Datadog URL tags from a captured HTTP server URL. */ +public final class DatadogServerUrl implements TagExtractor { + public static final DatadogServerUrl INSTANCE = new DatadogServerUrl(false); + public static final DatadogServerUrl URL_ONLY = new DatadogServerUrl(true); + + private final boolean urlOnly; + + private DatadogServerUrl(boolean urlOnly) { + this.urlOnly = urlOnly; + } + + @Override + public void extract(HttpServerUrl url, AgentSpan span) { + CharSequence legacyUrl = url.datadogUrl(); + if (legacyUrl != null) { + span.setTag(KnownTags.HTTP_URL_ID, legacyUrl); + } + if (!urlOnly) { + String hostname = url.datadogHostname(); + if (hostname != null) { + span.setTag(KnownTags.HTTP_HOSTNAME_ID, hostname); + } + if (url.isValid() && Config.get().isHttpServerTagQueryString()) { + span.setTag(KnownTags.HTTP_QUERY_STRING_ID, url.query()); + span.setTag(DDTags.HTTP_FRAGMENT, url.fragment()); + } + } + } +} diff --git a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/http/HttpServerUrl.java b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/http/HttpServerUrl.java new file mode 100644 index 00000000000..a31a0d891f9 --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/http/HttpServerUrl.java @@ -0,0 +1,280 @@ +package datadog.trace.bootstrap.instrumentation.api.http; + +import static datadog.trace.bootstrap.instrumentation.api.OtelHttpSemantics.forwardedValue; +import static datadog.trace.bootstrap.instrumentation.api.OtelHttpSemantics.parsePort; +import static datadog.trace.bootstrap.instrumentation.api.OtelHttpSemantics.portFromHost; +import static datadog.trace.bootstrap.instrumentation.api.OtelHttpSemantics.serverPort; +import static datadog.trace.bootstrap.instrumentation.api.URIUtils.decode; + +import datadog.trace.api.Config; +import datadog.trace.bootstrap.instrumentation.api.AgentSpanContext; +import datadog.trace.bootstrap.instrumentation.api.URIDataAdapter; +import datadog.trace.bootstrap.instrumentation.api.URIUtils; +import java.net.URI; + +/** Captured server URL data shared by the Datadog and OpenTelemetry extractors. */ +public abstract class HttpServerUrl { + private HttpServerUrl() {} + + public static HttpServerUrl fromRequest( + URIDataAdapter uri, String path, AgentSpanContext.Extracted extracted, String hostHeader) { + return new Request(uri, path, extracted, hostHeader); + } + + public static HttpServerUrl fromTransaction(String originalUrl, String host, String port) { + return new Transaction(originalUrl, host, port); + } + + public static HttpServerUrl fromInferredProxy(String domain, String path) { + return new InferredProxy(domain, path); + } + + abstract CharSequence datadogUrl(); + + String datadogHostname() { + return null; + } + + String fragment() { + return null; + } + + boolean isValid() { + return true; + } + + abstract String path(); + + abstract String query(); + + abstract Origin origin(); + + /** The effective origin, resolved independently of client-IP collection. */ + static final class Origin { + final String scheme; + final String address; + final int port; + + private Origin(String scheme, String address, int port) { + this.scheme = scheme; + this.address = address; + this.port = port; + } + + private static Origin resolve( + String scheme, String host, int requestPort, String forwardedHost, String forwardedPort) { + forwardedHost = forwardedValue(forwardedHost); + int hostPort = -1; + if (forwardedHost != null) { + host = forwardedHost; + if (forwardedHost.charAt(0) == '[') { + int closingBracket = forwardedHost.indexOf(']'); + if (closingBracket > 0) { + host = forwardedHost.substring(1, closingBracket); + if (closingBracket + 1 < forwardedHost.length() + && forwardedHost.charAt(closingBracket + 1) == ':') { + hostPort = parsePort(forwardedHost.substring(closingBracket + 2)); + } + } + } else { + int colon = forwardedHost.lastIndexOf(':'); + if (colon > 0 && colon == forwardedHost.indexOf(':')) { + host = forwardedHost.substring(0, colon); + hostPort = parsePort(forwardedHost.substring(colon + 1)); + } + } + } + int port = parsePort(forwardedValue(forwardedPort)); + if (port <= 0) { + port = hostPort; + } + if (port <= 0) { + port = forwardedHost == null && requestPort > 0 ? requestPort : serverPort(scheme, -1); + } + return new Origin(scheme, host, port); + } + } + + private static final class Request extends HttpServerUrl { + private final URIDataAdapter uri; + private final String path; + private final AgentSpanContext.Extracted extracted; + private final String hostHeader; + + private Request( + URIDataAdapter uri, String path, AgentSpanContext.Extracted extracted, String hostHeader) { + this.uri = uri; + this.path = path; + this.extracted = extracted; + this.hostHeader = hostHeader; + } + + @Override + CharSequence datadogUrl() { + if (uri.isValid()) { + return URIUtils.lazyValidURL(uri.scheme(), uri.host(), uri.port(), path); + } + return uri.supportsRaw() ? URIUtils.lazyInvalidUrl(uri.raw()) : null; + } + + @Override + String datadogHostname() { + String forwardedHost = extracted == null ? null : extracted.getXForwardedHost(); + return forwardedHost != null ? forwardedHost : uri.host(); + } + + @Override + boolean isValid() { + return uri.isValid(); + } + + @Override + String path() { + if (uri.isValid()) { + return path == null || path.isEmpty() ? "/" : path.charAt(0) == '/' ? path : "/" + path; + } + String raw = rawWithoutFragment(); + int queryStart = raw == null ? -1 : raw.indexOf('?'); + return queryStart < 0 ? raw : raw.substring(0, queryStart); + } + + @Override + String query() { + if (uri.isValid()) { + return uri.supportsRaw() && Config.get().isHttpServerRawQueryString() + ? uri.rawQuery() + : uri.query(); + } + String raw = rawWithoutFragment(); + int queryStart = raw == null ? -1 : raw.indexOf('?'); + if (queryStart < 0) { + return null; + } + String query = raw.substring(queryStart + 1); + return Config.get().isHttpServerRawQueryString() + ? query + : decode(query, uri.hasPlusEncodedSpaces()); + } + + private String rawWithoutFragment() { + String raw = uri.supportsRaw() ? uri.raw() : null; + int fragmentStart = raw == null ? -1 : raw.indexOf('#'); + return fragmentStart < 0 ? raw : raw.substring(0, fragmentStart); + } + + @Override + String fragment() { + return uri.fragment(); + } + + @Override + Origin origin() { + String scheme = forwardedValue(extracted == null ? null : extracted.getXForwardedProto()); + if (scheme == null) { + scheme = uri.scheme(); + } + int port = uri.port(); + if (scheme != null && !scheme.equalsIgnoreCase(uri.scheme())) { + port = portFromHost(hostHeader); + } + return Origin.resolve( + scheme, + uri.host(), + port, + extracted == null ? null : extracted.getXForwardedHost(), + extracted == null ? null : extracted.getXForwardedPort()); + } + } + + private static final class InferredProxy extends HttpServerUrl { + private final String domain; + private final String path; + + private InferredProxy(String domain, String path) { + this.domain = domain != null && !domain.isEmpty() ? domain : null; + this.path = path; + } + + @Override + CharSequence datadogUrl() { + return domain != null ? "https://" + domain + path : path; + } + + @Override + String path() { + return path; + } + + @Override + String query() { + return null; + } + + @Override + Origin origin() { + return new Origin(domain != null ? "https" : null, domain, domain != null ? 443 : -1); + } + } + + private static final class Transaction extends HttpServerUrl { + private final String originalUrl; + private final String host; + private final String port; + private boolean parsed; + private URI uri; + + private Transaction(String originalUrl, String host, String port) { + this.originalUrl = originalUrl; + this.host = host; + this.port = port; + } + + private URI uri() { + if (!parsed) { + if (originalUrl != null) { + try { + uri = URI.create(originalUrl); + } catch (IllegalArgumentException ignored) { + // Keep the separately captured host and port when the transaction URL is malformed. + } + } + parsed = true; + } + return uri; + } + + @Override + CharSequence datadogUrl() { + return originalUrl; + } + + @Override + String path() { + URI uri = uri(); + return uri == null ? null : uri.getPath(); + } + + @Override + String query() { + URI uri = uri(); + return uri == null ? null : uri.getRawQuery(); + } + + @Override + Origin origin() { + URI uri = uri(); + String scheme = uri == null ? null : uri.getScheme(); + String address = host; + int port = parsePort(this.port); + if (uri != null) { + if (address == null || address.isEmpty()) { + address = uri.getHost(); + } + if (port <= 0) { + port = uri.getPort(); + } + } + return new Origin(scheme, address, serverPort(scheme, port)); + } + } +} diff --git a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/http/OtelClientUrl.java b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/http/OtelClientUrl.java new file mode 100644 index 00000000000..34d0ae5def0 --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/http/OtelClientUrl.java @@ -0,0 +1,50 @@ +package datadog.trace.bootstrap.instrumentation.api.http; + +import datadog.trace.api.Config; +import datadog.trace.api.KnownTags; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.bootstrap.instrumentation.api.OtelHttpSemantics; +import datadog.trace.bootstrap.instrumentation.api.TagExtractor; +import datadog.trace.bootstrap.instrumentation.api.URIUtils; +import java.net.URI; + +/** Extracts the OpenTelemetry URL tags from an HTTP client URI. */ +public final class OtelClientUrl implements TagExtractor { + public static final OtelClientUrl INSTANCE = new OtelClientUrl(); + + private OtelClientUrl() {} + + @Override + public void extract(URI uri, AgentSpan span) { + // The Object overload avoids calling length(), which would render the lazy URL immediately. + span.setTag(KnownTags.HTTP_URL_ID, (Object) new ClientUrl(uri)); + if (Config.get().isHttpClientTagQueryString()) { + // The obfuscator matches decoded parameter names, including names such as t%6fken. + String query = uri.getQuery(); + if (query != null) { + span.setTag(KnownTags.HTTP_QUERY_STRING_ID, query); + } + } + } + + private static final class ClientUrl extends URIUtils.LazyUrl { + private final URI uri; + + private ClientUrl(URI uri) { + super(null); + this.uri = uri; + } + + @Override + public String path() { + return uri.getPath(); + } + + @Override + public String get() { + return lazy != null + ? lazy + : OtelHttpSemantics.withoutQuery(OtelHttpSemantics.redactedUrl(uri)); + } + } +} diff --git a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/http/OtelServerUrl.java b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/http/OtelServerUrl.java new file mode 100644 index 00000000000..61f0fb6a0f8 --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/http/OtelServerUrl.java @@ -0,0 +1,37 @@ +package datadog.trace.bootstrap.instrumentation.api.http; + +import datadog.trace.api.Config; +import datadog.trace.api.KnownTags; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.bootstrap.instrumentation.api.TagExtractor; + +/** Extracts the OpenTelemetry URL tags from a captured HTTP server URL. */ +public final class OtelServerUrl implements TagExtractor { + public static final OtelServerUrl INSTANCE = new OtelServerUrl(); + + private OtelServerUrl() {} + + @Override + public void extract(HttpServerUrl url, AgentSpan span) { + HttpServerUrl.Origin origin = url.origin(); + if (url.isValid() && origin.scheme != null) { + span.setTag(KnownTags.URL_SCHEME_ID, origin.scheme); + } + String path = url.path(); + if (path != null) { + span.setTag(KnownTags.URL_PATH_ID, path); + } + if (origin.address != null && !origin.address.isEmpty()) { + span.setTag(KnownTags.Inbound.SERVER_ADDRESS_ID, origin.address); + } + if (origin.port > 0) { + span.setTag(KnownTags.Inbound.SERVER_PORT_ID, origin.port); + } + if (Config.get().isHttpServerTagQueryString()) { + String query = url.query(); + if (query != null && !query.isEmpty()) { + span.setTag(KnownTags.HTTP_QUERY_STRING_ID, query); + } + } + } +} diff --git a/internal-api/src/test/java/datadog/trace/api/ConfigTest.java b/internal-api/src/test/java/datadog/trace/api/ConfigTest.java index b82e1aa6769..675e110fa48 100644 --- a/internal-api/src/test/java/datadog/trace/api/ConfigTest.java +++ b/internal-api/src/test/java/datadog/trace/api/ConfigTest.java @@ -156,6 +156,7 @@ import static datadog.trace.api.config.TracerConfig.TRACE_LONG_RUNNING_ENABLED; import static datadog.trace.api.config.TracerConfig.TRACE_LONG_RUNNING_FLUSH_INTERVAL; import static datadog.trace.api.config.TracerConfig.TRACE_LONG_RUNNING_INITIAL_FLUSH_INTERVAL; +import static datadog.trace.api.config.TracerConfig.TRACE_PEER_SERVICE_DEFAULTS_ENABLED; import static datadog.trace.api.config.TracerConfig.TRACE_PROPAGATION_BEHAVIOR_EXTRACT; import static datadog.trace.api.config.TracerConfig.TRACE_PROPAGATION_EXTRACT_FIRST; import static datadog.trace.api.config.TracerConfig.TRACE_RATE_LIMIT; @@ -165,6 +166,7 @@ import static datadog.trace.api.config.TracerConfig.TRACE_SAMPLING_OPERATION_RULES; import static datadog.trace.api.config.TracerConfig.TRACE_SAMPLING_SERVICE_RULES; import static datadog.trace.api.config.TracerConfig.TRACE_SERVICE_DISCOVERY_ENABLED; +import static datadog.trace.api.config.TracerConfig.TRACE_SPAN_ATTRIBUTE_SCHEMA; import static datadog.trace.api.config.TracerConfig.TRACE_X_DATADOG_TAGS_MAX_LENGTH; import static datadog.trace.api.config.TracerConfig.WRITER_TYPE; import static datadog.trace.api.env.FixedCapturedEnvironment.useFixedEnv; @@ -699,6 +701,33 @@ void otelDefaultValuesWhenNotSet() { assertEquals(10000, config.getTraceStatsInterval()); } + @Test + void otelSemanticsForcesItsRequiredConfiguration() { + ConfigCollector.get().collect(); + Properties prop = new Properties(); + prop.setProperty(TRACE_OTEL_SEMANTICS_ENABLED, "true"); + prop.setProperty(TRACE_OTEL_EXPORTER, "agent"); + prop.setProperty(WRITER_TYPE, "LoggingWriter"); + prop.setProperty(TRACE_SPAN_ATTRIBUTE_SCHEMA, "v1"); + prop.setProperty(TRACE_PEER_SERVICE_DEFAULTS_ENABLED, "true"); + + Config config = Config.get(prop); + Map calculated = + ConfigCollector.get().collect().get(ConfigOrigin.CALCULATED); + + assertTrue(config.isTraceOtelSemanticsEnabled()); + assertTrue(config.isTraceOtlpExporterEnabled()); + assertEquals("OtlpWriter", config.getWriterType()); + assertEquals(0, config.getSpanAttributeSchemaVersion()); + assertFalse(config.isPeerServiceDefaultsEnabled()); + assertEquals(toBitSet(400, 599), config.getHttpClientErrorStatuses()); + + assertEquals("otlp", calculated.get(TRACE_OTEL_EXPORTER).value); + assertEquals("OtlpWriter", calculated.get(WRITER_TYPE).value); + assertEquals("v0", calculated.get(TRACE_SPAN_ATTRIBUTE_SCHEMA).value); + assertEquals(false, calculated.get(TRACE_PEER_SERVICE_DEFAULTS_ENABLED).value); + } + @Test void otelCheckSyntaxForOtlpHeaders() { Properties prop = new Properties(); @@ -1002,7 +1031,7 @@ void specifyOverridesViaSystemProperties() throws Exception { assertEquals("new site", config.getSite()); assertEquals("something else", config.getServiceName()); assertFalse(config.isTraceEnabled()); - assertEquals("LoggingWriter", config.getWriterType()); + assertEquals("OtlpWriter", config.getWriterType()); assertEquals("somehost", config.getAgentHost()); assertEquals(123, config.getAgentPort()); assertEquals("somepath", config.getAgentUnixDomainSocket()); diff --git a/internal-api/src/test/java/datadog/trace/api/KnownTagsTest.java b/internal-api/src/test/java/datadog/trace/api/KnownTagsTest.java index 29c7fa1ac17..594ec377a5e 100644 --- a/internal-api/src/test/java/datadog/trace/api/KnownTagsTest.java +++ b/internal-api/src/test/java/datadog/trace/api/KnownTagsTest.java @@ -41,8 +41,16 @@ static Stream knownTags() { Arguments.of(Tags.PEER_SERVICE, KnownTags.PEER_SERVICE_ID), Arguments.of(DDTags.PEER_SERVICE_REMAPPED_FROM, KnownTags.DD_PEER_SERVICE_REMAPPED_FROM_ID), Arguments.of(Tags.HTTP_METHOD, KnownTags.HTTP_METHOD_ID), + Arguments.of( + KnownTags.HTTP_REQUEST_METHOD_ORIGINAL_NAME, KnownTags.HTTP_REQUEST_METHOD_ORIGINAL_ID), Arguments.of(Tags.HTTP_ROUTE, KnownTags.HTTP_ROUTE_ID), Arguments.of(Tags.HTTP_URL, KnownTags.HTTP_URL_ID), + Arguments.of(Tags.HTTP_HOSTNAME, KnownTags.HTTP_HOSTNAME_ID), + Arguments.of(KnownTags.SERVER_PORT_NAME, KnownTags.SERVER_PORT_ID), + Arguments.of(KnownTags.URL_PATH_NAME, KnownTags.URL_PATH_ID), + Arguments.of(KnownTags.HTTP_QUERY_STRING_NAME, KnownTags.HTTP_QUERY_STRING_ID), + Arguments.of(KnownTags.URL_SCHEME_NAME, KnownTags.URL_SCHEME_ID), + Arguments.of(KnownTags.NETWORK_CLIENT_IP_NAME, KnownTags.NETWORK_CLIENT_IP_ID), Arguments.of(Tags.PEER_HOSTNAME, KnownTags.PEER_HOSTNAME_ID), Arguments.of(Tags.PEER_HOST_IPV4, KnownTags.PEER_IPV4_ID), Arguments.of(Tags.PEER_HOST_IPV6, KnownTags.PEER_IPV6_ID), @@ -66,6 +74,7 @@ static Stream otelNamedTags() { Arguments.of( "http.response.status_code", KnownTags.HTTP_STATUS_CODE_ID, "http.status_code"), Arguments.of("url.full", KnownTags.HTTP_URL_ID, "http.url"), + Arguments.of("url.query", KnownTags.HTTP_QUERY_STRING_ID, "http.query.string"), Arguments.of("user_agent.original", KnownTags.HTTP_USERAGENT_ID, "http.useragent"), Arguments.of("client.address", KnownTags.HTTP_CLIENT_IP_ID, "http.client_ip"), Arguments.of("db.system", KnownTags.DB_TYPE_ID, "db.type"), @@ -163,6 +172,28 @@ void unknownNamesResolveToZero() { assertEquals(0L, KnownTagCodec.keyOf("")); } + @Test + void networkPeerAddressIsNotADirectionFreeAliasForClientIp() { + assertEquals(0L, KnownTagCodec.keyOf("network.peer.address")); + assertNull(KnownTagCodec.openTelemetryNameOf(KnownTags.NETWORK_CLIENT_IP_ID)); + } + + @TableTest({ + "Scenario | Kind | Expected Name ", + "server | server | network.peer.address", + "consumer | consumer | network.peer.address", + "client | client | network.client.ip ", + "producer | producer | network.client.ip ", + "internal | internal | network.client.ip ", + "unknown kind | | network.client.ip " + }) + void networkClientIpIsRenamedOnlyOnInboundSpans(String kind, String expectedName) { + assertEquals( + expectedName, + KnownTagCodec.openTelemetryTagOf( + KnownTags.NETWORK_CLIENT_IP_ID, KnownTagCodec.directionOfKind(kind))); + } + /** * {@code peer.port} is declared once per direction, but it is one tag with one id: only its * OpenTelemetry name depends on the span's direction ({@code client.port} inbound, {@code diff --git a/internal-api/src/test/java/datadog/trace/api/gateway/InferredProxySpanTests.java b/internal-api/src/test/java/datadog/trace/api/gateway/InferredProxySpanTests.java index 309e8616080..2973fcd62d8 100644 --- a/internal-api/src/test/java/datadog/trace/api/gateway/InferredProxySpanTests.java +++ b/internal-api/src/test/java/datadog/trace/api/gateway/InferredProxySpanTests.java @@ -23,6 +23,7 @@ import static org.mockito.Mockito.when; import datadog.context.Context; +import datadog.trace.api.KnownTags; import datadog.trace.bootstrap.instrumentation.api.AgentSpan; import java.lang.reflect.Field; import java.util.HashMap; @@ -710,7 +711,7 @@ void testFinishPhasesChildCallAndPublishesOnServiceEntry() throws Exception { verify(inferredSpan).setHttpStatusCode(503); verify(inferredSpan).setError(true, HTTP_SERVER_DECORATOR); - verify(inferredSpan).setTag(HTTP_USER_AGENT, "curl/8.0"); + verify(inferredSpan).setTag(KnownTags.HTTP_USERAGENT_ID, "curl/8.0"); verify(inferredSpan).setTag(HTTP_REQUEST_HEADERS_X_DATADOG_ENDPOINT_SCAN, "scan-uuid"); verify(inferredSpan).setTag(HTTP_REQUEST_HEADERS_X_DATADOG_SECURITY_TEST, "test-uuid"); verify(inferredSpan).publish(); diff --git a/internal-api/src/test/java/datadog/trace/bootstrap/instrumentation/api/HttpUrlExtractorsTest.java b/internal-api/src/test/java/datadog/trace/bootstrap/instrumentation/api/HttpUrlExtractorsTest.java new file mode 100644 index 00000000000..b2cd5bea52a --- /dev/null +++ b/internal-api/src/test/java/datadog/trace/bootstrap/instrumentation/api/HttpUrlExtractorsTest.java @@ -0,0 +1,271 @@ +package datadog.trace.bootstrap.instrumentation.api; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import datadog.trace.api.DDTags; +import datadog.trace.api.KnownTags; +import datadog.trace.bootstrap.instrumentation.api.URIUtils.LazyUrl; +import datadog.trace.bootstrap.instrumentation.api.http.DatadogClientUrl; +import datadog.trace.bootstrap.instrumentation.api.http.DatadogServerUrl; +import datadog.trace.bootstrap.instrumentation.api.http.HttpServerUrl; +import datadog.trace.bootstrap.instrumentation.api.http.OtelClientUrl; +import datadog.trace.bootstrap.instrumentation.api.http.OtelServerUrl; +import datadog.trace.test.junit.utils.config.WithConfig; +import java.net.URI; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.mockito.ArgumentCaptor; +import org.tabletest.junit.TableTest; + +@WithConfig(key = "http.client.tag.query-string", value = "true") +@WithConfig(key = "http.server.tag.query-string", value = "true") +class HttpUrlExtractorsTest { + private final AgentSpan span = mock(AgentSpan.class); + + @Test + void otelClientLazilyStripsQueryAndRedactsCredentials() { + ArgumentCaptor urlCaptor = ArgumentCaptor.forClass(LazyUrl.class); + + OtelClientUrl.INSTANCE.extract(URI.create("https://u:p@host/a?token=secret#fragment"), span); + + verify(span).setTag(eq(KnownTags.HTTP_URL_ID), (Object) urlCaptor.capture()); + LazyUrl url = urlCaptor.getValue(); + + // Reading the path must leave the full URL unrendered. + assertEquals("/a", url.path()); + assertNull(url.lazy); + assertEquals("https://REDACTED:REDACTED@host/a#fragment", url.toString()); + assertEquals("https://REDACTED:REDACTED@host/a#fragment", url.get()); + verify(span).setTag(KnownTags.HTTP_QUERY_STRING_ID, "token=secret"); + } + + @TableTest({ + "Scenario | Raw URL | Expected query ", + "Plain name | http://host/a?token=secret | token=secret ", + "Encoded name | http://host/a?t%6fken=secret | token=secret ", + "Leading encoded name | http://host/a?%74oken=secret | token=secret ", + "Encoded name and value | http://host/a?t%6fken=s%65cret | token=secret ", + "Encoded space in value | http://host/a?q=a%20b | q=a b ", + "Multiple parameters | http://host/a?view=full&t%6fken=secret | view=full&token=secret" + }) + void otelClientDecodesQuerySoTheObfuscatorCanMatchEncodedNames( + String rawUrl, String expectedQuery) { + OtelClientUrl.INSTANCE.extract(URI.create(rawUrl), span); + + // The obfuscator matches literal parameter names, so the query it receives must be decoded. + verify(span).setTag(KnownTags.HTTP_QUERY_STRING_ID, expectedQuery); + } + + @Test + void otelClientOmitsQueryTagWhenAbsent() { + OtelClientUrl.INSTANCE.extract(URI.create("http://host/a"), span); + + verify(span, never()).setTag(eq(KnownTags.HTTP_QUERY_STRING_ID), anyString()); + } + + @Test + void extractServerUrlUsesRequestHostAndPort() { + extractServerUrl(null, null, "http", "host", 8080); + + verify(span).setTag(KnownTags.HTTP_HOSTNAME_ID, "host"); + verify(span).setTag(KnownTags.Inbound.SERVER_PORT_ID, 8080); + } + + @Test + void extractServerUrlFallsBackToSchemeDefaultPort() { + extractServerUrl(null, null, "https", "host", -1); + + verify(span).setTag(KnownTags.HTTP_HOSTNAME_ID, "host"); + verify(span).setTag(KnownTags.Inbound.SERVER_PORT_ID, 443); + } + + @Test + void extractServerUrlOmitsTagsWhenNothingResolvable() { + extractServerUrl(null, null, "ftp", null, -1); + + verify(span, never()).setTag(eq(KnownTags.HTTP_HOSTNAME_ID), anyString()); + verify(span, never()).setTag(eq(KnownTags.Inbound.SERVER_PORT_ID), anyInt()); + } + + @Test + void extractServerUrlOmitsAddressWhenRequestHostIsEmpty() { + extractServerUrl(null, null, "http", "", -1); + + verify(span, never()).setTag(eq(KnownTags.HTTP_HOSTNAME_ID), anyString()); + verify(span).setTag(KnownTags.Inbound.SERVER_PORT_ID, 80); + } + + @Test + void extractServerUrlPrefersForwardedPortHeader() { + extractServerUrl("proxy:8443", "9443", "http", "host", 8080); + + verify(span).setTag(KnownTags.HTTP_HOSTNAME_ID, "proxy"); + verify(span).setTag(KnownTags.Inbound.SERVER_PORT_ID, 9443); + } + + @TableTest({ + "Scenario | Forwarded Host | Forwarded Port | Expected Port", + "Host port | proxy:8443 | bogus | 8443 ", + "Excess header digits | proxy:8443 | 000080 | 8443 ", + "Scheme default | proxy | bogus | 80 ", + "Request port | | bogus | 8080 " + }) + void extractServerUrlFallsBackFromInvalidForwardedPort( + String forwardedHost, String forwardedPort, int expectedPort) { + extractServerUrl(forwardedHost, forwardedPort, "http", "host", 8080); + + verify(span).setTag(KnownTags.Inbound.SERVER_PORT_ID, expectedPort); + } + + @TableTest({ + "Scenario | Forwarded Host | Expected Address | Expected Port", + "Host only | proxy | proxy | 80 ", + "Host with port | proxy:8443 | proxy | 8443 ", + "Host with invalid port | proxy:bogus | proxy | 80 ", + "IPv6 host | '[::1]' | ::1 | 80 ", + "IPv6 with port | '[::1]:8443' | ::1 | 8443 ", + "IPv6 with invalid suffix | '[::1]bogus' | ::1 | 80 ", + "Unbracketed IPv6 | ::1 | ::1 | 80 ", + "Unclosed IPv6 bracket | '[::1' | '[::1' | 80 " + }) + void extractServerUrlParsesForwardedHost( + String forwardedHost, String expectedAddress, int expectedPort) { + extractServerUrl(forwardedHost, null, "http", "host", 8080); + + verify(span).setTag(KnownTags.HTTP_HOSTNAME_ID, expectedAddress); + // A forwarded host suppresses the request port, so the scheme default applies instead. + verify(span).setTag(KnownTags.Inbound.SERVER_PORT_ID, expectedPort); + } + + @Test + void datadogClientKeepsNormalizedUrlAndSeparateQueryAndFragment() { + DatadogClientUrl.INSTANCE.extract(URI.create("https://u:p@host:443/a?q=1#fragment"), span); + + ArgumentCaptor url = ArgumentCaptor.forClass(CharSequence.class); + verify(span).setTag(eq(KnownTags.HTTP_URL_ID), url.capture()); + assertEquals("https://host/a", url.getValue().toString()); + verify(span).setTag(KnownTags.HTTP_QUERY_STRING_ID, "q=1"); + verify(span).setTag(DDTags.HTTP_FRAGMENT, "fragment"); + } + + @Test + @WithConfig(key = "http.client.tag.query-string", value = "false") + void otelClientRespectsQueryOptOut() { + OtelClientUrl.INSTANCE.extract(URI.create("http://host/a?token=secret"), span); + + verify(span, never()).setTag(eq(KnownTags.HTTP_QUERY_STRING_ID), anyString()); + } + + @Test + void datadogServerKeepsBackendUrlAndOriginalForwardedHostname() { + URI uri = URI.create("http://internal:8080/a?q=1#fragment"); + AgentSpanContext.Extracted extracted = mock(AgentSpanContext.Extracted.class); + when(extracted.getXForwardedHost()).thenReturn("public:8443, proxy"); + when(extracted.getXForwardedProto()).thenReturn("https"); + + DatadogServerUrl.INSTANCE.extract( + HttpServerUrl.fromRequest(new URIDefaultDataAdapter(uri), "/a", extracted, null), span); + + ArgumentCaptor url = ArgumentCaptor.forClass(CharSequence.class); + verify(span).setTag(eq(KnownTags.HTTP_URL_ID), url.capture()); + assertEquals("http://internal:8080/a", url.getValue().toString()); + verify(span).setTag(KnownTags.HTTP_HOSTNAME_ID, "public:8443, proxy"); + verify(span).setTag(KnownTags.HTTP_QUERY_STRING_ID, "q=1"); + verify(span).setTag(DDTags.HTTP_FRAGMENT, "fragment"); + verify(extracted, never()).getXForwardedProto(); + verify(extracted, never()).getXForwardedPort(); + } + + @TableTest({ + "Scenario | Original URL | Host | Port | Expected host | Expected port", + "State origin | https://url-host:8443/a?token=secret | state-host | 9443 | state-host | 9443 ", + "URL origin | https://url-host:8443/a?token=secret | | bad | url-host | 8443 ", + "Default port | https://url-host/a?token=secret | | | url-host | 443 ", + "Invalid URL | /bad%path | state-host | 9443 | state-host | 9443 " + }) + void transactionPreservesOriginalUrlAndResolvesOtelOrigin( + String originalUrl, String host, String port, String expectedHost, int expectedPort) { + HttpServerUrl url = HttpServerUrl.fromTransaction(originalUrl, host, port); + AgentSpan datadog = mock(AgentSpan.class); + + DatadogServerUrl.URL_ONLY.extract(url, datadog); + OtelServerUrl.INSTANCE.extract(url, span); + + verify(datadog).setTag(KnownTags.HTTP_URL_ID, (CharSequence) originalUrl); + verify(datadog, never()).setTag(eq(KnownTags.HTTP_HOSTNAME_ID), anyString()); + verify(span).setTag(KnownTags.Inbound.SERVER_ADDRESS_ID, expectedHost); + verify(span).setTag(KnownTags.Inbound.SERVER_PORT_ID, expectedPort); + } + + @Test + void transactionExtractsUrlComponents() { + OtelServerUrl.INSTANCE.extract( + HttpServerUrl.fromTransaction("https://host/a?token=secret", null, null), span); + + verify(span).setTag(KnownTags.URL_SCHEME_ID, "https"); + verify(span).setTag(KnownTags.URL_PATH_ID, "/a"); + verify(span).setTag(KnownTags.HTTP_QUERY_STRING_ID, "token=secret"); + } + + @Test + @WithConfig(key = "http.server.tag.query-string", value = "false") + void transactionRespectsQueryOptOut() { + OtelServerUrl.INSTANCE.extract( + HttpServerUrl.fromTransaction("https://host/a?token=secret", null, null), span); + + verify(span, never()).setTag(eq(KnownTags.HTTP_QUERY_STRING_ID), anyString()); + } + + @Test + void inferredProxySupportsBothUrlStyles() { + HttpServerUrl url = HttpServerUrl.fromInferredProxy("api.host", "/a"); + AgentSpan datadog = mock(AgentSpan.class); + + DatadogServerUrl.URL_ONLY.extract(url, datadog); + OtelServerUrl.INSTANCE.extract(url, span); + + verify(datadog).setTag(KnownTags.HTTP_URL_ID, (CharSequence) "https://api.host/a"); + verify(span).setTag(KnownTags.URL_PATH_ID, "/a"); + verify(span).setTag(KnownTags.URL_SCHEME_ID, "https"); + verify(span).setTag(KnownTags.Inbound.SERVER_ADDRESS_ID, "api.host"); + verify(span).setTag(KnownTags.Inbound.SERVER_PORT_ID, 443); + } + + @ParameterizedTest + @NullAndEmptySource + void inferredProxyOmitsUnknownOrigin(String domain) { + HttpServerUrl url = HttpServerUrl.fromInferredProxy(domain, "/a"); + AgentSpan datadog = mock(AgentSpan.class); + + DatadogServerUrl.URL_ONLY.extract(url, datadog); + OtelServerUrl.INSTANCE.extract(url, span); + + verify(datadog).setTag(KnownTags.HTTP_URL_ID, (CharSequence) "/a"); + verify(span).setTag(KnownTags.URL_PATH_ID, "/a"); + verify(span, never()).setTag(eq(KnownTags.URL_SCHEME_ID), anyString()); + verify(span, never()).setTag(eq(KnownTags.Inbound.SERVER_ADDRESS_ID), anyString()); + verify(span, never()).setTag(eq(KnownTags.Inbound.SERVER_PORT_ID), anyInt()); + } + + private void extractServerUrl( + String forwardedHost, String forwardedPort, String scheme, String host, int port) { + URIDataAdapter uri = mock(URIDataAdapter.class); + when(uri.isValid()).thenReturn(true); + when(uri.scheme()).thenReturn(scheme); + when(uri.host()).thenReturn(host); + when(uri.port()).thenReturn(port); + AgentSpanContext.Extracted extracted = mock(AgentSpanContext.Extracted.class); + when(extracted.getXForwardedHost()).thenReturn(forwardedHost); + when(extracted.getXForwardedPort()).thenReturn(forwardedPort); + OtelServerUrl.INSTANCE.extract(HttpServerUrl.fromRequest(uri, "/", extracted, null), span); + } +} diff --git a/internal-api/src/test/java/datadog/trace/bootstrap/instrumentation/api/OtelHttpSemanticsTest.java b/internal-api/src/test/java/datadog/trace/bootstrap/instrumentation/api/OtelHttpSemanticsTest.java new file mode 100644 index 00000000000..bfbf0a748fe --- /dev/null +++ b/internal-api/src/test/java/datadog/trace/bootstrap/instrumentation/api/OtelHttpSemanticsTest.java @@ -0,0 +1,267 @@ +package datadog.trace.bootstrap.instrumentation.api; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import datadog.trace.api.DDTags; +import datadog.trace.api.KnownTags; +import java.net.URI; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.tabletest.junit.TableTest; + +class OtelHttpSemanticsTest { + + @ParameterizedTest + @ValueSource(strings = {"GET", "POST", "DELETE", "PATCH", "QUERY", "CONNECT"}) + void setRequestMethodKeepsKnownMethods(String method) { + AgentSpan span = mock(AgentSpan.class); + + OtelHttpSemantics.setRequestMethod(span, method); + + verify(span).setTag(KnownTags.HTTP_METHOD_ID, method); + verify(span, never()).setTag(eq(KnownTags.HTTP_REQUEST_METHOD_ORIGINAL_ID), anyString()); + } + + @ParameterizedTest + @ValueSource(strings = {"PROPFIND", "get", "Get", "BOGUS"}) + void setRequestMethodNormalizesUnknownMethodsToOther(String method) { + AgentSpan span = mock(AgentSpan.class); + + OtelHttpSemantics.setRequestMethod(span, method); + + verify(span).setTag(KnownTags.HTTP_METHOD_ID, OtelHttpSemantics.OTHER_METHOD); + verify(span).setTag(KnownTags.HTTP_REQUEST_METHOD_ORIGINAL_ID, method); + } + + @TableTest({ + "Scenario | Method | Expected", + "GET method | GET | GET ", + "POST method | POST | POST ", + "QUERY method | QUERY | QUERY ", + "Extension method | PROPFIND | HTTP ", + "Lowercase method | get | HTTP " + }) + void spanNameMethodUsesNormalizedMethod(String method, String expected) { + assertEquals(expected, OtelHttpSemantics.spanNameMethod(method)); + } + + @Test + void spanNameMethodUsesHttpForNullMethod() { + assertEquals("HTTP", OtelHttpSemantics.spanNameMethod(null)); + } + + @TableTest({ + "Scenario | Http URL | URL path | Expected ", + "No URL or path | | | ", + "URL only | https://example.com/full | | https://example.com/full", + "Path only | | /orders | /orders ", + "Prefer URL | https://example.com/full | /different | https://example.com/full", + "Empty URL | '' | /orders | '' ", + "Empty path | | '' | '' " + }) + void urlOrPathFallsBackOnlyWhenUrlIsAbsent(String httpUrl, String path, String expected) { + Map tags = new HashMap<>(); + tags.put(Tags.HTTP_URL, httpUrl); + tags.put(KnownTags.URL_PATH_NAME, path); + + assertEquals(expected, OtelHttpSemantics.urlOrPath(tags)); + } + + @Test + void urlOrPathDoesNotRenderLazyUrl() { + URIUtils.LazyUrl url = URIUtils.lazyValidURL("https", "example.com", 443, "/orders"); + Map tags = new HashMap<>(); + tags.put(Tags.HTTP_URL, url); + tags.put(KnownTags.URL_PATH_NAME, "/different"); + + assertSame(url, OtelHttpSemantics.urlOrPath(tags)); + assertNull(url.lazy); + } + + @TableTest({ + "Scenario | Method | Expected", + "Known method | GET | GET ", + "Extension method | PROPFIND | HTTP " + }) + void setMethodResourceNameUsesNormalizedMethod(String method, String expected) { + AgentSpan span = mock(AgentSpan.class); + + OtelHttpSemantics.setMethodResourceName(span, method); + + verify(span).setResourceName(expected, ResourceNamePriorities.HTTP_PATH_NORMALIZER); + } + + @Test + void setMethodResourceNameUsesHttpForNullMethod() { + AgentSpan span = mock(AgentSpan.class); + + OtelHttpSemantics.setMethodResourceName(span, null); + + verify(span).setResourceName("HTTP", ResourceNamePriorities.HTTP_PATH_NORMALIZER); + } + + @TableTest({ + "Scenario | Raw URL | Expected URL ", + "No credentials | http://host:8080/p | http://host:8080/p ", + "Username and password | http://user:pass@host/p | http://REDACTED:REDACTED@host/p ", + "Username only | http://user@host/p | http://REDACTED@host/p ", + "Query and fragment | https://u:p@host:443/a?b=c#frag | https://REDACTED:REDACTED@host:443/a?b=c#frag", + "Credentials in path | https://u:p@host/u:p@path | https://REDACTED:REDACTED@host/u:p@path ", + "Underscore host | http://user:pass@foo_bar/p | http://REDACTED:REDACTED@foo_bar/p ", + "Underscore username | http://user_@host/p | http://REDACTED@host/p ", + "Unicode host | http://user:pass@例え.テスト/p | http://REDACTED:REDACTED@例え.テスト/p ", + "Encoded credentials | http://u%40ser:p%40ss@foo_bar/p | http://REDACTED:REDACTED@foo_bar/p ", + "At sign in password | http://user:p@ss@foo_bar/p | http://REDACTED:REDACTED@foo_bar/p ", + "Empty username | http://:pass@foo_bar/p | http://REDACTED:REDACTED@foo_bar/p ", + "IPv6 host | http://user:pass@[::1]:8080/p | http://REDACTED:REDACTED@[::1]:8080/p ", + "IPv6 username only | http://user@[::1]:8080/p | http://REDACTED@[::1]:8080/p ", + "Scheme relative URL | //user:pass@foo_bar/p | //REDACTED:REDACTED@foo_bar/p ", + "At signs outside host | http://foo_bar/u:p@path?q=u:p@query | http://foo_bar/u:p@path?q=u:p@query ", + "Relative path | /u:p@path | /u:p@path ", + "Opaque URI | mailto:user:pass@host | mailto:user:pass@host " + }) + void redactedUrlRedactsCredentials(String raw, String expected) { + assertEquals(expected, OtelHttpSemantics.redactedUrl(URI.create(raw))); + } + + @TableTest({ + "Scenario | URL | Expected ", + "Query removed | http://host/p?token=secret | http://host/p ", + "Fragment retained | http://host/p#frag | http://host/p#frag ", + "Query before fragment | http://host/p?q=1#frag | http://host/p#frag ", + "Question mark in fragment | http://host/p#frag?q=1 | http://host/p#frag?q=1", + "No query or fragment | http://host/p | http://host/p " + }) + void withoutQueryPreservesFragment(String url, String expected) { + assertEquals(expected, OtelHttpSemantics.withoutQuery(url)); + } + + @TableTest({ + "Scenario | URL | Expected Port", + "Explicit port | http://host:8080/ | 8080 ", + "HTTP default | http://host/ | 80 ", + "HTTPS default | https://host/ | 443 ", + "Unknown scheme | ftp://host/ | -1 " + }) + void serverPortFallsBackToSchemeDefault(String url, int expectedPort) { + assertEquals(expectedPort, OtelHttpSemantics.serverPort(URI.create(url))); + } + + @Test + void setErrorTypeSetsStatusWhenAbsent() { + AgentSpan span = mock(AgentSpan.class); + when(span.getTag(DDTags.ERROR_TYPE)).thenReturn(null); + + OtelHttpSemantics.setErrorType(span, 500); + + verify(span).setTag(KnownTags.ERROR_TYPE_ID, "500"); + } + + @Test + void setErrorTypeDoesNotOverrideExistingErrorType() { + AgentSpan span = mock(AgentSpan.class); + when(span.getTag(DDTags.ERROR_TYPE)).thenReturn("java.net.UnknownHostException"); + + OtelHttpSemantics.setErrorType(span, 500); + + verify(span, never()).setTag(eq(KnownTags.ERROR_TYPE_ID), any(String.class)); + } + + @TableTest({ + "Scenario | Header | Expected", + "Single host | {host, ' host '} | host ", + "First forwarded host | {'host, proxy', ' host , proxy'} | host " + }) + void forwardedValueUsesFirstTrimmedEntry(String header, String expected) { + assertEquals(expected, OtelHttpSemantics.forwardedValue(header)); + } + + @ParameterizedTest + @ValueSource(strings = {"", " ", ", proxy"}) + void forwardedValueReturnsNullForBlankFirstEntry(String header) { + assertNull(OtelHttpSemantics.forwardedValue(header)); + } + + @Test + void forwardedValueReturnsNullForNullHeader() { + assertNull(OtelHttpSemantics.forwardedValue(null)); + } + + @TableTest({ + "Scenario | Value | Expected", + "Valid port | 8080 | 8080 ", + "Minimum port | 1 | 1 ", + "Maximum port | 65535 | 65535 ", + "Leading zeros | 00080 | 80 ", + "Out of range | {0, 65536, 99999} | -1 ", + "Malformed port | {abc, 80x, 8.0, ' 80', '80 '} | -1 ", + "Signed port | {+80, -1} | -1 ", + "Non-ASCII digits | {80, ٨٠} | -1 ", + "Excess digits | {100000, 000080, 008080, 2147483647, 2147483648} | -1 " + }) + void parsePortRejectsOutOfRangeAndMalformedValues(String value, int expected) { + assertEquals(expected, OtelHttpSemantics.parsePort(value)); + } + + @Test + void parsePortReturnsUnsetForEmptyValue() { + assertEquals(-1, OtelHttpSemantics.parsePort("")); + } + + @Test + void parsePortReturnsUnsetForNullValue() { + assertEquals(-1, OtelHttpSemantics.parsePort(null)); + } + + @TableTest({ + "Scenario | Scheme | Port | Expected", + "HTTPS default | {https, HTTPS} | -1 | 443 ", + "HTTP default | {http, HTTP} | -1 | 80 ", + "Unknown scheme | ftp | -1 | -1 " + }) + void serverPortUsesSchemeDefaultWhenPortUnset(String scheme, int port, int expected) { + assertEquals(expected, OtelHttpSemantics.serverPort(scheme, port)); + } + + @Test + void serverPortReturnsUnsetForNullScheme() { + assertEquals(-1, OtelHttpSemantics.serverPort(null, -1)); + } + + @TableTest({ + "Scenario | Host | Expected port", + "No host | | -1 ", + "Empty host | '' | -1 ", + "Host only | public.example.com | -1 ", + "Empty port | public.example.com: | -1 ", + "Explicit port | public.example.com:9443 | 9443 ", + "Minimum port | public.example.com:1 | 1 ", + "Maximum port | public.example.com:65535 | 65535 ", + "Leading zeros | public.example.com:00080 | 80 ", + "Excess digits | public.example.com:008080 | -1 ", + "Signed port | {'host:+80', 'host:-1'} | -1 ", + "Whitespace | {'host: 80', 'host:80 '} | -1 ", + "Non-ASCII digits | {'host:80', 'host:٨٠'} | -1 ", + "Invalid port | public.example.com:bad | -1 ", + "Port out of range | public.example.com:65536 | -1 ", + "IPv6 host | '[2001:db8::1]' | -1 ", + "IPv6 explicit port | '[2001:db8::1]:8443' | 8443 ", + "Unbracketed IPv6 | 2001:db8::1:8443 | -1 ", + "Malformed IPv6 | '[2001:db8::1:8443' | -1 " + }) + void portFromHostRequiresAnExplicitValidPort(String host, int expectedPort) { + assertEquals(expectedPort, OtelHttpSemantics.portFromHost(host)); + } +} diff --git a/tag-conventions.yaml b/tag-conventions.yaml index 3f13f0fbd49..b6dfdcbdca3 100644 --- a/tag-conventions.yaml +++ b/tag-conventions.yaml @@ -112,12 +112,13 @@ span_types: abstract: true extends: base tags: - - { dd-name: http.method, type: string, required: required, otel-name: http.request.method } - - { 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 } - - { dd-name: http.fragment.string, type: string, required: optional } # only when query strings are tagged - - { dd-name: http.endpoint, type: string, required: optional } # simplified endpoint, when resource renaming is on # 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: http.method, type: string, required: required, otel-name: http.request.method } + - { dd-name: http.request.method_original, type: string, required: conditional } # OTel-only; set only while OTel semantics are enabled + - { 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 } + - { dd-name: http.fragment.string, type: string, required: optional } # only when query strings are tagged + - { dd-name: http.endpoint, type: string, required: optional } # simplified endpoint, when resource renaming is on # 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. http.server: extends: http @@ -128,7 +129,9 @@ span_types: - { dd-name: http.hostname, type: string, required: required, otel-name: server.address } # inbound only; on outbound spans server.address is peer.hostname - { dd-name: server.port, type: int, required: conditional } # the server's own port; on outbound spans server.port is peer.port - { dd-name: http.useragent, type: string, required: recommended, otel-name: user_agent.original, span-kind-neutral: true } - - { dd-name: http.query.string, type: string, required: recommended } # not a rename: url.query is also carried inside url.full, while http.url excludes the query (QueryObfuscator re-appends it) + - { dd-name: url.path, type: string, required: conditional } # OTel-only; server replacement for http.url + - { dd-name: url.scheme, type: string, required: conditional } # OTel-only; server replacement for http.url + - { dd-name: http.query.string, type: string, required: recommended, otel-name: url.query, span-kind-neutral: true } - { dd-name: http.client_ip, type: string, required: recommended, otel-name: client.address, span-kind-neutral: true } # 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. @@ -387,7 +390,7 @@ mixins: inbound_peer: span-kind: server tags: - - { dd-name: peer.port, type: int, otel-name: client.port } + - { dd-name: peer.port, type: int, otel-name: client.port } # code_origin — the debugger's code origin, on entry spans (and their local root), annotated # controller methods' handler spans, and @Trace spans.