From 30c758dc6f3fb2df32ef479744320aee0faef562 Mon Sep 17 00:00:00 2001 From: Zach Montoya Date: Wed, 23 Sep 2026 15:18:51 -0700 Subject: [PATCH 01/22] Configure required settings for OpenTelemetry HTTP semantics: - Http client error status codes default to 400-600 (instead of 400-500) - The environment variable DD_TRACE_OTEL_SEMANTICS_ENABLED automatically configures the following - OTEL_TRACES_EXPORTER=otlp - DD_WRITER_TYPE=OtlpWriter - DD_TRACE_SPAN_ATTRIBUTE_SCHEMA=v0 - DD_TRACE_PEER_SERVICE_DEFAULTS_ENABLED=false --- .../datadog/trace/api/ConfigDefaults.java | 4 ++ .../main/java/datadog/trace/api/Config.java | 68 +++++++++++++++++-- .../java/datadog/trace/api/ConfigTest.java | 31 ++++++++- 3 files changed, 96 insertions(+), 7 deletions(-) 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/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/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()); From 46e94e7a38d8c94750d7dc0cac9e6e65c83ed4f3 Mon Sep 17 00:00:00 2001 From: Zach Montoya Date: Thu, 24 Sep 2026 08:34:00 -0700 Subject: [PATCH 02/22] Add the following tags: http: - http.request.method_original (OTel) - server.address (OTel) - server.port (OTel) http.server: - url.path (OTel) - url.query (OTel) - url.scheme (OTel) mixins: peer: tags: - network.peer.address (OTel) - peer.port (DD) => network.peer.port (OTel) Map peer port to the OTel network attribute Fixup http tag conventions. Squash so we can edit the message --- .../trace/OtlpTraceJsonCollectorTest.java | 37 +++++++++++++++++-- .../java/datadog/trace/api/KnownTagsTest.java | 14 +++++-- tag-conventions.yaml | 29 +++++++++------ 3 files changed, 62 insertions(+), 18 deletions(-) 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..716a8b3dd64 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,18 @@ void tagsAreEmittedUnderTheirOpenTelemetryNameWhenOtelSemanticsEnabled() throws assertFalse( attrKeys.contains("http.method"), "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,17 +127,33 @@ 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("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); } + // Restore the commented out pairings when an OTel name can also be qualified by a direction. + // Currently, OTel tags server.address and server.port represent the "peer" on client spans + // and the "self" on server spans @TableTest({ "scenario | kind | tag | otelName ", - "client hostname | client | peer.hostname | server.address ", - "server hostname | server | http.hostname | server.address ", + // "client hostname | client | peer.hostname | server.address ", + // "server hostname | server | http.hostname | server.address ", "server client ip | server | network.client.ip | network.peer.address", - "client peer port | client | peer.port | server.port ", + // "client peer port | client | peer.port | server.port ", "server peer port | server | peer.port | client.port " }) void aDirectionScopedTagIsEmittedUnderTheNameForTheSpansKind( @@ -211,6 +239,9 @@ private static Set collectTagsAttributeKeys(boolean otelSemanticsEnabled AgentSpan agentSpan = TRACER.startSpan("test", "op.tagged"); agentSpan.setResourceName("GET /api"); agentSpan.setTag("http.method", "GET"); + 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/internal-api/src/test/java/datadog/trace/api/KnownTagsTest.java b/internal-api/src/test/java/datadog/trace/api/KnownTagsTest.java index 29c7fa1ac17..5b85361fa8a 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(KnownTags.SERVER_ADDRESS_NAME, KnownTags.SERVER_ADDRESS_ID), + Arguments.of(KnownTags.SERVER_PORT_NAME, KnownTags.SERVER_PORT_ID), + Arguments.of(KnownTags.URL_PATH_NAME, KnownTags.URL_PATH_ID), + Arguments.of(KnownTags.URL_QUERY_NAME, KnownTags.URL_QUERY_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), @@ -68,6 +76,7 @@ static Stream otelNamedTags() { Arguments.of("url.full", KnownTags.HTTP_URL_ID, "http.url"), 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("network.peer.address", KnownTags.NETWORK_CLIENT_IP_ID, "network.client.ip"), Arguments.of("db.system", KnownTags.DB_TYPE_ID, "db.type"), Arguments.of("db.operation.name", KnownTags.DB_OPERATION_ID, "db.operation"), Arguments.of("db.query.text", KnownTags.DB_STATEMENT_ID, "db.statement"), @@ -177,9 +186,8 @@ void aNameDeclaredPerDirectionIsOneTag() { assertEquals( "client.port", KnownTagCodec.openTelemetryTagOf(KnownTags.PEER_PORT_ID, KnownTagCodec.DIRECTION_INBOUND)); - assertEquals( - "server.port", - KnownTagCodec.openTelemetryTagOf(KnownTags.PEER_PORT_ID, KnownTagCodec.DIRECTION_OUTBOUND)); + // Temporarily remove "server.port" == openTelemetryTagOf(peer.port, outbound) because + // server.port in OTel is also used by server spans assertEquals( Tags.PEER_PORT, KnownTagCodec.openTelemetryTagOf(KnownTags.PEER_PORT_ID, KnownTagCodec.DIRECTION_NONE)); diff --git a/tag-conventions.yaml b/tag-conventions.yaml index 3f13f0fbd49..2d989e7206a 100644 --- a/tag-conventions.yaml +++ b/tag-conventions.yaml @@ -112,12 +112,15 @@ 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. + - { dd-name: server.address, type: string, required: conditional } # OTel-only; set only while OTel semantics are enabled + - { dd-name: server.port, type: int, required: conditional } # OTel-only; set only while OTel semantics are enabled http.server: extends: http @@ -125,14 +128,16 @@ span_types: include: [ peer_address, inbound_peer ] tags: - { dd-name: http.route, type: string, required: conditional } # passes through: dd-name already is the OTel name - - { 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.hostname, type: string, required: required } - { dd-name: http.useragent, type: string, required: recommended, otel-name: user_agent.original, span-kind-neutral: true } + - { dd-name: url.path, type: string, required: conditional } # OTel-only; server replacement for http.url + - { dd-name: url.query, 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 } # not a rename: url.query is also carried inside url.full, while http.url excludes the query (QueryObfuscator re-appends it) - { 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. - - { dd-name: network.client.ip, type: string, required: recommended, otel-name: network.peer.address } + - { dd-name: network.client.ip, type: string, required: recommended, otel-name: network.peer.address, span-kind-neutral: true } # Temporary direction-free alias until name resolution is span-kind-aware # Forwarding headers, when client IPs are tagged and the header is present. - { dd-name: http.forwarded, type: string, required: optional } - { dd-name: http.forwarded.proto, type: string, required: optional } @@ -380,14 +385,14 @@ mixins: outbound_peer: span-kind: client tags: - - { dd-name: peer.hostname, type: string, required: recommended, otel-name: server.address } - - { dd-name: peer.port, type: int, otel-name: server.port } + - { dd-name: peer.hostname, type: string, required: recommended } + - { dd-name: peer.port, type: int } # inbound_peer — the other end of an inbound connection: the client. 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. From ea6f1be8ca73eee290cea4a1fc43756c87304885 Mon Sep 17 00:00:00 2001 From: Zach Montoya Date: Tue, 6 Oct 2026 15:08:14 -0700 Subject: [PATCH 03/22] Implement OpenTelemetry HTTP client semantic conventions --- .../decorator/HttpClientDecorator.java | 39 +++- .../decorator/UriBasedClientDecorator.java | 22 ++- .../decorator/UrlConnectionDecorator.java | 9 +- .../decorator/http/HttpResourceDecorator.java | 8 +- ...HttpDecoratorsOtelSemanticsForkedTest.java | 120 +++++++++++++ .../core/tagprocessor/QueryObfuscator.java | 42 ++++- .../tagprocessor/QueryObfuscatorTest.java | 64 ++++++- .../api/OtelHttpSemantics.java | 109 +++++++++++ .../api/OtelHttpSemanticsTest.java | 169 ++++++++++++++++++ 9 files changed, 552 insertions(+), 30 deletions(-) create mode 100644 dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/decorator/HttpDecoratorsOtelSemanticsForkedTest.java create mode 100644 internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/OtelHttpSemantics.java create mode 100644 internal-api/src/test/java/datadog/trace/bootstrap/instrumentation/api/OtelHttpSemanticsTest.java 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..1d7fca795b0 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 @@ -23,6 +23,7 @@ import datadog.trace.bootstrap.instrumentation.api.AgentSpan; import datadog.trace.bootstrap.instrumentation.api.AgentTracer; import datadog.trace.bootstrap.instrumentation.api.InternalSpanTypes; +import datadog.trace.bootstrap.instrumentation.api.OtelHttpSemantics; import datadog.trace.bootstrap.instrumentation.api.Tags; import datadog.trace.bootstrap.instrumentation.api.URIUtils; import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; @@ -42,6 +43,7 @@ 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 UTF8BytesString DEFAULT_RESOURCE_NAME = UTF8BytesString.create("/"); @@ -118,7 +120,14 @@ 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); + if (shouldSetResourceName()) { + OtelHttpSemantics.setMethodResourceName(span, method); + } + } else { + span.setTag(Tags.HTTP_METHOD, method); + } if (CLIENT_TAG_HEADERS) { for (Map.Entry headerTag : @@ -135,19 +144,28 @@ protected void doOnRequest(final AgentSpan span, final REQUEST request) { final URI url = url(request); if (url != null) { onURI(span, url); - span.setTag( - Tags.HTTP_URL, - URIUtils.lazyValidURL(url.getScheme(), url.getHost(), url.getPort(), url.getPath())); - if (Config.get().isHttpClientTagQueryString()) { - span.setTag(DDTags.HTTP_QUERY, url.getQuery()); - span.setTag(DDTags.HTTP_FRAGMENT, url.getFragment()); + if (OTEL_SEMANTICS) { + OtelHttpSemantics.setClientUrl(span, url); + // url.full may trigger URLAsResourceNameRule; restore the OTel span name afterwards. + if (shouldSetResourceName()) { + OtelHttpSemantics.setMethodResourceName(span, method); + } + } else { + span.setTag( + Tags.HTTP_URL, + URIUtils.lazyValidURL( + url.getScheme(), url.getHost(), url.getPort(), url.getPath())); + if (Config.get().isHttpClientTagQueryString()) { + span.setTag(DDTags.HTTP_QUERY, url.getQuery()); + span.setTag(DDTags.HTTP_FRAGMENT, url.getFragment()); + } } - if (shouldSetResourceName()) { + if (!OTEL_SEMANTICS && shouldSetResourceName()) { HTTP_RESOURCE_DECORATOR.withClientPath(span, method, url.getPath()); } // 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) { @@ -177,6 +195,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/UriBasedClientDecorator.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/UriBasedClientDecorator.java index 0ab130eab19..68c1b42d876 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 @@ -6,24 +6,36 @@ import datadog.context.Context; import datadog.context.propagation.CarrierSetter; import datadog.trace.api.Config; +import datadog.trace.api.KnownTags; 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 (OTEL_SEMANTICS) { + span.setTag(KnownTags.SERVER_ADDRESS_NAME, host); + int serverPort = OtelHttpSemantics.serverPort(uri); + if (serverPort > 0) { + span.setTag(KnownTags.SERVER_PORT_NAME, serverPort); + } + } else { + span.setTag(Tags.PEER_HOSTNAME, host); + int port = uri.getPort(); + if (port > 0) { + setPeerPort(span, port); + } + } if (Config.get().isHttpClientSplitByDomain() && host.charAt(0) >= 'A') { span.setServiceName(host, component()); } - 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..00cf9056ec0 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,10 +1,12 @@ package datadog.trace.bootstrap.instrumentation.decorator; +import datadog.trace.api.Config; 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.OtelHttpSemantics; import datadog.trace.bootstrap.instrumentation.api.Tags; import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; import java.net.URI; @@ -16,6 +18,7 @@ import org.slf4j.LoggerFactory; public class UrlConnectionDecorator extends UriBasedClientDecorator { + private static final boolean OTEL_SEMANTICS = Config.get().isTraceOtelSemanticsEnabled(); private static final DDCache CACHE = DDCaches.newFixedSizeCache(16); private static final Function ADDER = @@ -51,7 +54,11 @@ protected String service() { @Override public void onURI(@Nonnull AgentSpan span, @Nonnull URI uri) { super.onURI(span, uri); - span.setTag(Tags.HTTP_URL, uri.toString()); + if (OTEL_SEMANTICS) { + OtelHttpSemantics.setClientUrl(span, uri); + } else { + span.setTag(Tags.HTTP_URL, uri.toString()); + } } public void onURL(@Nonnull final AgentSpan span, @Nonnull final URL url) { 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..ba75e1cdee8 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 @@ -3,6 +3,7 @@ import datadog.trace.api.Config; import datadog.trace.api.normalize.HttpResourceNames; import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.bootstrap.instrumentation.api.OtelHttpSemantics; import datadog.trace.bootstrap.instrumentation.api.ResourceNamePriorities; import datadog.trace.bootstrap.instrumentation.api.Tags; import datadog.trace.bootstrap.instrumentation.api.URIUtils; @@ -12,6 +13,7 @@ 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"); @@ -19,7 +21,11 @@ public class HttpResourceDecorator { private HttpResourceDecorator() {} public final void withClientPath(AgentSpan span, CharSequence method, CharSequence path) { - HttpResourceNames.setForClient(span, method, path, false); + if (OTEL_SEMANTICS) { + OtelHttpSemantics.setMethodResourceName(span, method); + } else { + HttpResourceNames.setForClient(span, method, path, false); + } } public final void withServerPath( 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..a23d61185fd --- /dev/null +++ b/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/decorator/HttpDecoratorsOtelSemanticsForkedTest.java @@ -0,0 +1,120 @@ +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_TAG_QUERY_STRING; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import datadog.trace.api.KnownTags; +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 = "true") +@WithConfig(key = HTTP_CLIENT_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); + + 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("token=secret", span.getTag(KnownTags.URL_QUERY_NAME)); + assertEquals("example.com", span.getTag(KnownTags.SERVER_ADDRESS_NAME)); + assertEquals(443, span.getTag(KnownTags.SERVER_PORT_NAME)); + assertEquals(503, span.getHttpStatusCode()); + assertEquals("503", span.getTag("error.type")); + assertTrue(span.isError()); + assertNull(span.getTag("peer.hostname")); + assertNull(span.getTag("peer.port")); + } + + @Test + void urlConnectionDecoratorCapturesQueryForObfuscation() throws Exception { + DDSpan span = (DDSpan) tracer.startSpan("test", "urlconnection.request"); + + UrlConnectionDecorator.DECORATE.onURI( + span, new URI("https://example.com/orders?token=secret#fragment")); + + assertEquals("https://example.com/orders#fragment", span.getTag(KnownTags.HTTP_URL_OTEL_NAME)); + assertEquals("token=secret", span.getTag(KnownTags.URL_QUERY_NAME)); + } + + private static final class ClientRequest { + private final String method; + private final URI uri; + + private ClientRequest(String method, URI uri) { + this.method = method; + this.uri = uri; + } + } + + private static final 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; + } + } +} 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..72ee76e0752 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,7 +3,10 @@ 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.KnownTags; import datadog.trace.api.TagMap; import datadog.trace.api.internal.VisibleForTesting; import datadog.trace.bootstrap.instrumentation.api.AppendableSpanLinks; @@ -58,12 +61,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; @@ -116,10 +125,11 @@ private String obfuscate(String query) { @Override public void processTags( TagMap unsafeTags, DDSpanContext spanContext, AppendableSpanLinks spanLinks) { - Object query = unsafeTags.getObject(DDTags.HTTP_QUERY); + String queryTag = otelSemanticsEnabled ? KnownTags.URL_QUERY_NAME : DDTags.HTTP_QUERY; + Object query = unsafeTags.getObject(queryTag); if (query instanceof CharSequence) { if (failClosed) { - unsafeTags.remove(DDTags.HTTP_QUERY); + unsafeTags.remove(queryTag); return; } try { @@ -127,16 +137,38 @@ public void processTags( } catch (Exception e) { // never report a query that could not be obfuscated, and never log it log.debug("Could not obfuscate query string, dropping it", e); - unsafeTags.remove(DDTags.HTTP_QUERY); + unsafeTags.remove(queryTag); return; } - unsafeTags.put(DDTags.HTTP_QUERY, query); + unsafeTags.put(queryTag, query); 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(queryTag); } } } + + private static String appendQuery(final String url, final String query) { + final int fragment = url.indexOf('#'); + if (fragment < 0) { + return url + "?" + query; + } + return new StringBuilder(url.length() + query.length() + 1) + .append(url, 0, fragment) + .append('?') + .append(query) + .append(url, fragment, url.length()) + .toString(); + } } 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..7508ef465ca 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,11 +3,16 @@ 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.junit.jupiter.api.Assertions.assertNull; +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.KnownTags; import datadog.trace.api.TagMap; import datadog.trace.bootstrap.instrumentation.api.Tags; +import datadog.trace.core.DDSpanContext; import datadog.trace.test.util.DDJavaSpecification; import java.util.LinkedHashMap; import java.util.Map; @@ -65,9 +70,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 +139,26 @@ 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) { + String queryTag = otelSemantics ? KnownTags.URL_QUERY_NAME : DDTags.HTTP_QUERY; + TagMap unsafeTags = + process(new QueryObfuscator("(?<=a)b", otelSemantics), "pwd=secret", queryTag); - assertFalse(unsafeTags.containsKey(DDTags.HTTP_QUERY)); + assertFalse(unsafeTags.containsKey(queryTag)); assertEquals("http://site.com/index", unsafeTags.getObject(Tags.HTTP_URL)); } - @Test - void obfuscationFailureDropsQueryString() { + @TableTest({ + "Scenario | OTel semantics", + "Datadog | false ", + "OpenTelemetry | true " + }) + void obfuscationFailureDropsQueryString(boolean otelSemantics) { CharSequence failing = new CharSequence() { @Override @@ -163,9 +182,12 @@ public String toString() { } }; - TagMap unsafeTags = assertDoesNotThrow(() -> process(new QueryObfuscator(null), failing)); + String queryTag = otelSemantics ? KnownTags.URL_QUERY_NAME : DDTags.HTTP_QUERY; + 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 +200,28 @@ void emptyRegexKeepsObfuscationDisabled() { assertEquals(query, unsafeTags.getObject(DDTags.HTTP_QUERY)); assertEquals("http://site.com/index?" + query, unsafeTags.getObject(Tags.HTTP_URL)); } + + @TableTest({ + "spanType | retainQuery", + "http | false ", + "web | true " + }) + void otelClientQueryIsOnlyUsedToBuildUrlFull(String spanType, boolean retainQuery) { + QueryObfuscator obfuscator = new QueryObfuscator(null, true); + 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(KnownTags.URL_QUERY_NAME, "token=secret"); + + obfuscator.processTags(tags, spanContext, link -> {}); + + assertEquals("http://site.com/index?#fragment", tags.get(Tags.HTTP_URL)); + if (retainQuery) { + assertEquals("", tags.get(KnownTags.URL_QUERY_NAME)); + return; + } + assertNull(tags.get(KnownTags.URL_QUERY_NAME)); + } } 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..af572558d85 --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/OtelHttpSemantics.java @@ -0,0 +1,109 @@ +package datadog.trace.bootstrap.instrumentation.api; + +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.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_OTEL_NAME, OTHER_METHOD); + span.setTag(KnownTags.HTTP_REQUEST_METHOD_ORIGINAL_NAME, method); + } else { + span.setTag(KnownTags.HTTP_METHOD_OTEL_NAME, method); + } + } + + public static void setMethodResourceName(final AgentSpan span, final CharSequence method) { + span.setResourceName(spanNameMethod(method), ResourceNamePriorities.HTTP_PATH_NORMALIZER); + } + + public static void setClientUrl(final AgentSpan span, final URI url) { + span.setTag(KnownTags.HTTP_URL_OTEL_NAME, withoutQuery(redactedUrl(url))); + // QueryObfuscator scrubs this before rebuilding url.full and removes it from client spans. + final String query = url.getRawQuery(); + if (query != null) { + span.setTag(KnownTags.URL_QUERY_NAME, query); + } + } + + public static String spanNameMethod(final CharSequence method) { + final String methodName = method == null ? null : method.toString(); + return KNOWN_METHODS.contains(methodName) ? methodName : "HTTP"; + } + + 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 new StringBuilder(url.length() - (fragment - query)) + .append(url, 0, query) + .append(url, fragment, url.length()) + .toString(); + } + + public static void setErrorType(final AgentSpan span, final int status) { + if (span.getTag(DDTags.ERROR_TYPE) == null) { + span.setTag(DDTags.ERROR_TYPE, 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; + } +} 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..7b08faafcae --- /dev/null +++ b/internal-api/src/test/java/datadog/trace/bootstrap/instrumentation/api/OtelHttpSemanticsTest.java @@ -0,0 +1,169 @@ +package datadog.trace.bootstrap.instrumentation.api; + +import static org.junit.jupiter.api.Assertions.assertEquals; +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 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_OTEL_NAME, method); + verify(span, never()).setTag(eq(KnownTags.HTTP_REQUEST_METHOD_ORIGINAL_NAME), 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_OTEL_NAME, OtelHttpSemantics.OTHER_METHOD); + verify(span).setTag(KnownTags.HTTP_REQUEST_METHOD_ORIGINAL_NAME, 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 | 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); + } + + @Test + void setClientUrlStripsQueryAndRedactsCredentials() { + AgentSpan span = mock(AgentSpan.class); + + OtelHttpSemantics.setClientUrl(span, URI.create("https://u:p@host/a?token=secret")); + + verify(span).setTag(KnownTags.HTTP_URL_OTEL_NAME, "https://REDACTED:REDACTED@host/a"); + verify(span).setTag(KnownTags.URL_QUERY_NAME, "token=secret"); + } + + @Test + void setClientUrlOmitsQueryTagWhenAbsent() { + AgentSpan span = mock(AgentSpan.class); + + OtelHttpSemantics.setClientUrl(span, URI.create("http://host/a")); + + verify(span).setTag(KnownTags.HTTP_URL_OTEL_NAME, "http://host/a"); + verify(span, never()).setTag(eq(KnownTags.URL_QUERY_NAME), anyString()); + } + + @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(DDTags.ERROR_TYPE, "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(DDTags.ERROR_TYPE), any(String.class)); + } +} From 61d02403a787918c05396a072fd52520facf7ae6 Mon Sep 17 00:00:00 2001 From: Zach Montoya Date: Tue, 6 Oct 2026 15:20:28 -0700 Subject: [PATCH 04/22] Implement OpenTelemetry HTTP server semantic conventions --- .../decorator/HttpServerDecorator.java | 87 +++++++-- .../decorator/http/HttpResourceDecorator.java | 10 +- ...HttpDecoratorsOtelSemanticsForkedTest.java | 169 ++++++++++++++++++ .../axway/AxwayHTTPPluginDecorator.java | 66 ++++++- .../HttpEndpointPostProcessor.java | 4 + .../HttpEndpointPostProcessorTest.java | 16 ++ .../trace/api/gateway/InferredProxySpan.java | 50 ++++-- .../api/OtelHttpSemantics.java | 68 +++++++ .../api/OtelHttpSemanticsTest.java | 130 ++++++++++++++ 9 files changed, 563 insertions(+), 37 deletions(-) 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..a893faacd42 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 @@ -14,6 +14,7 @@ import datadog.context.propagation.Propagators; import datadog.trace.api.Config; import datadog.trace.api.DDTags; +import datadog.trace.api.KnownTags; import datadog.trace.api.datastreams.DataStreamsTransactionExtractor; import datadog.trace.api.datastreams.DataStreamsTransactionTracker; import datadog.trace.api.function.TriConsumer; @@ -34,6 +35,7 @@ import datadog.trace.bootstrap.instrumentation.api.ClientIpAddressData; import datadog.trace.bootstrap.instrumentation.api.ErrorPriorities; import datadog.trace.bootstrap.instrumentation.api.InternalSpanTypes; +import datadog.trace.bootstrap.instrumentation.api.OtelHttpSemantics; import datadog.trace.bootstrap.instrumentation.api.ResourceNamePriorities; import datadog.trace.bootstrap.instrumentation.api.TagContext; import datadog.trace.bootstrap.instrumentation.api.Tags; @@ -68,13 +70,15 @@ public abstract class HttpServerDecorator flow = callIGCallbackURI(span, url, method); if (flow.getAction() instanceof RequestBlockingAction) { @@ -422,6 +469,9 @@ protected void doOnRequest( } if (shouldTagIps) { span.setTag(Tags.NETWORK_CLIENT_IP, peerIp); + if (OTEL_SEMANTICS && span.getTag(KnownTags.HTTP_CLIENT_IP_OTEL_NAME) == null) { + span.setTag(KnownTags.HTTP_CLIENT_IP_OTEL_NAME, peerIp); + } } } if (shouldStashIps && (peerIp != null || inferredAddressStr != null)) { @@ -484,6 +534,9 @@ protected void doOnResponseStatus(final AgentSpan span, final int status) { if (!BlockingException.class.getName().equals(span.getTag("error.type"))) { span.setError(SERVER_ERROR_STATUSES.get(status), ErrorPriorities.HTTP_SERVER_DECORATOR); } + if (OTEL_SEMANTICS && SERVER_ERROR_STATUSES.get(status)) { + OtelHttpSemantics.setErrorType(span, status); + } } if (SHOULD_SET_404_RESOURCE_NAME && status == 404) { 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 ba75e1cdee8..567646c7fb2 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 @@ -30,6 +30,10 @@ public final void withClientPath(AgentSpan span, CharSequence method, CharSequen 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; @@ -50,7 +54,11 @@ public final void withRoute( routeTag = URIUtils.decode(route.toString()); } span.setTag(Tags.HTTP_ROUTE, routeTag); - if (Config.get().isHttpServerRouteBasedNaming()) { + if (OTEL_SEMANTICS) { + final CharSequence resourceName = + HttpResourceNames.join(OtelHttpSemantics.spanNameMethod(method), routeTag); + span.setResourceName(resourceName, ResourceNamePriorities.HTTP_FRAMEWORK_ROUTE); + } else if (Config.get().isHttpServerRouteBasedNaming()) { final CharSequence resourceName = HttpResourceNames.join(method, route); span.setResourceName(resourceName, ResourceNamePriorities.HTTP_FRAMEWORK_ROUTE); } 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 index a23d61185fd..ec36e6d897e 100644 --- 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 @@ -1,12 +1,22 @@ 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_TAG_QUERY_STRING; +import static datadog.trace.bootstrap.instrumentation.decorator.http.HttpResourceDecorator.HTTP_RESOURCE_DECORATOR; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import datadog.trace.api.DDTraceId; 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.TagContext; +import datadog.trace.bootstrap.instrumentation.api.URIDataAdapter; +import datadog.trace.bootstrap.instrumentation.api.URIDefaultDataAdapter; import datadog.trace.common.writer.ListWriter; import datadog.trace.core.CoreTracer; import datadog.trace.core.DDSpan; @@ -15,9 +25,11 @@ 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 = HTTP_CLIENT_TAG_QUERY_STRING, value = "false") +@WithConfig(key = HTTP_SERVER_TAG_QUERY_STRING, value = "false") class HttpDecoratorsOtelSemanticsForkedTest { private CoreTracer tracer; @@ -70,6 +82,90 @@ void urlConnectionDecoratorCapturesQueryForObfuscation() throws Exception { assertEquals("token=secret", span.getTag(KnownTags.URL_QUERY_NAME)); } + @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); + + 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)); + assertEquals("view=full", span.getTag(KnownTags.URL_QUERY_NAME)); + assertEquals("example.com", span.getTag(KnownTags.SERVER_ADDRESS_NAME)); + assertEquals(8443, span.getTag(KnownTags.SERVER_PORT_NAME)); + assertEquals("192.0.2.10", span.getTag(KnownTags.HTTP_CLIENT_IP_OTEL_NAME)); + assertEquals("192.0.2.10", span.getTag(KnownTags.NETWORK_CLIENT_IP_OTEL_NAME)); + assertEquals(503, span.getHttpStatusCode()); + assertEquals("503", span.getTag("error.type")); + assertTrue(span.isError()); + assertFalse(span.getTags().containsKey("http.url")); + } + + @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.SERVER_ADDRESS_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.SERVER_ADDRESS_NAME)); + assertEquals(9443, span.getTag(KnownTags.SERVER_PORT_NAME)); + } + private static final class ClientRequest { private final String method; private final URI uri; @@ -117,4 +213,77 @@ protected String getResponseHeader(Integer response, String headerName) { return null; } } + + private static final class ServerRequest { + private final String method; + private final URI uri; + + private ServerRequest(String method, URI uri) { + this.method = method; + this.uri = uri; + } + } + + 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 new URIDefaultDataAdapter(request.uri); + } + + @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/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..756d4fec11e 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,8 +2,11 @@ import static java.lang.invoke.MethodType.methodType; +import datadog.trace.api.Config; +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.OtelHttpSemantics; import datadog.trace.bootstrap.instrumentation.api.Tags; import datadog.trace.bootstrap.instrumentation.api.URIDataAdapter; import datadog.trace.bootstrap.instrumentation.api.URIDefaultDataAdapter; @@ -22,6 +25,7 @@ // 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 boolean OTEL_SEMANTICS = Config.get().isTraceOtelSemanticsEnabled(); public static final CharSequence AXWAY_TRY_TRANSACTION = UTF8BytesString.create("axway.trytransaction"); @@ -198,13 +202,47 @@ 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); + if (OTEL_SEMANTICS) { + setOtelTransactionTags(span, stateInstance); + } else { + 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); + } } } + private static void setOtelTransactionTags(AgentSpan span, Object stateInstance) { + String method = getStringFromStateField(stateInstance, methodField_mh); + OtelHttpSemantics.setRequestMethod(span, method); + OtelHttpSemantics.setMethodResourceName(span, method); + + URI uri = getUriFromStateField(stateInstance); + String scheme = null; + String host = getStringFromStateField(stateInstance, hostField_mh); + int port = OtelHttpSemantics.parsePort(getStringFromStateField(stateInstance, portField_mh)); + if (uri != null) { + scheme = uri.getScheme(); + if (uri.getPath() != null) { + span.setTag(KnownTags.URL_PATH_NAME, uri.getPath()); + } + if (uri.getRawQuery() != null && !uri.getRawQuery().isEmpty()) { + span.setTag(KnownTags.URL_QUERY_NAME, uri.getRawQuery()); + } + if (host == null || host.isEmpty()) { + host = uri.getHost(); + } + if (port <= 0) { + port = uri.getPort(); + } + } + if (scheme != null) { + span.setTag(KnownTags.URL_SCHEME_NAME, scheme); + } + OtelHttpSemantics.setServerAddressAndPort(span, null, null, scheme, host, port); + } + /** * class hierarchy in package com.vordel.dwe.http : * @@ -245,6 +283,16 @@ private static void setStringTagFromStateField( span.setTag(tag, v); } + private static String getStringFromStateField(Object stateInstance, MethodHandle mh) { + try { + Object value = mh.invoke(stateInstance); + return value == null ? null : value.toString(); + } catch (Throwable e) { + log.debug("Can't invoke '{}' on instance '{}'; value not read.", mh, stateInstance, e); + return null; + } + } + private static void setURLTagFromUriStateField(AgentSpan span, Object stateInstance) { try { span.setTag(Tags.HTTP_URL, uriField_mh.invoke(stateInstance).toString()); @@ -257,4 +305,14 @@ private static void setURLTagFromUriStateField(AgentSpan span, Object stateInsta e); } } + + private static URI getUriFromStateField(Object stateInstance) { + try { + Object value = uriField_mh.invoke(stateInstance); + return value == null ? null : URI.create(value.toString()); + } catch (Throwable e) { + log.debug("Can't invoke '{}' on instance '{}'; URL not read.", uriField_mh, stateInstance, e); + return null; + } + } } 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..56057e996e8 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 @@ -4,6 +4,7 @@ import static datadog.trace.bootstrap.instrumentation.api.Tags.HTTP_ROUTE; import static datadog.trace.bootstrap.instrumentation.api.Tags.HTTP_URL; +import datadog.trace.api.KnownTags; import datadog.trace.api.TagMap; import datadog.trace.api.endpoint.EndpointResolver; import datadog.trace.api.internal.VisibleForTesting; @@ -68,6 +69,9 @@ public void processTags( try { String httpRoute = unsafeTags.getString(HTTP_ROUTE); String httpUrl = unsafeTags.getString(HTTP_URL); + if (httpUrl == null) { + httpUrl = unsafeTags.getString(KnownTags.URL_PATH_NAME); + } endpointResolver.resolveEndpoint(unsafeTags, httpRoute, httpUrl); } catch (Throwable t) { log.debug("Error processing HTTP endpoint for span {}", spanContext.getSpanId(), t); 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/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..6eed4b07b77 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 @@ -18,10 +18,12 @@ 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 java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -42,6 +44,7 @@ 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(); 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 @@ -112,16 +114,24 @@ public AgentSpanContext start(AgentSpanContext extracted) { // SpanType: web span.setTag(SPAN_TYPE, "web"); - // Http.method - value of x-dd-proxy-httpmethod - span.setTag(HTTP_METHOD, httpMethod); - - // 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); + span.setTag(KnownTags.URL_PATH_NAME, path); + if (domainName != null && !domainName.isEmpty()) { + span.setTag(KnownTags.URL_SCHEME_NAME, "https"); + span.setTag(KnownTags.SERVER_ADDRESS_NAME, domainName); + } + } else { + span.setTag(HTTP_METHOD, httpMethod); + span.setTag( + HTTP_URL, + domainName != null && !domainName.isEmpty() ? "https://" + domainName + path : path); + } - // 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(HTTP_ROUTE, OTEL_SEMANTICS ? route : route != null ? route : path); // "stage" - value of x-dd-proxy-stage span.setTag("stage", header(STAGE)); @@ -153,12 +163,19 @@ 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 spanNameMethod = + OTEL_SEMANTICS ? OtelHttpSemantics.spanNameMethod(httpMethod) : httpMethod; + String resourceName; + if (OTEL_SEMANTICS) { + resourceName = route != null ? spanNameMethod + " " + route : spanNameMethod; + } else { + String routeOrPath = route != null ? route : path; + resourceName = + spanNameMethod != null && routeOrPath != null ? spanNameMethod + " " + routeOrPath : null; + } if (resourceName != null) { span.setResourceName(resourceName, MANUAL_INSTRUMENTATION); } @@ -290,6 +307,9 @@ 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); 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 index af572558d85..5b23167fd88 100644 --- 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 @@ -106,4 +106,72 @@ public static int serverPort(final String scheme, final int port) { } 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; + } + + public static void setServerAddressAndPort( + final AgentSpan span, + final String forwardedHostHeader, + final String forwardedPortHeader, + final String scheme, + final String requestHost, + final int requestPort) { + final String forwardedHost = forwardedValue(forwardedHostHeader); + String serverAddress = requestHost; + int hostPort = -1; + + if (forwardedHost != null) { + serverAddress = forwardedHost; + if (forwardedHost.charAt(0) == '[') { + final int closingBracket = forwardedHost.indexOf(']'); + if (closingBracket > 0) { + serverAddress = forwardedHost.substring(1, closingBracket); + if (closingBracket + 1 < forwardedHost.length() + && forwardedHost.charAt(closingBracket + 1) == ':') { + hostPort = parsePort(forwardedHost.substring(closingBracket + 2)); + } + } + } else { + final int colon = forwardedHost.lastIndexOf(':'); + if (colon > 0 && colon == forwardedHost.indexOf(':')) { + serverAddress = forwardedHost.substring(0, colon); + hostPort = parsePort(forwardedHost.substring(colon + 1)); + } + } + } + + if (serverAddress != null && !serverAddress.isEmpty()) { + span.setTag(KnownTags.SERVER_ADDRESS_NAME, serverAddress); + } + + int serverPort = parsePort(forwardedValue(forwardedPortHeader)); + if (serverPort <= 0) { + serverPort = hostPort; + } + if (serverPort <= 0) { + serverPort = forwardedHost == null && requestPort > 0 ? requestPort : serverPort(scheme, -1); + } + if (serverPort > 0) { + span.setTag(KnownTags.SERVER_PORT_NAME, serverPort); + } + } + + public static int parsePort(final String value) { + if (value == null || value.isEmpty()) { + return -1; + } + try { + final int port = Integer.parseInt(value); + return port > 0 && port <= 65535 ? port : -1; + } catch (NumberFormatException ignored) { + return -1; + } + } } 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 index 7b08faafcae..1f588967368 100644 --- 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 @@ -1,7 +1,9 @@ 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.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; @@ -166,4 +168,132 @@ void setErrorTypeDoesNotOverrideExistingErrorType() { verify(span, never()).setTag(eq(DDTags.ERROR_TYPE), 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 ", + "Invalid port | {0, 65536, -1, abc} | -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)); + } + + @Test + void setServerAddressAndPortUsesRequestHostAndPort() { + AgentSpan span = mock(AgentSpan.class); + + OtelHttpSemantics.setServerAddressAndPort(span, null, null, "http", "host", 8080); + + verify(span).setTag(KnownTags.SERVER_ADDRESS_NAME, "host"); + verify(span).setTag(KnownTags.SERVER_PORT_NAME, 8080); + } + + @Test + void setServerAddressAndPortFallsBackToSchemeDefaultPort() { + AgentSpan span = mock(AgentSpan.class); + + OtelHttpSemantics.setServerAddressAndPort(span, null, null, "https", "host", -1); + + verify(span).setTag(KnownTags.SERVER_ADDRESS_NAME, "host"); + verify(span).setTag(KnownTags.SERVER_PORT_NAME, 443); + } + + @Test + void setServerAddressAndPortOmitsTagsWhenNothingResolvable() { + AgentSpan span = mock(AgentSpan.class); + + OtelHttpSemantics.setServerAddressAndPort(span, null, null, "ftp", null, -1); + + verify(span, never()).setTag(eq(KnownTags.SERVER_ADDRESS_NAME), anyString()); + verify(span, never()).setTag(eq(KnownTags.SERVER_PORT_NAME), anyInt()); + } + + @Test + void setServerAddressAndPortOmitsAddressWhenRequestHostIsEmpty() { + AgentSpan span = mock(AgentSpan.class); + + OtelHttpSemantics.setServerAddressAndPort(span, null, null, "http", "", -1); + + verify(span, never()).setTag(eq(KnownTags.SERVER_ADDRESS_NAME), anyString()); + verify(span).setTag(KnownTags.SERVER_PORT_NAME, 80); + } + + @Test + void setServerAddressAndPortPrefersForwardedPortHeader() { + AgentSpan span = mock(AgentSpan.class); + + OtelHttpSemantics.setServerAddressAndPort(span, "proxy:8443", "9443", "http", "host", 8080); + + verify(span).setTag(KnownTags.SERVER_ADDRESS_NAME, "proxy"); + verify(span).setTag(KnownTags.SERVER_PORT_NAME, 9443); + } + + @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 setServerAddressAndPortParsesForwardedHost( + String forwardedHost, String expectedAddress, int expectedPort) { + AgentSpan span = mock(AgentSpan.class); + + OtelHttpSemantics.setServerAddressAndPort(span, forwardedHost, null, "http", "host", 8080); + + verify(span).setTag(KnownTags.SERVER_ADDRESS_NAME, expectedAddress); + // A forwarded host suppresses the request port, so the scheme default applies instead. + verify(span).setTag(KnownTags.SERVER_PORT_NAME, expectedPort); + } } From 099041cf187b9a588047ceb9e75a67da7ef454fe Mon Sep 17 00:00:00 2001 From: Zach Montoya Date: Thu, 1 Oct 2026 15:12:18 -0700 Subject: [PATCH 05/22] Test end-to-end OpenTelemetry HTTP client semantics - Apache HTTP client - Apache HTTP Async Client - Google HTTP Client - HttpURLConnection - JAX-RS Client - OkHttp - Play WSClient --- .../agent/test/base/HttpClientTest.groovy | 57 ++++++++++++++----- .../test/naming/NamingConventions.groovy | 4 ++ .../naming/VersionedNamingTestBase.groovy | 4 ++ .../groovy/ApacheHttpAsyncClientTest.groovy | 4 ++ .../test/groovy/ApacheHttpClientTest.groovy | 5 ++ .../test/groovy/GoogleHttpClientTest.groovy | 5 ++ .../test/groovy/HttpUrlConnectionTest.groovy | 4 ++ .../src/test/groovy/OkHttp3Test.groovy | 20 +++++++ .../src/test/groovy/PlayWSClientTest.groovy | 6 ++ .../test/groovy/PlayWSClientTestBase.groovy | 5 ++ .../test/groovy/JaxRsClientAsyncTest.groovy | 5 ++ .../src/test/groovy/JaxRsClientTest.groovy | 5 ++ 12 files changed, 110 insertions(+), 14 deletions(-) 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..2f2394d78c3 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 expectedQuery = tagQueryString ? uri.query : null + def otelSemantics = Config.get().isTraceOtelSemanticsEnabled() + 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" } + if (otelSemantics && emitsUrlFragment() && uri.rawFragment != null) { + expectedUrl = "$expectedUrl#$uri.rawFragment" + } trace.span { if (parentSpan == null) { parent() @@ -1005,28 +1012,40 @@ 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 "$Tags.SPAN_KIND" Tags.SPAN_KIND_CLIENT - "$Tags.PEER_HOSTNAME" { - it == uri.host || ignorePeer - } - "$Tags.PEER_HOST_IPV4" { - it == null || it == "127.0.0.1" || ignorePeer - } // Optional - "$Tags.PEER_PORT" { - it == null || it == uri.port || it == proxy.port || it == 443 || ignorePeer + if (otelSemantics) { + "$Tags.PEER_HOSTNAME" null + "$Tags.PEER_HOST_IPV4" null + "$Tags.PEER_PORT" null + "$KnownTags.SERVER_ADDRESS_NAME" uri.host + "$KnownTags.SERVER_PORT_NAME" OtelHttpSemantics.serverPort(uri) + } else { + "$Tags.PEER_HOSTNAME" { + it == uri.host || ignorePeer + } + "$Tags.PEER_HOST_IPV4" { + it == null || it == "127.0.0.1" || ignorePeer + } // Optional + "$Tags.PEER_PORT" { + 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) { + "$DDTags.HTTP_QUERY" null + "$KnownTags.URL_QUERY_NAME" null + "$DDTags.HTTP_FRAGMENT" null + } else if (tagQueryString) { "$DDTags.HTTP_QUERY" expectedQuery "$DDTags.HTTP_FRAGMENT" { it == null || it == uri.fragment @@ -1041,9 +1060,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 +1120,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/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/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/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/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/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 From 36f08f9dc3d5450af74cac12adfd433a202e8bcc Mon Sep 17 00:00:00 2001 From: Zach Montoya Date: Thu, 1 Oct 2026 15:15:29 -0700 Subject: [PATCH 06/22] Test end-to-end OpenTelemetry HTTP server semantics - Akka-Http Server - JAX-RS Server - Jersey Server - Jetty - Netty HTTP server - Servlet - Play HTTP server - Ratpack HTTP server - Spring MVC - Spring WebFlux - Tomcat - Undertow - Vert.x --- .../agent/test/base/HttpServerTest.groovy | 76 ++++++++++++--- .../AkkaHttpServerInstrumentationTest.groovy | 4 + .../jersey/jersey-2.0/build.gradle | 1 + .../jersey2/Jersey2JettyTest.groovy | 4 + .../src/test/groovy/Jetty11Test.groovy | 4 + .../src/test/groovy/Netty41ServerTest.groovy | 5 +- .../play26/server/PlayServerTest.groovy | 5 + .../server/AbstractPlayServerTest.groovy | 25 ++++- .../server/RatpackHttpServerTest.groovy | 41 +++++--- ...axRsAnnotations2InstrumentationTest.groovy | 40 +++++--- .../src/test/groovy/JettyServlet3Test.groovy | 4 + .../spring-webflux-6.0/build.gradle | 1 + .../bootTest/groovy/SpringWebfluxTest.groovy | 95 ++++++++++++------- .../boot/SpringBootBasedTest.groovy | 8 ++ .../src/test/groovy/TomcatServletTest.groovy | 4 + .../src/test/groovy/UndertowTest.groovy | 4 + .../server/VertxHttpServerForkedTest.groovy | 8 ++ 17 files changed, 250 insertions(+), 79 deletions(-) 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..00882783093 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,17 @@ 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 + "$Tags.HTTP_HOSTNAME" null + "$KnownTags.SERVER_ADDRESS_NAME" address.host + "$KnownTags.URL_PATH_NAME" expectedPath + "$KnownTags.URL_SCHEME_NAME" requestUrl.scheme + "$KnownTags.SERVER_PORT_NAME" requestUrl.port + } else { + "$Tags.HTTP_HOSTNAME" address.host + "$Tags.HTTP_URL" "$expectedUrl" + } "$Tags.HTTP_METHOD" method "$Tags.HTTP_STATUS" expectedStatus "$Tags.HTTP_USER_AGENT" String @@ -2723,8 +2764,17 @@ 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 + if (otelSemantics) { + "$KnownTags.URL_QUERY_NAME" expectedQueryTag + "$DDTags.HTTP_QUERY" null + } else { + "$DDTags.HTTP_QUERY" expectedQueryTag + } } if ({ isDataStreamsEnabled() 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/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/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..358e2028fac 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,17 @@ 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" null + "$KnownTags.SERVER_ADDRESS_NAME" 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 +160,12 @@ class AbstractPlayServerTest extends HttpServerTest { errorTags(endpoint == CUSTOM_EXCEPTION ? TestHttpErrorHandler.CustomRuntimeException : RuntimeException, endpoint.body) } if (endpoint.query) { - "$DDTags.HTTP_QUERY" expectedQueryTag + if (otelSemantics) { + "$KnownTags.URL_QUERY_NAME" expectedQueryTag + "$DDTags.HTTP_QUERY" null + } 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..49590dbb748 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,35 @@ 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" null + "$KnownTags.SERVER_ADDRESS_NAME" 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(otelSemantics ? KnownTags.URL_QUERY_NAME : 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/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..66a2ccd79a4 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.SERVER_ADDRESS_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.URL_QUERY_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) { + "$KnownTags.SERVER_ADDRESS_NAME" uri.host + "$KnownTags.SERVER_PORT_NAME" 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 { +} From 4fe3e789e2bc5f82fbacd074cc6001a767cd3d0a Mon Sep 17 00:00:00 2001 From: Zach Montoya Date: Fri, 16 Oct 2026 10:49:05 -0700 Subject: [PATCH 07/22] PR feedback on logic/processing, avoiding tags definitions: - Restore IP tags collection so it is opt-in. The DD->OTel tag mapping remains - Restore query tags collection so it honors the user's query tag opt-out - Improve OtelHttpSemantics.parsePort performance (especially since this can be influenced by a user) - Stores the OTel http client span's "url.full" URL in a lazy wrapper so redaction and query removal only occur at export time --- .../decorator/HttpServerDecorator.java | 6 +- ...HttpDecoratorsOtelSemanticsForkedTest.java | 127 +++++++++++++++++- .../agent/test/base/HttpClientTest.groovy | 2 +- .../api/OtelHttpSemantics.java | 53 ++++++-- .../api/OtelHttpSemanticsTest.java | 48 +++++-- 5 files changed, 206 insertions(+), 30 deletions(-) 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 a893faacd42..adbcbec7f4c 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 @@ -291,9 +291,7 @@ protected void doOnRequest( // - peer.ipv4 // - peer.ipv6 final boolean shouldTagIps = - OTEL_SEMANTICS - || config.isClientIpEnabled() - || traceClientIpResolverEnabled && APPSEC_ACTIVE; + config.isClientIpEnabled() || traceClientIpResolverEnabled && APPSEC_ACTIVE; // Whether to stash IP data for later tagging or not. // AI Guard requires client IP tags on the local root span when an ai_guard span is created. // Resolve the IPs eagerly but do not tag the span yet; stash them on the request context so @@ -392,7 +390,7 @@ protected void doOnRequest( } } - if (valid && (OTEL_SEMANTICS || config.isHttpServerTagQueryString())) { + if (valid && config.isHttpServerTagQueryString()) { String query = supportsRaw && config.isHttpServerRawQueryString() ? url.rawQuery() : url.query(); if (OTEL_SEMANTICS) { 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 index ec36e6d897e..3b949697092 100644 --- 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 @@ -4,7 +4,10 @@ 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_TAG_QUERY_STRING; +import static datadog.trace.api.config.TracerConfig.CLIENT_IP_ENABLED; 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 org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; @@ -15,6 +18,7 @@ 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.common.writer.ListWriter; @@ -54,6 +58,7 @@ void clientDecoratorCapturesOtelHttpValues() throws Exception { 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)); @@ -61,7 +66,7 @@ void clientDecoratorCapturesOtelHttpValues() throws Exception { assertEquals( "https://REDACTED:REDACTED@example.com/orders#fragment", span.getTag(KnownTags.HTTP_URL_OTEL_NAME).toString()); - assertEquals("token=secret", span.getTag(KnownTags.URL_QUERY_NAME)); + assertNull(span.getTag(KnownTags.URL_QUERY_NAME)); assertEquals("example.com", span.getTag(KnownTags.SERVER_ADDRESS_NAME)); assertEquals(443, span.getTag(KnownTags.SERVER_PORT_NAME)); assertEquals(503, span.getHttpStatusCode()); @@ -72,14 +77,57 @@ void clientDecoratorCapturesOtelHttpValues() throws Exception { } @Test - void urlConnectionDecoratorCapturesQueryForObfuscation() throws Exception { + 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()); + assertNull(span.getTag(KnownTags.URL_QUERY_NAME)); + } + + @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("https://example.com/orders#fragment", span.getTag(KnownTags.HTTP_URL_OTEL_NAME)); assertEquals("token=secret", span.getTag(KnownTags.URL_QUERY_NAME)); + + span.processTagsAndBaggage(NO_OP, OWN_PROPAGATION_TAGS); + + assertEquals( + "https://REDACTED:REDACTED@example.com/orders?#fragment", + span.getTag(KnownTags.HTTP_URL_OTEL_NAME).toString()); + assertNull(span.getTag(KnownTags.URL_QUERY_NAME)); + } + + @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.URL_QUERY_NAME)); + + span.processTagsAndBaggage(NO_OP, OWN_PROPAGATION_TAGS); + + assertEquals( + "https://REDACTED:REDACTED@example.com/orders?#fragment", + span.getTag(KnownTags.HTTP_URL_OTEL_NAME).toString()); + assertNull(span.getTag(KnownTags.URL_QUERY_NAME)); } @Test @@ -93,22 +141,89 @@ void serverDecoratorCapturesOtelHttpValuesAndRouteName() throws Exception { 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)); - assertEquals("view=full", span.getTag(KnownTags.URL_QUERY_NAME)); + assertNull(span.getTag(KnownTags.URL_QUERY_NAME)); assertEquals("example.com", span.getTag(KnownTags.SERVER_ADDRESS_NAME)); assertEquals(8443, span.getTag(KnownTags.SERVER_PORT_NAME)); - assertEquals("192.0.2.10", span.getTag(KnownTags.HTTP_CLIENT_IP_OTEL_NAME)); - assertEquals("192.0.2.10", span.getTag(KnownTags.NETWORK_CLIENT_IP_OTEL_NAME)); + assertNull(span.getTag(KnownTags.HTTP_CLIENT_IP_OTEL_NAME)); + assertNull(span.getTag(KnownTags.NETWORK_CLIENT_IP_OTEL_NAME)); 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.URL_QUERY_NAME)); + + span.processTagsAndBaggage(NO_OP, OWN_PROPAGATION_TAGS); + + assertEquals("&view=full", span.getTag(KnownTags.URL_QUERY_NAME)); + assertEquals("/orders", span.getTag(KnownTags.URL_PATH_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(KnownTags.HTTP_CLIENT_IP_OTEL_NAME)); + assertNull(span.getTag(KnownTags.NETWORK_CLIENT_IP_OTEL_NAME)); + 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(KnownTags.NETWORK_CLIENT_IP_OTEL_NAME)); + assertEquals(forwardedIp, span.getTag(Tags.HTTP_FORWARDED_IP)); + assertEquals(headers.forwarded, span.getTag(Tags.HTTP_FORWARDED)); + } + @TableTest({ "Scenario | Request URI | Expected Path", "relative path | orders | /orders ", 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 2f2394d78c3..43eadb74544 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 @@ -994,7 +994,7 @@ abstract class HttpClientTest extends VersionedNamingTestBase { Map extraTags = null) { def otelSemantics = Config.get().isTraceOtelSemanticsEnabled() - def expectedQuery = otelSemantics || tagQueryString ? uri.query : null + 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" 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 index 5b23167fd88..8e4fd79e7b1 100644 --- 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 @@ -1,5 +1,6 @@ package datadog.trace.bootstrap.instrumentation.api; +import datadog.trace.api.Config; import datadog.trace.api.DDTags; import datadog.trace.api.KnownTags; import datadog.trace.api.cache.RadixTreeCache; @@ -37,11 +38,15 @@ public static void setMethodResourceName(final AgentSpan span, final CharSequenc } public static void setClientUrl(final AgentSpan span, final URI url) { - span.setTag(KnownTags.HTTP_URL_OTEL_NAME, withoutQuery(redactedUrl(url))); - // QueryObfuscator scrubs this before rebuilding url.full and removes it from client spans. - final String query = url.getRawQuery(); - if (query != null) { - span.setTag(KnownTags.URL_QUERY_NAME, query); + // Use the object tag setter rather than the CharSequence overload, which checks length(), which + // would render the URL immediately. + span.setTag(KnownTags.HTTP_URL_OTEL_NAME, (Object) new ClientUrl(url)); + if (Config.get().isHttpClientTagQueryString()) { + // QueryObfuscator scrubs this before rebuilding url.full and removes it from client spans. + final String query = url.getRawQuery(); + if (query != null) { + span.setTag(KnownTags.URL_QUERY_NAME, query); + } } } @@ -164,14 +169,42 @@ public static void setServerAddressAndPort( } public static int parsePort(final String value) { - if (value == null || value.isEmpty()) { + if (value == null) { return -1; } - try { - final int port = Integer.parseInt(value); - return port > 0 && port <= 65535 ? port : -1; - } catch (NumberFormatException ignored) { + final int length = value.length(); + if (length == 0 || length > 5) { return -1; } + int port = 0; + for (int i = 0; i < length; i++) { + final char c = value.charAt(i); + if (c < '0' || c > '9') { + return -1; + } + port = port * 10 + (c - '0'); + } + return port > 0 && port <= 65535 ? port : -1; + } + + /** Defers client URL redaction and query removal until the URL is read. */ + 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() { + String value = lazy; + return value != null ? value : withoutQuery(redactedUrl(uri)); + } } } 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 index 1f588967368..4a37422ef01 100644 --- 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 @@ -13,10 +13,12 @@ import datadog.trace.api.DDTags; import datadog.trace.api.KnownTags; +import datadog.trace.bootstrap.instrumentation.api.URIUtils.LazyUrl; import java.net.URI; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.ArgumentCaptor; import org.tabletest.junit.TableTest; class OtelHttpSemanticsTest { @@ -83,12 +85,19 @@ void setMethodResourceNameUsesHttpForNullMethod() { } @Test - void setClientUrlStripsQueryAndRedactsCredentials() { + void setClientUrlLazilyStripsQueryAndRedactsCredentials() { AgentSpan span = mock(AgentSpan.class); + ArgumentCaptor urlCaptor = ArgumentCaptor.forClass(LazyUrl.class); - OtelHttpSemantics.setClientUrl(span, URI.create("https://u:p@host/a?token=secret")); + OtelHttpSemantics.setClientUrl(span, URI.create("https://u:p@host/a?token=secret#fragment")); - verify(span).setTag(KnownTags.HTTP_URL_OTEL_NAME, "https://REDACTED:REDACTED@host/a"); + verify(span).setTag(eq(KnownTags.HTTP_URL_OTEL_NAME), (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()); verify(span).setTag(KnownTags.URL_QUERY_NAME, "token=secret"); } @@ -98,7 +107,6 @@ void setClientUrlOmitsQueryTagWhenAbsent() { OtelHttpSemantics.setClientUrl(span, URI.create("http://host/a")); - verify(span).setTag(KnownTags.HTTP_URL_OTEL_NAME, "http://host/a"); verify(span, never()).setTag(eq(KnownTags.URL_QUERY_NAME), anyString()); } @@ -190,11 +198,16 @@ void forwardedValueReturnsNullForNullHeader() { } @TableTest({ - "Scenario | Value | Expected", - "Valid port | 8080 | 8080 ", - "Minimum port | 1 | 1 ", - "Maximum port | 65535 | 65535 ", - "Invalid port | {0, 65536, -1, abc} | -1 " + "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, 2147483647, 2147483648} | -1 " }) void parsePortRejectsOutOfRangeAndMalformedValues(String value, int expected) { assertEquals(expected, OtelHttpSemantics.parsePort(value)); @@ -275,6 +288,23 @@ void setServerAddressAndPortPrefersForwardedPortHeader() { verify(span).setTag(KnownTags.SERVER_PORT_NAME, 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 setServerAddressAndPortFallsBackFromInvalidForwardedPort( + String forwardedHost, String forwardedPort, int expectedPort) { + AgentSpan span = mock(AgentSpan.class); + + OtelHttpSemantics.setServerAddressAndPort( + span, forwardedHost, forwardedPort, "http", "host", 8080); + + verify(span).setTag(KnownTags.SERVER_PORT_NAME, expectedPort); + } + @TableTest({ "Scenario | Forwarded Host | Expected Address | Expected Port", "Host only | proxy | proxy | 80 ", From 01377cceb14ef3e2aa8e8493ca29ea59981b2c6b Mon Sep 17 00:00:00 2001 From: Zach Montoya Date: Fri, 16 Oct 2026 12:00:40 -0700 Subject: [PATCH 08/22] PR feedback on tag-conventions: Restore network.client.ip so it is not span-kind-neutral --- ...HttpDecoratorsOtelSemanticsForkedTest.java | 10 ++++---- .../trace/OtlpTraceJsonCollectorTest.java | 24 +++++++++++++++++++ .../java/datadog/trace/api/KnownTagsTest.java | 23 +++++++++++++++++- tag-conventions.yaml | 2 +- 4 files changed, 52 insertions(+), 7 deletions(-) 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 index 3b949697092..55a5bc41671 100644 --- 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 @@ -150,8 +150,8 @@ void serverDecoratorCapturesOtelHttpValuesAndRouteName() throws Exception { assertNull(span.getTag(KnownTags.URL_QUERY_NAME)); assertEquals("example.com", span.getTag(KnownTags.SERVER_ADDRESS_NAME)); assertEquals(8443, span.getTag(KnownTags.SERVER_PORT_NAME)); - assertNull(span.getTag(KnownTags.HTTP_CLIENT_IP_OTEL_NAME)); - assertNull(span.getTag(KnownTags.NETWORK_CLIENT_IP_OTEL_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()); @@ -190,8 +190,8 @@ void serverDecoratorOmitsClientIpsAndForwardedHeadersByDefault() throws Exceptio decorator.onRequest( span, new Connection("192.0.2.10", 32100), request, AgentSpan.fromSpanContext(extracted)); - assertNull(span.getTag(KnownTags.HTTP_CLIENT_IP_OTEL_NAME)); - assertNull(span.getTag(KnownTags.NETWORK_CLIENT_IP_OTEL_NAME)); + 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)); } @@ -219,7 +219,7 @@ void serverDecoratorCapturesClientIpsWithOptIn(String forwardedIp, String expect forwardedIp == null ? root() : AgentSpan.fromSpanContext(extracted)); assertEquals(expectedClientIp, span.getTag(KnownTags.HTTP_CLIENT_IP_OTEL_NAME)); - assertEquals("192.0.2.10", span.getTag(KnownTags.NETWORK_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)); } 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 716a8b3dd64..99c18dd79a2 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 @@ -228,6 +228,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(); @@ -237,6 +260,7 @@ 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("network.client.ip", "192.0.2.10"); 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 5b85361fa8a..7443e02a41b 100644 --- a/internal-api/src/test/java/datadog/trace/api/KnownTagsTest.java +++ b/internal-api/src/test/java/datadog/trace/api/KnownTagsTest.java @@ -76,7 +76,6 @@ static Stream otelNamedTags() { Arguments.of("url.full", KnownTags.HTTP_URL_ID, "http.url"), 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("network.peer.address", KnownTags.NETWORK_CLIENT_IP_ID, "network.client.ip"), Arguments.of("db.system", KnownTags.DB_TYPE_ID, "db.type"), Arguments.of("db.operation.name", KnownTags.DB_OPERATION_ID, "db.operation"), Arguments.of("db.query.text", KnownTags.DB_STATEMENT_ID, "db.statement"), @@ -172,6 +171,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/tag-conventions.yaml b/tag-conventions.yaml index 2d989e7206a..29bd206cfa9 100644 --- a/tag-conventions.yaml +++ b/tag-conventions.yaml @@ -137,7 +137,7 @@ span_types: - { 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. - - { dd-name: network.client.ip, type: string, required: recommended, otel-name: network.peer.address, span-kind-neutral: true } # Temporary direction-free alias until name resolution is span-kind-aware + - { dd-name: network.client.ip, type: string, required: recommended, otel-name: network.peer.address } # Forwarding headers, when client IPs are tagged and the header is present. - { dd-name: http.forwarded, type: string, required: optional } - { dd-name: http.forwarded.proto, type: string, required: optional } From 2ac5dd204d9678254cc875cf1289bda00a904b0a Mon Sep 17 00:00:00 2001 From: Zach Montoya Date: Fri, 16 Oct 2026 12:23:29 -0700 Subject: [PATCH 09/22] PR feedback on tag-conventions: Fix the tag definitions of OTel server.port and server.address while keeping the span direction relationships correct HTTP server spans: - http.server: restore "http.hostname" => OTel "server.address" - http.server: add standalone (OTel-only) "server.port" HTTP client spans: - outbound_peer: restore "peer.hostname" => OTel "server.address" - outbound_peer: restore "peer.port" => OTel "server.port" --- .../decorator/UriBasedClientDecorator.java | 2 +- ...HttpDecoratorsOtelSemanticsForkedTest.java | 12 ++++----- .../agent/test/base/HttpClientTest.groovy | 26 +++++++------------ .../agent/test/base/HttpServerTest.groovy | 5 ++-- .../server/AbstractPlayServerTest.groovy | 3 +-- .../server/RatpackHttpServerTest.groovy | 3 +-- .../bootTest/groovy/SpringWebfluxTest.groovy | 7 ++--- .../trace/OtlpTraceJsonCollectorTest.java | 9 +++---- .../trace/api/gateway/InferredProxySpan.java | 2 +- .../api/OtelHttpSemantics.java | 2 +- .../java/datadog/trace/api/KnownTagsTest.java | 7 ++--- .../api/OtelHttpSemanticsTest.java | 12 ++++----- tag-conventions.yaml | 9 +++---- 13 files changed, 43 insertions(+), 56 deletions(-) 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 68c1b42d876..143964842b3 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 @@ -21,7 +21,7 @@ public void onURI(@Nonnull final AgentSpan span, @Nonnull final URI uri) { String host = uri.getHost(); if (null != host && !host.isEmpty()) { if (OTEL_SEMANTICS) { - span.setTag(KnownTags.SERVER_ADDRESS_NAME, host); + span.setTag(KnownTags.PEER_HOSTNAME_NAME, host); int serverPort = OtelHttpSemantics.serverPort(uri); if (serverPort > 0) { span.setTag(KnownTags.SERVER_PORT_NAME, serverPort); 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 index 55a5bc41671..bb4cde1d7c2 100644 --- 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 @@ -67,13 +67,11 @@ void clientDecoratorCapturesOtelHttpValues() throws Exception { "https://REDACTED:REDACTED@example.com/orders#fragment", span.getTag(KnownTags.HTTP_URL_OTEL_NAME).toString()); assertNull(span.getTag(KnownTags.URL_QUERY_NAME)); - assertEquals("example.com", span.getTag(KnownTags.SERVER_ADDRESS_NAME)); - assertEquals(443, span.getTag(KnownTags.SERVER_PORT_NAME)); + 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()); - assertNull(span.getTag("peer.hostname")); - assertNull(span.getTag("peer.port")); } @Test @@ -148,7 +146,7 @@ void serverDecoratorCapturesOtelHttpValuesAndRouteName() throws Exception { assertEquals("/users/123", span.getTag(KnownTags.URL_PATH_NAME)); assertEquals("https", span.getTag(KnownTags.URL_SCHEME_NAME)); assertNull(span.getTag(KnownTags.URL_QUERY_NAME)); - assertEquals("example.com", span.getTag(KnownTags.SERVER_ADDRESS_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)); @@ -256,7 +254,7 @@ void serverDecoratorUsesForwardedOrigin() throws Exception { 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.SERVER_ADDRESS_NAME)); + assertEquals("public.example.com", span.getTag(KnownTags.HTTP_HOSTNAME_NAME)); assertEquals(8443, span.getTag(KnownTags.SERVER_PORT_NAME)); } @@ -277,7 +275,7 @@ void serverDecoratorUsesForwardedPortWithIpv6Host() throws Exception { 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.SERVER_ADDRESS_NAME)); + assertEquals("2001:db8::1", span.getTag(KnownTags.HTTP_HOSTNAME_NAME)); assertEquals(9443, span.getTag(KnownTags.SERVER_PORT_NAME)); } 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 43eadb74544..8dfa5845331 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 @@ -1019,22 +1019,16 @@ abstract class HttpClientTest extends VersionedNamingTestBase { tags { "$Tags.COMPONENT" component "$Tags.SPAN_KIND" Tags.SPAN_KIND_CLIENT - if (otelSemantics) { - "$Tags.PEER_HOSTNAME" null - "$Tags.PEER_HOST_IPV4" null - "$Tags.PEER_PORT" null - "$KnownTags.SERVER_ADDRESS_NAME" uri.host - "$KnownTags.SERVER_PORT_NAME" OtelHttpSemantics.serverPort(uri) - } else { - "$Tags.PEER_HOSTNAME" { - it == uri.host || ignorePeer - } - "$Tags.PEER_HOST_IPV4" { - it == null || it == "127.0.0.1" || ignorePeer - } // Optional - "$Tags.PEER_PORT" { - it == null || it == uri.port || it == proxy.port || it == 443 || ignorePeer - } + "$Tags.PEER_HOSTNAME" { + it == uri.host || ignorePeer + } + "$Tags.PEER_HOST_IPV4" { + it == null || it == "127.0.0.1" || ignorePeer + } // Optional + "$Tags.PEER_PORT" { + otelSemantics + ? it == OtelHttpSemantics.serverPort(uri) + : (it == null || it == uri.port || it == proxy.port || it == 443 || ignorePeer) } "$Tags.HTTP_URL" expectedUrl "$Tags.HTTP_METHOD" method 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 00882783093..92490b94324 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 @@ -2740,11 +2740,10 @@ abstract class HttpServerTest extends WithHttpServer { } if (otelSemantics) { "$Tags.HTTP_URL" null - "$Tags.HTTP_HOSTNAME" null - "$KnownTags.SERVER_ADDRESS_NAME" address.host + "$KnownTags.HTTP_HOSTNAME_NAME" address.host + "$KnownTags.SERVER_PORT_NAME" requestUrl.port "$KnownTags.URL_PATH_NAME" expectedPath "$KnownTags.URL_SCHEME_NAME" requestUrl.scheme - "$KnownTags.SERVER_PORT_NAME" requestUrl.port } else { "$Tags.HTTP_HOSTNAME" address.host "$Tags.HTTP_URL" "$expectedUrl" 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 358e2028fac..b60df98d54a 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 @@ -144,8 +144,7 @@ class AbstractPlayServerTest extends HttpServerTest { "$Tags.NETWORK_CLIENT_IP" '127.0.0.1' if (otelSemantics) { "$Tags.HTTP_URL" null - "$Tags.HTTP_HOSTNAME" null - "$KnownTags.SERVER_ADDRESS_NAME" address.host + "$Tags.HTTP_HOSTNAME" address.host "$KnownTags.SERVER_PORT_NAME" address.port "$KnownTags.URL_PATH_NAME" expectedPath "$KnownTags.URL_SCHEME_NAME" address.scheme 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 49590dbb748..db23561dee3 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 @@ -148,8 +148,7 @@ class RatpackHttpServerTest extends HttpServerTest { "$Tags.PEER_PORT" Integer if (otelSemantics) { "$Tags.HTTP_URL" null - "$Tags.HTTP_HOSTNAME" null - "$KnownTags.SERVER_ADDRESS_NAME" address.host + "$Tags.HTTP_HOSTNAME" address.host "$KnownTags.SERVER_PORT_NAME" address.port "$KnownTags.URL_PATH_NAME" requestUrl.rawPath "$KnownTags.URL_SCHEME_NAME" address.scheme 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 66a2ccd79a4..0759b96f64e 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 @@ -1,4 +1,5 @@ import datadog.trace.agent.test.base.OkHttpWebsocketClient +import datadog.trace.bootstrap.instrumentation.api.OtelHttpSemantics import dd.trace.instrumentation.springwebflux.server.WsHandler import net.bytebuddy.utility.RandomString import org.springframework.beans.factory.annotation.Autowired @@ -807,7 +808,7 @@ class SpringWebfluxHttp11Test extends InstrumentationSpecification { void serverUrlTags(TagsAssert tags, String url) { def uri = URI.create(url) if (Config.get().isTraceOtelSemanticsEnabled()) { - tags.tag(KnownTags.SERVER_ADDRESS_NAME, uri.host) + 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) @@ -863,8 +864,8 @@ class SpringWebfluxHttp11Test extends InstrumentationSpecification { "$Tags.PEER_PORT" { it == null || it == uri.port } } if (otelSemantics) { - "$KnownTags.SERVER_ADDRESS_NAME" uri.host - "$KnownTags.SERVER_PORT_NAME" uri.port + "$Tags.PEER_HOSTNAME" uri.host + "$Tags.PEER_PORT" uri.port } "$Tags.HTTP_URL" expectedUrl "$Tags.HTTP_METHOD" method 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 99c18dd79a2..496cc01d76b 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 @@ -145,15 +145,12 @@ void tagsAreEmittedUnderTheirDatadogNameWhenOtelSemanticsDisabled() throws IOExc "a tag the registry does not name passes through unchanged; got " + attrKeys); } - // Restore the commented out pairings when an OTel name can also be qualified by a direction. - // Currently, OTel tags server.address and server.port represent the "peer" on client spans - // and the "self" on server spans @TableTest({ "scenario | kind | tag | otelName ", - // "client hostname | client | peer.hostname | server.address ", - // "server hostname | server | http.hostname | server.address ", + "client hostname | client | peer.hostname | server.address ", + "server hostname | server | http.hostname | server.address ", "server client ip | server | network.client.ip | network.peer.address", - // "client peer port | client | peer.port | server.port ", + "client peer port | client | peer.port | server.port ", "server peer port | server | peer.port | client.port " }) void aDirectionScopedTagIsEmittedUnderTheNameForTheSpansKind( 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 6eed4b07b77..f9939385b0d 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 @@ -119,7 +119,7 @@ public AgentSpanContext start(AgentSpanContext extracted) { span.setTag(KnownTags.URL_PATH_NAME, path); if (domainName != null && !domainName.isEmpty()) { span.setTag(KnownTags.URL_SCHEME_NAME, "https"); - span.setTag(KnownTags.SERVER_ADDRESS_NAME, domainName); + span.setTag(KnownTags.HTTP_HOSTNAME_NAME, domainName); } } else { span.setTag(HTTP_METHOD, httpMethod); 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 index 8e4fd79e7b1..55e5d24580c 100644 --- 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 @@ -153,7 +153,7 @@ public static void setServerAddressAndPort( } if (serverAddress != null && !serverAddress.isEmpty()) { - span.setTag(KnownTags.SERVER_ADDRESS_NAME, serverAddress); + span.setTag(KnownTags.HTTP_HOSTNAME_NAME, serverAddress); } int serverPort = parsePort(forwardedValue(forwardedPortHeader)); 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 7443e02a41b..c80730fa2c9 100644 --- a/internal-api/src/test/java/datadog/trace/api/KnownTagsTest.java +++ b/internal-api/src/test/java/datadog/trace/api/KnownTagsTest.java @@ -45,7 +45,7 @@ static Stream knownTags() { 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(KnownTags.SERVER_ADDRESS_NAME, KnownTags.SERVER_ADDRESS_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.URL_QUERY_NAME, KnownTags.URL_QUERY_ID), @@ -207,8 +207,9 @@ void aNameDeclaredPerDirectionIsOneTag() { assertEquals( "client.port", KnownTagCodec.openTelemetryTagOf(KnownTags.PEER_PORT_ID, KnownTagCodec.DIRECTION_INBOUND)); - // Temporarily remove "server.port" == openTelemetryTagOf(peer.port, outbound) because - // server.port in OTel is also used by server spans + assertEquals( + "server.port", + KnownTagCodec.openTelemetryTagOf(KnownTags.PEER_PORT_ID, KnownTagCodec.DIRECTION_OUTBOUND)); assertEquals( Tags.PEER_PORT, KnownTagCodec.openTelemetryTagOf(KnownTags.PEER_PORT_ID, KnownTagCodec.DIRECTION_NONE)); 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 index 4a37422ef01..29cebe35228 100644 --- 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 @@ -244,7 +244,7 @@ void setServerAddressAndPortUsesRequestHostAndPort() { OtelHttpSemantics.setServerAddressAndPort(span, null, null, "http", "host", 8080); - verify(span).setTag(KnownTags.SERVER_ADDRESS_NAME, "host"); + verify(span).setTag(KnownTags.HTTP_HOSTNAME_NAME, "host"); verify(span).setTag(KnownTags.SERVER_PORT_NAME, 8080); } @@ -254,7 +254,7 @@ void setServerAddressAndPortFallsBackToSchemeDefaultPort() { OtelHttpSemantics.setServerAddressAndPort(span, null, null, "https", "host", -1); - verify(span).setTag(KnownTags.SERVER_ADDRESS_NAME, "host"); + verify(span).setTag(KnownTags.HTTP_HOSTNAME_NAME, "host"); verify(span).setTag(KnownTags.SERVER_PORT_NAME, 443); } @@ -264,7 +264,7 @@ void setServerAddressAndPortOmitsTagsWhenNothingResolvable() { OtelHttpSemantics.setServerAddressAndPort(span, null, null, "ftp", null, -1); - verify(span, never()).setTag(eq(KnownTags.SERVER_ADDRESS_NAME), anyString()); + verify(span, never()).setTag(eq(KnownTags.HTTP_HOSTNAME_NAME), anyString()); verify(span, never()).setTag(eq(KnownTags.SERVER_PORT_NAME), anyInt()); } @@ -274,7 +274,7 @@ void setServerAddressAndPortOmitsAddressWhenRequestHostIsEmpty() { OtelHttpSemantics.setServerAddressAndPort(span, null, null, "http", "", -1); - verify(span, never()).setTag(eq(KnownTags.SERVER_ADDRESS_NAME), anyString()); + verify(span, never()).setTag(eq(KnownTags.HTTP_HOSTNAME_NAME), anyString()); verify(span).setTag(KnownTags.SERVER_PORT_NAME, 80); } @@ -284,7 +284,7 @@ void setServerAddressAndPortPrefersForwardedPortHeader() { OtelHttpSemantics.setServerAddressAndPort(span, "proxy:8443", "9443", "http", "host", 8080); - verify(span).setTag(KnownTags.SERVER_ADDRESS_NAME, "proxy"); + verify(span).setTag(KnownTags.HTTP_HOSTNAME_NAME, "proxy"); verify(span).setTag(KnownTags.SERVER_PORT_NAME, 9443); } @@ -322,7 +322,7 @@ void setServerAddressAndPortParsesForwardedHost( OtelHttpSemantics.setServerAddressAndPort(span, forwardedHost, null, "http", "host", 8080); - verify(span).setTag(KnownTags.SERVER_ADDRESS_NAME, expectedAddress); + verify(span).setTag(KnownTags.HTTP_HOSTNAME_NAME, expectedAddress); // A forwarded host suppresses the request port, so the scheme default applies instead. verify(span).setTag(KnownTags.SERVER_PORT_NAME, expectedPort); } diff --git a/tag-conventions.yaml b/tag-conventions.yaml index 29bd206cfa9..ec44d23bb6a 100644 --- a/tag-conventions.yaml +++ b/tag-conventions.yaml @@ -119,8 +119,6 @@ span_types: - { 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: server.address, type: string, required: conditional } # OTel-only; set only while OTel semantics are enabled - - { dd-name: server.port, type: int, required: conditional } # OTel-only; set only while OTel semantics are enabled http.server: extends: http @@ -128,7 +126,8 @@ span_types: include: [ peer_address, inbound_peer ] tags: - { dd-name: http.route, type: string, required: conditional } # passes through: dd-name already is the OTel name - - { dd-name: http.hostname, type: string, required: required } + - { 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: url.path, type: string, required: conditional } # OTel-only; server replacement for http.url - { dd-name: url.query, type: string, required: conditional } # OTel-only; server replacement for http.url @@ -385,8 +384,8 @@ mixins: outbound_peer: span-kind: client tags: - - { dd-name: peer.hostname, type: string, required: recommended } - - { dd-name: peer.port, type: int } + - { dd-name: peer.hostname, type: string, required: recommended, otel-name: server.address } + - { dd-name: peer.port, type: int, otel-name: server.port } # inbound_peer — the other end of an inbound connection: the client. inbound_peer: From f0f092df556f44826c7ca5548c4139af41ab71a4 Mon Sep 17 00:00:00 2001 From: Zach Montoya Date: Fri, 16 Oct 2026 14:59:51 -0700 Subject: [PATCH 10/22] Polish: Update the obfuscator tag name for OpenTelemetry semantics to DDTags.HTTP_QUERY_OTEL_NAME, which mimics DDTags.HTTP_QUERY so we have one consistent tag name --- .../instrumentation/decorator/HttpServerDecorator.java | 2 +- .../decorator/HttpDecoratorsOtelSemanticsForkedTest.java | 4 ---- .../datadog/trace/agent/test/base/HttpClientTest.groovy | 2 +- .../datadog/trace/agent/test/base/HttpServerTest.groovy | 3 +-- .../play26/server/AbstractPlayServerTest.groovy | 3 +-- .../src/test/groovy/server/RatpackHttpServerTest.groovy | 2 +- dd-trace-api/src/main/java/datadog/trace/api/DDTags.java | 1 + .../java/datadog/trace/core/tagprocessor/QueryObfuscator.java | 3 +-- .../datadog/trace/core/tagprocessor/QueryObfuscatorTest.java | 4 ++-- .../bootstrap/instrumentation/api/OtelHttpSemantics.java | 2 +- 10 files changed, 10 insertions(+), 16 deletions(-) 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 adbcbec7f4c..840d91867c2 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 @@ -395,7 +395,7 @@ protected void doOnRequest( supportsRaw && config.isHttpServerRawQueryString() ? url.rawQuery() : url.query(); if (OTEL_SEMANTICS) { if (query != null && !query.isEmpty()) { - span.setTag(KnownTags.URL_QUERY_NAME, query); + span.setTag(DDTags.HTTP_QUERY_OTEL_NAME, query); } } else { span.setTag(DDTags.HTTP_QUERY, query); 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 index bb4cde1d7c2..15c3995bb36 100644 --- 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 @@ -66,7 +66,6 @@ void clientDecoratorCapturesOtelHttpValues() throws Exception { assertEquals( "https://REDACTED:REDACTED@example.com/orders#fragment", span.getTag(KnownTags.HTTP_URL_OTEL_NAME).toString()); - assertNull(span.getTag(KnownTags.URL_QUERY_NAME)); assertEquals("example.com", span.getTag(Tags.PEER_HOSTNAME)); assertEquals(443, span.getTag(Tags.PEER_PORT)); assertEquals(503, span.getHttpStatusCode()); @@ -86,7 +85,6 @@ void urlConnectionDecoratorRespectsQueryStringOptOut() throws Exception { assertEquals( "https://example.com/orders#fragment", span.getTag(KnownTags.HTTP_URL_OTEL_NAME).toString()); - assertNull(span.getTag(KnownTags.URL_QUERY_NAME)); } @Test @@ -106,7 +104,6 @@ void clientDecoratorCapturesAndObfuscatesQueryWhenEnabled() throws Exception { assertEquals( "https://REDACTED:REDACTED@example.com/orders?#fragment", span.getTag(KnownTags.HTTP_URL_OTEL_NAME).toString()); - assertNull(span.getTag(KnownTags.URL_QUERY_NAME)); } @Test @@ -125,7 +122,6 @@ void urlConnectionDecoratorCapturesAndObfuscatesQueryWhenEnabled() throws Except assertEquals( "https://REDACTED:REDACTED@example.com/orders?#fragment", span.getTag(KnownTags.HTTP_URL_OTEL_NAME).toString()); - assertNull(span.getTag(KnownTags.URL_QUERY_NAME)); } @Test 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 8dfa5845331..244e3fdc650 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 @@ -1036,7 +1036,7 @@ abstract class HttpClientTest extends VersionedNamingTestBase { "$Tags.HTTP_STATUS" status } if (otelSemantics) { - "$DDTags.HTTP_QUERY" null + "$DDTags.HTTP_QUERY_OTEL_NAME" null "$KnownTags.URL_QUERY_NAME" null "$DDTags.HTTP_FRAGMENT" null } else if (tagQueryString) { 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 92490b94324..f2be4d853fd 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 @@ -2769,8 +2769,7 @@ abstract class HttpServerTest extends WithHttpServer { } if (endpoint.query) { if (otelSemantics) { - "$KnownTags.URL_QUERY_NAME" expectedQueryTag - "$DDTags.HTTP_QUERY" null + "$DDTags.HTTP_QUERY_OTEL_NAME" expectedQueryTag } else { "$DDTags.HTTP_QUERY" expectedQueryTag } 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 b60df98d54a..86217ddcc97 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 @@ -160,8 +160,7 @@ class AbstractPlayServerTest extends HttpServerTest { } if (endpoint.query) { if (otelSemantics) { - "$KnownTags.URL_QUERY_NAME" expectedQueryTag - "$DDTags.HTTP_QUERY" null + "$DDTags.HTTP_QUERY_OTEL_NAME" expectedQueryTag } else { "$DDTags.HTTP_QUERY" expectedQueryTag } 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 db23561dee3..ca2fa0ddf5d 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 @@ -167,7 +167,7 @@ class RatpackHttpServerTest extends HttpServerTest { "$DDTags.ERROR_TYPE" endpoint.status.toString() } if (endpoint.query) { - tag(otelSemantics ? KnownTags.URL_QUERY_NAME : DDTags.HTTP_QUERY, endpoint.rawQuery) + tag(otelSemantics ? DDTags.HTTP_QUERY_OTEL_NAME : DDTags.HTTP_QUERY, endpoint.rawQuery) } defaultTags() } diff --git a/dd-trace-api/src/main/java/datadog/trace/api/DDTags.java b/dd-trace-api/src/main/java/datadog/trace/api/DDTags.java index 756048551c9..f2c34f9a9e0 100644 --- a/dd-trace-api/src/main/java/datadog/trace/api/DDTags.java +++ b/dd-trace-api/src/main/java/datadog/trace/api/DDTags.java @@ -35,6 +35,7 @@ public class DDTags { public static final String PATHWAY_HASH = "pathway.hash"; public static final String HTTP_QUERY = "http.query.string"; + public static final String HTTP_QUERY_OTEL_NAME = "url.query"; public static final String HTTP_FRAGMENT = "http.fragment.string"; public static final String USER_NAME = "user.principal"; 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 72ee76e0752..5982d936d22 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 @@ -6,7 +6,6 @@ import datadog.trace.api.Config; import datadog.trace.api.DDSpanTypes; import datadog.trace.api.DDTags; -import datadog.trace.api.KnownTags; import datadog.trace.api.TagMap; import datadog.trace.api.internal.VisibleForTesting; import datadog.trace.bootstrap.instrumentation.api.AppendableSpanLinks; @@ -125,7 +124,7 @@ private String obfuscate(String query) { @Override public void processTags( TagMap unsafeTags, DDSpanContext spanContext, AppendableSpanLinks spanLinks) { - String queryTag = otelSemanticsEnabled ? KnownTags.URL_QUERY_NAME : DDTags.HTTP_QUERY; + String queryTag = otelSemanticsEnabled ? DDTags.HTTP_QUERY_OTEL_NAME : DDTags.HTTP_QUERY; Object query = unsafeTags.getObject(queryTag); if (query instanceof CharSequence) { if (failClosed) { 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 7508ef465ca..e2223939d18 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 @@ -145,7 +145,7 @@ void userCopyOfDefaultPatternAlsoRedactsDelimiter() { "OpenTelemetry | true " }) void invalidRegexDropsQueryString(boolean otelSemantics) { - String queryTag = otelSemantics ? KnownTags.URL_QUERY_NAME : DDTags.HTTP_QUERY; + String queryTag = otelSemantics ? DDTags.HTTP_QUERY_OTEL_NAME : DDTags.HTTP_QUERY; TagMap unsafeTags = process(new QueryObfuscator("(?<=a)b", otelSemantics), "pwd=secret", queryTag); @@ -182,7 +182,7 @@ public String toString() { } }; - String queryTag = otelSemantics ? KnownTags.URL_QUERY_NAME : DDTags.HTTP_QUERY; + String queryTag = otelSemantics ? DDTags.HTTP_QUERY_OTEL_NAME : DDTags.HTTP_QUERY; TagMap unsafeTags = assertDoesNotThrow( () -> process(new QueryObfuscator(null, otelSemantics), failing, queryTag)); 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 index 55e5d24580c..9f310139e44 100644 --- 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 @@ -45,7 +45,7 @@ public static void setClientUrl(final AgentSpan span, final URI url) { // QueryObfuscator scrubs this before rebuilding url.full and removes it from client spans. final String query = url.getRawQuery(); if (query != null) { - span.setTag(KnownTags.URL_QUERY_NAME, query); + span.setTag(DDTags.HTTP_QUERY_OTEL_NAME, query); } } } From 4e1404ef821111b775365cd09226f25ae0bc61a9 Mon Sep 17 00:00:00 2001 From: Zach Montoya Date: Fri, 16 Oct 2026 16:05:15 -0700 Subject: [PATCH 11/22] Polish: Update setters to use IDs rather than tag names --- .../decorator/BaseDecorator.java | 5 +-- .../decorator/HttpClientDecorator.java | 6 +-- .../decorator/HttpServerDecorator.java | 33 +++++++------- .../decorator/UrlConnectionDecorator.java | 4 +- .../decorator/http/HttpResourceDecorator.java | 4 +- .../shim/trace/OtelConventions.java | 10 ++--- .../axway/AxwayHTTPPluginDecorator.java | 25 ++++++----- .../trace/api/gateway/InferredProxySpan.java | 26 +++++------ .../api/OtelHttpSemantics.java | 16 +++---- .../api/gateway/InferredProxySpanTests.java | 3 +- .../api/OtelHttpSemanticsTest.java | 44 +++++++++---------- 11 files changed, 87 insertions(+), 89 deletions(-) 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..31484e8b21c 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.Outbound.SERVER_PORT_ID, port); } public void setPeerPort(AgentSpan span, int port) { if (port > UNSET_PORT) { - span.setTag(Tags.PEER_PORT, port); + span.setTag(KnownTags.Outbound.SERVER_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 1d7fca795b0..5a2102f0835 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 @@ -9,6 +9,7 @@ import datadog.trace.api.Config; import datadog.trace.api.DDTags; 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; @@ -24,7 +25,6 @@ import datadog.trace.bootstrap.instrumentation.api.AgentTracer; import datadog.trace.bootstrap.instrumentation.api.InternalSpanTypes; import datadog.trace.bootstrap.instrumentation.api.OtelHttpSemantics; -import datadog.trace.bootstrap.instrumentation.api.Tags; import datadog.trace.bootstrap.instrumentation.api.URIUtils; import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; import java.net.URI; @@ -126,7 +126,7 @@ protected void doOnRequest(final AgentSpan span, final REQUEST request) { OtelHttpSemantics.setMethodResourceName(span, method); } } else { - span.setTag(Tags.HTTP_METHOD, method); + span.setTag(KnownTags.HTTP_METHOD_ID, method); } if (CLIENT_TAG_HEADERS) { @@ -152,7 +152,7 @@ protected void doOnRequest(final AgentSpan span, final REQUEST request) { } } else { span.setTag( - Tags.HTTP_URL, + KnownTags.HTTP_URL_ID, URIUtils.lazyValidURL( url.getScheme(), url.getHost(), url.getPort(), url.getPath())); if (Config.get().isHttpClientTagQueryString()) { 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 840d91867c2..3a406ba258e 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 @@ -327,7 +327,7 @@ protected void doOnRequest( } String userAgent = extracted.getUserAgent(); if (userAgent != null) { - span.setTag(Tags.HTTP_USER_AGENT, userAgent); + span.setTag(KnownTags.HTTP_USERAGENT_ID, userAgent); } } @@ -337,7 +337,7 @@ protected void doOnRequest( OtelHttpSemantics.setRequestMethod(span, method); OtelHttpSemantics.setMethodResourceName(span, method); } else { - span.setTag(Tags.HTTP_METHOD, method); + span.setTag(KnownTags.HTTP_METHOD_ID, method); } // Copy of HttpClientDecorator url handling @@ -357,7 +357,7 @@ protected void doOnRequest( } if (valid) { if (scheme != null) { - span.setTag(KnownTags.URL_SCHEME_NAME, scheme); + span.setTag(KnownTags.URL_SCHEME_ID, scheme); } // Ensure that path components have a leading '/' if (path == null || path.isEmpty()) { @@ -365,9 +365,9 @@ protected void doOnRequest( } else if (path.charAt(0) != '/') { path = "/" + path; } - span.setTag(KnownTags.URL_PATH_NAME, path); + span.setTag(KnownTags.URL_PATH_ID, path); } else if (supportsRaw) { - span.setTag(KnownTags.URL_PATH_NAME, url.raw()); + span.setTag(KnownTags.URL_PATH_ID, url.raw()); } OtelHttpSemantics.setServerAddressAndPort( span, @@ -379,14 +379,15 @@ protected void doOnRequest( } else { if (valid) { span.setTag( - Tags.HTTP_URL, URIUtils.lazyValidURL(url.scheme(), url.host(), url.port(), path)); + KnownTags.HTTP_URL_ID, + URIUtils.lazyValidURL(url.scheme(), url.host(), url.port(), path)); } else if (supportsRaw) { - span.setTag(Tags.HTTP_URL, URIUtils.lazyInvalidUrl(url.raw())); + span.setTag(KnownTags.HTTP_URL_ID, URIUtils.lazyInvalidUrl(url.raw())); } if (extracted != null && extracted.getXForwardedHost() != null) { - span.setTag(Tags.HTTP_HOSTNAME, extracted.getXForwardedHost()); + span.setTag(KnownTags.HTTP_HOSTNAME_ID, extracted.getXForwardedHost()); } else if (url.host() != null) { - span.setTag(Tags.HTTP_HOSTNAME, url.host()); + span.setTag(KnownTags.HTTP_HOSTNAME_ID, url.host()); } } @@ -398,7 +399,7 @@ protected void doOnRequest( span.setTag(DDTags.HTTP_QUERY_OTEL_NAME, query); } } else { - span.setTag(DDTags.HTTP_QUERY, query); + span.setTag(KnownTags.HTTP_QUERY_STRING_ID, query); span.setTag(DDTags.HTTP_FRAGMENT, url.fragment()); } } @@ -443,7 +444,7 @@ 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); } } } else if (shouldTagIps && span.getLocalRootSpan() != span) { @@ -455,20 +456,20 @@ 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); } } 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 && span.getTag(KnownTags.HTTP_CLIENT_IP_OTEL_NAME) == null) { - span.setTag(KnownTags.HTTP_CLIENT_IP_OTEL_NAME, peerIp); + span.setTag(KnownTags.HTTP_CLIENT_IP_ID, peerIp); } } } 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 00cf9056ec0..09979ae34fb 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,13 +1,13 @@ 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.OtelHttpSemantics; -import datadog.trace.bootstrap.instrumentation.api.Tags; import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; import java.net.URI; import java.net.URISyntaxException; @@ -57,7 +57,7 @@ public void onURI(@Nonnull AgentSpan span, @Nonnull URI uri) { if (OTEL_SEMANTICS) { OtelHttpSemantics.setClientUrl(span, uri); } else { - span.setTag(Tags.HTTP_URL, uri.toString()); + 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 567646c7fb2..d4ebd4cb19c 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,11 +1,11 @@ package datadog.trace.bootstrap.instrumentation.decorator.http; import datadog.trace.api.Config; +import datadog.trace.api.KnownTags; 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; @@ -53,7 +53,7 @@ public final void withRoute( if (encoded) { routeTag = URIUtils.decode(route.toString()); } - span.setTag(Tags.HTTP_ROUTE, routeTag); + span.setTag(KnownTags.HTTP_ROUTE_ID, routeTag); if (OTEL_SEMANTICS) { final CharSequence resourceName = HttpResourceNames.join(OtelHttpSemantics.spanNameMethod(method), routeTag); 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/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 756d4fec11e..1343aa5854a 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 @@ -3,6 +3,7 @@ import static java.lang.invoke.MethodType.methodType; import datadog.trace.api.Config; +import datadog.trace.api.KnownTagCodec; import datadog.trace.api.KnownTags; import datadog.trace.bootstrap.instrumentation.api.AgentPropagation; import datadog.trace.bootstrap.instrumentation.api.AgentSpan; @@ -205,9 +206,9 @@ public void onTransaction(AgentSpan span, Object stateInstance) { if (OTEL_SEMANTICS) { setOtelTransactionTags(span, stateInstance); } else { - setStringTagFromStateField(span, Tags.PEER_HOSTNAME, stateInstance, hostField_mh); - setStringTagFromStateField(span, Tags.PEER_PORT, stateInstance, portField_mh); - setStringTagFromStateField(span, Tags.HTTP_METHOD, stateInstance, methodField_mh); + setStringTagFromStateField(span, KnownTags.PEER_HOSTNAME_ID, stateInstance, hostField_mh); + setStringTagFromStateField(span, KnownTags.PEER_PORT_ID, stateInstance, portField_mh); + setStringTagFromStateField(span, KnownTags.HTTP_METHOD_ID, stateInstance, methodField_mh); setURLTagFromUriStateField(span, stateInstance); } } @@ -225,10 +226,10 @@ private static void setOtelTransactionTags(AgentSpan span, Object stateInstance) if (uri != null) { scheme = uri.getScheme(); if (uri.getPath() != null) { - span.setTag(KnownTags.URL_PATH_NAME, uri.getPath()); + span.setTag(KnownTags.URL_PATH_ID, uri.getPath()); } if (uri.getRawQuery() != null && !uri.getRawQuery().isEmpty()) { - span.setTag(KnownTags.URL_QUERY_NAME, uri.getRawQuery()); + span.setTag(KnownTags.URL_QUERY_ID, uri.getRawQuery()); } if (host == null || host.isEmpty()) { host = uri.getHost(); @@ -238,7 +239,7 @@ private static void setOtelTransactionTags(AgentSpan span, Object stateInstance) } } if (scheme != null) { - span.setTag(KnownTags.URL_SCHEME_NAME, scheme); + span.setTag(KnownTags.URL_SCHEME_ID, scheme); } OtelHttpSemantics.setServerAddressAndPort(span, null, null, scheme, host, port); } @@ -272,15 +273,19 @@ private static InetSocketAddress getRemoteAddr(Object obj) { } private static void setStringTagFromStateField( - AgentSpan span, String tag, Object stateInstance, MethodHandle mh) { + AgentSpan span, long tagId, Object stateInstance, MethodHandle mh) { String v = ""; try { v = (String) mh.invoke(stateInstance); } catch (Throwable e) { log.debug( - "Can't invoke '{}' on instance '{}'; ; Tag '{}' not set.", mh, stateInstance, tag, e); + "Can't invoke '{}' on instance '{}'; ; Tag '{}' not set.", + mh, + stateInstance, + KnownTagCodec.nameOf(tagId), + e); } - span.setTag(tag, v); + span.setTag(tagId, v); } private static String getStringFromStateField(Object stateInstance, MethodHandle mh) { @@ -295,7 +300,7 @@ private static String getStringFromStateField(Object stateInstance, MethodHandle private static void setURLTagFromUriStateField(AgentSpan span, Object stateInstance) { try { - span.setTag(Tags.HTTP_URL, uriField_mh.invoke(stateInstance).toString()); + span.setTag(KnownTags.HTTP_URL_ID, uriField_mh.invoke(stateInstance).toString()); } catch (Throwable e) { log.debug( "Can't invoke '{}' on instance '{}'; Tag '{}' not set.", 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 f9939385b0d..8ecae2405fe 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,17 +1,11 @@ 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; @@ -106,32 +100,32 @@ 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"); + span.setTag(KnownTags.SPAN_TYPE_ID, "web"); if (OTEL_SEMANTICS) { OtelHttpSemantics.setRequestMethod(span, httpMethod); - span.setTag(KnownTags.URL_PATH_NAME, path); + span.setTag(KnownTags.URL_PATH_ID, path); if (domainName != null && !domainName.isEmpty()) { - span.setTag(KnownTags.URL_SCHEME_NAME, "https"); - span.setTag(KnownTags.HTTP_HOSTNAME_NAME, domainName); + span.setTag(KnownTags.URL_SCHEME_ID, "https"); + span.setTag(KnownTags.Inbound.SERVER_ADDRESS_ID, domainName); } } else { - span.setTag(HTTP_METHOD, httpMethod); + span.setTag(KnownTags.HTTP_METHOD_ID, httpMethod); span.setTag( - HTTP_URL, + KnownTags.HTTP_URL_ID, domainName != null && !domainName.isEmpty() ? "https://" + domainName + path : 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(HTTP_ROUTE, OTEL_SEMANTICS ? route : route != null ? route : path); + 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)); @@ -314,7 +308,7 @@ private void copyTagsFromServiceEntry(AgentSpan serviceEntrySpan) { 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 index 9f310139e44..dfd953018a3 100644 --- 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 @@ -26,10 +26,10 @@ private OtelHttpSemantics() {} public static void setRequestMethod(final AgentSpan span, final String method) { if (method != null && !KNOWN_METHODS.contains(method)) { - span.setTag(KnownTags.HTTP_METHOD_OTEL_NAME, OTHER_METHOD); - span.setTag(KnownTags.HTTP_REQUEST_METHOD_ORIGINAL_NAME, method); + span.setTag(KnownTags.HTTP_METHOD_ID, OTHER_METHOD); + span.setTag(KnownTags.HTTP_REQUEST_METHOD_ORIGINAL_ID, method); } else { - span.setTag(KnownTags.HTTP_METHOD_OTEL_NAME, method); + span.setTag(KnownTags.HTTP_METHOD_ID, method); } } @@ -40,12 +40,12 @@ public static void setMethodResourceName(final AgentSpan span, final CharSequenc public static void setClientUrl(final AgentSpan span, final URI url) { // Use the object tag setter rather than the CharSequence overload, which checks length(), which // would render the URL immediately. - span.setTag(KnownTags.HTTP_URL_OTEL_NAME, (Object) new ClientUrl(url)); + span.setTag(KnownTags.HTTP_URL_ID, (Object) new ClientUrl(url)); if (Config.get().isHttpClientTagQueryString()) { // QueryObfuscator scrubs this before rebuilding url.full and removes it from client spans. final String query = url.getRawQuery(); if (query != null) { - span.setTag(DDTags.HTTP_QUERY_OTEL_NAME, query); + span.setTag(KnownTags.URL_QUERY_ID, query); } } } @@ -91,7 +91,7 @@ public static String withoutQuery(final String url) { public static void setErrorType(final AgentSpan span, final int status) { if (span.getTag(DDTags.ERROR_TYPE) == null) { - span.setTag(DDTags.ERROR_TYPE, RadixTreeCache.HTTP_STATUSES.get(status).toString()); + span.setTag(KnownTags.ERROR_TYPE_ID, RadixTreeCache.HTTP_STATUSES.get(status).toString()); } } @@ -153,7 +153,7 @@ public static void setServerAddressAndPort( } if (serverAddress != null && !serverAddress.isEmpty()) { - span.setTag(KnownTags.HTTP_HOSTNAME_NAME, serverAddress); + span.setTag(KnownTags.Inbound.SERVER_ADDRESS_ID, serverAddress); } int serverPort = parsePort(forwardedValue(forwardedPortHeader)); @@ -164,7 +164,7 @@ public static void setServerAddressAndPort( serverPort = forwardedHost == null && requestPort > 0 ? requestPort : serverPort(scheme, -1); } if (serverPort > 0) { - span.setTag(KnownTags.SERVER_PORT_NAME, serverPort); + span.setTag(KnownTags.Inbound.SERVER_PORT_ID, serverPort); } } 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/OtelHttpSemanticsTest.java b/internal-api/src/test/java/datadog/trace/bootstrap/instrumentation/api/OtelHttpSemanticsTest.java index 29cebe35228..a48d9c8daca 100644 --- 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 @@ -30,8 +30,8 @@ void setRequestMethodKeepsKnownMethods(String method) { OtelHttpSemantics.setRequestMethod(span, method); - verify(span).setTag(KnownTags.HTTP_METHOD_OTEL_NAME, method); - verify(span, never()).setTag(eq(KnownTags.HTTP_REQUEST_METHOD_ORIGINAL_NAME), anyString()); + verify(span).setTag(KnownTags.HTTP_METHOD_ID, method); + verify(span, never()).setTag(eq(KnownTags.HTTP_REQUEST_METHOD_ORIGINAL_ID), anyString()); } @ParameterizedTest @@ -41,8 +41,8 @@ void setRequestMethodNormalizesUnknownMethodsToOther(String method) { OtelHttpSemantics.setRequestMethod(span, method); - verify(span).setTag(KnownTags.HTTP_METHOD_OTEL_NAME, OtelHttpSemantics.OTHER_METHOD); - verify(span).setTag(KnownTags.HTTP_REQUEST_METHOD_ORIGINAL_NAME, method); + verify(span).setTag(KnownTags.HTTP_METHOD_ID, OtelHttpSemantics.OTHER_METHOD); + verify(span).setTag(KnownTags.HTTP_REQUEST_METHOD_ORIGINAL_ID, method); } @TableTest({ @@ -91,14 +91,14 @@ void setClientUrlLazilyStripsQueryAndRedactsCredentials() { OtelHttpSemantics.setClientUrl(span, URI.create("https://u:p@host/a?token=secret#fragment")); - verify(span).setTag(eq(KnownTags.HTTP_URL_OTEL_NAME), (Object) urlCaptor.capture()); + 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()); - verify(span).setTag(KnownTags.URL_QUERY_NAME, "token=secret"); + verify(span).setTag(KnownTags.URL_QUERY_ID, "token=secret"); } @Test @@ -107,7 +107,7 @@ void setClientUrlOmitsQueryTagWhenAbsent() { OtelHttpSemantics.setClientUrl(span, URI.create("http://host/a")); - verify(span, never()).setTag(eq(KnownTags.URL_QUERY_NAME), anyString()); + verify(span, never()).setTag(eq(KnownTags.URL_QUERY_ID), anyString()); } @TableTest({ @@ -164,7 +164,7 @@ void setErrorTypeSetsStatusWhenAbsent() { OtelHttpSemantics.setErrorType(span, 500); - verify(span).setTag(DDTags.ERROR_TYPE, "500"); + verify(span).setTag(KnownTags.ERROR_TYPE_ID, "500"); } @Test @@ -174,7 +174,7 @@ void setErrorTypeDoesNotOverrideExistingErrorType() { OtelHttpSemantics.setErrorType(span, 500); - verify(span, never()).setTag(eq(DDTags.ERROR_TYPE), any(String.class)); + verify(span, never()).setTag(eq(KnownTags.ERROR_TYPE_ID), any(String.class)); } @TableTest({ @@ -244,8 +244,8 @@ void setServerAddressAndPortUsesRequestHostAndPort() { OtelHttpSemantics.setServerAddressAndPort(span, null, null, "http", "host", 8080); - verify(span).setTag(KnownTags.HTTP_HOSTNAME_NAME, "host"); - verify(span).setTag(KnownTags.SERVER_PORT_NAME, 8080); + verify(span).setTag(KnownTags.HTTP_HOSTNAME_ID, "host"); + verify(span).setTag(KnownTags.Inbound.SERVER_PORT_ID, 8080); } @Test @@ -254,8 +254,8 @@ void setServerAddressAndPortFallsBackToSchemeDefaultPort() { OtelHttpSemantics.setServerAddressAndPort(span, null, null, "https", "host", -1); - verify(span).setTag(KnownTags.HTTP_HOSTNAME_NAME, "host"); - verify(span).setTag(KnownTags.SERVER_PORT_NAME, 443); + verify(span).setTag(KnownTags.HTTP_HOSTNAME_ID, "host"); + verify(span).setTag(KnownTags.Inbound.SERVER_PORT_ID, 443); } @Test @@ -264,8 +264,8 @@ void setServerAddressAndPortOmitsTagsWhenNothingResolvable() { OtelHttpSemantics.setServerAddressAndPort(span, null, null, "ftp", null, -1); - verify(span, never()).setTag(eq(KnownTags.HTTP_HOSTNAME_NAME), anyString()); - verify(span, never()).setTag(eq(KnownTags.SERVER_PORT_NAME), anyInt()); + verify(span, never()).setTag(eq(KnownTags.HTTP_HOSTNAME_ID), anyString()); + verify(span, never()).setTag(eq(KnownTags.Inbound.SERVER_PORT_ID), anyInt()); } @Test @@ -274,8 +274,8 @@ void setServerAddressAndPortOmitsAddressWhenRequestHostIsEmpty() { OtelHttpSemantics.setServerAddressAndPort(span, null, null, "http", "", -1); - verify(span, never()).setTag(eq(KnownTags.HTTP_HOSTNAME_NAME), anyString()); - verify(span).setTag(KnownTags.SERVER_PORT_NAME, 80); + verify(span, never()).setTag(eq(KnownTags.HTTP_HOSTNAME_ID), anyString()); + verify(span).setTag(KnownTags.Inbound.SERVER_PORT_ID, 80); } @Test @@ -284,8 +284,8 @@ void setServerAddressAndPortPrefersForwardedPortHeader() { OtelHttpSemantics.setServerAddressAndPort(span, "proxy:8443", "9443", "http", "host", 8080); - verify(span).setTag(KnownTags.HTTP_HOSTNAME_NAME, "proxy"); - verify(span).setTag(KnownTags.SERVER_PORT_NAME, 9443); + verify(span).setTag(KnownTags.HTTP_HOSTNAME_ID, "proxy"); + verify(span).setTag(KnownTags.Inbound.SERVER_PORT_ID, 9443); } @TableTest({ @@ -302,7 +302,7 @@ void setServerAddressAndPortFallsBackFromInvalidForwardedPort( OtelHttpSemantics.setServerAddressAndPort( span, forwardedHost, forwardedPort, "http", "host", 8080); - verify(span).setTag(KnownTags.SERVER_PORT_NAME, expectedPort); + verify(span).setTag(KnownTags.Inbound.SERVER_PORT_ID, expectedPort); } @TableTest({ @@ -322,8 +322,8 @@ void setServerAddressAndPortParsesForwardedHost( OtelHttpSemantics.setServerAddressAndPort(span, forwardedHost, null, "http", "host", 8080); - verify(span).setTag(KnownTags.HTTP_HOSTNAME_NAME, expectedAddress); + 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.SERVER_PORT_NAME, expectedPort); + verify(span).setTag(KnownTags.Inbound.SERVER_PORT_ID, expectedPort); } } From 5f48fc937775934fcb5d49404c8b1434d0d24d56 Mon Sep 17 00:00:00 2001 From: Zach Montoya Date: Mon, 19 Oct 2026 15:08:07 -0700 Subject: [PATCH 12/22] Fix: Add regression tests for identified errors when enabling the opt-in changes, including: HttpDecoratorsOtelSemanticsForkedTest.java - Elasticsearch and OpenSearch spans should be unchanged - There must not be duplicate attributes for client destination ports (server.port) - InferredProxySpan should record server.port=443 due to the default https scheme - When OTel semantics are enabled, Forwarded-header extraction should be run and determine client span attributes, even if DD_TRACE_CLIENT_IP_ENABLED=false or AppSec+AI Guard are disabled - When OTel semantics are enabled, url.full (OTel equivalent of http.url) decodes the query before storing and sending to QueryObfuscator. OtelHttpSemanticsTest.java - When OTel semantics are enabled, url.full (OTel equivalent of http.url) decodes the query before storing and sending to QueryObfuscator. HttpClientPathMappingOtelSemanticsForkedTest.java - TRACE_HTTP_CLIENT_PATH_RESOURCE_NAME_MAPPING takes precedence and replaces the http.route placeholder in the OTel HTTP server span name "{http.request.method} {http.route}" LambdaHttpOtelSemanticsForkedTest.java & LambdaAppSecHandlerTest.java - Lambda query tags should continue to be obfuscated GatewayBridgeOtelSemanticsForkedTest.java - API Security must continue to fall back to the http.url span attribute when the server span does not have an http.route span attribute --- ...entPathMappingOtelSemanticsForkedTest.java | 91 ++++++++ ...HttpDecoratorsOtelSemanticsForkedTest.java | 207 +++++++++++++++++- .../GatewayBridgeOtelSemanticsForkedTest.java | 147 +++++++++++++ .../trace/lambda/LambdaAppSecHandler.java | 6 +- .../trace/lambda/LambdaAppSecHandlerTest.java | 5 +- .../LambdaHttpOtelSemanticsForkedTest.java | 99 +++++++++ .../api/OtelHttpSemanticsTest.java | 19 ++ 7 files changed, 567 insertions(+), 7 deletions(-) create mode 100644 dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/decorator/HttpClientPathMappingOtelSemanticsForkedTest.java create mode 100644 dd-java-agent/appsec/src/test/java/com/datadog/appsec/gateway/GatewayBridgeOtelSemanticsForkedTest.java create mode 100644 dd-trace-core/src/test/java/datadog/trace/lambda/LambdaHttpOtelSemanticsForkedTest.java 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/HttpDecoratorsOtelSemanticsForkedTest.java b/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/decorator/HttpDecoratorsOtelSemanticsForkedTest.java index 15c3995bb36..3c511e0bdb8 100644 --- 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 @@ -5,18 +5,29 @@ import static datadog.trace.api.config.TraceInstrumentationConfig.HTTP_CLIENT_TAG_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.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.assertTrue; +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; @@ -24,11 +35,21 @@ 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") @@ -275,18 +296,196 @@ void serverDecoratorUsesForwardedPortWithIpv6Host() throws Exception { assertEquals(9443, span.getTag(KnownTags.SERVER_PORT_NAME)); } - private static final class ClientRequest { + @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; - private ClientRequest(String method, URI uri) { + ClientRequest(String method, URI uri) { this.method = method; this.uri = uri; } } - private static final class TestClientDecorator - extends HttpClientDecorator { + static final class TestClientDecorator extends HttpClientDecorator { @Override protected String[] instrumentationNames() { return new String[] {"test-http-client"}; 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-trace-core/src/main/java/datadog/trace/lambda/LambdaAppSecHandler.java b/dd-trace-core/src/main/java/datadog/trace/lambda/LambdaAppSecHandler.java index 8ed1eb4cb6a..eafa4636d96 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 @@ -55,6 +55,8 @@ public class LambdaAppSecHandler { private static final Logger log = LoggerFactory.getLogger(LambdaAppSecHandler.class); private static final RatelimitedLogger rlLog = new RatelimitedLogger(log, 5, TimeUnit.MINUTES); + private static final String HTTP_QUERY_TAG = + Config.get().isTraceOtelSemanticsEnabled() ? DDTags.HTTP_QUERY_OTEL_NAME : DDTags.HTTP_QUERY; /** * Marks an invocation AppSec did not process because the trigger is not HTTP, or if the event is @@ -334,14 +336,14 @@ 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())); } String query = url.rawQuery(); if (query != null && !query.isEmpty() && Config.get().isHttpServerTagQueryString()) { - ctx.putTag(DDTags.HTTP_QUERY, query); + ctx.putTag(HTTP_QUERY_TAG, query); } String userAgent = findHeader(req.headers, "user-agent"); 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..486ad23c91b 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 @@ -2468,6 +2468,7 @@ void appliesHttpTagsForRestApiEvent() { assertEquals("GET", tags.get(Tags.HTTP_METHOD)); assertEquals("https://api.example.com/users/42", tags.get(Tags.HTTP_URL)); assertEquals("q=hello", tags.get(DDTags.HTTP_QUERY)); + assertNull(tags.get(DDTags.HTTP_QUERY_OTEL_NAME)); assertEquals("curl/8.1", tags.get(Tags.HTTP_USER_AGENT)); assertEquals("/users/{id}", tags.get(Tags.HTTP_ROUTE)); assertEquals("api.example.com", tags.get(Tags.HTTP_HOSTNAME)); @@ -2562,7 +2563,9 @@ 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)); + assertNull(tags.get(DDTags.HTTP_QUERY_OTEL_NAME)); } @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..309676f8f79 --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/lambda/LambdaHttpOtelSemanticsForkedTest.java @@ -0,0 +1,99 @@ +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_OTEL_NAME)); + 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_OTEL_NAME)); + assertNull(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_OTEL_NAME)); + assertNull(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/test/java/datadog/trace/bootstrap/instrumentation/api/OtelHttpSemanticsTest.java b/internal-api/src/test/java/datadog/trace/bootstrap/instrumentation/api/OtelHttpSemanticsTest.java index a48d9c8daca..70675c0a2ab 100644 --- 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 @@ -101,6 +101,25 @@ void setClientUrlLazilyStripsQueryAndRedactsCredentials() { verify(span).setTag(KnownTags.URL_QUERY_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 setClientUrlDecodesQuerySoTheObfuscatorCanMatchEncodedNames( + String rawUrl, String expectedQuery) { + AgentSpan span = mock(AgentSpan.class); + + OtelHttpSemantics.setClientUrl(span, URI.create(rawUrl)); + + // The obfuscator matches literal parameter names, so the query it receives must be decoded. + verify(span).setTag(KnownTags.URL_QUERY_ID, expectedQuery); + } + @Test void setClientUrlOmitsQueryTagWhenAbsent() { AgentSpan span = mock(AgentSpan.class); From 70be501b683f884710807ef406924f97a474adcf Mon Sep 17 00:00:00 2001 From: Zach Montoya Date: Mon, 19 Oct 2026 15:42:10 -0700 Subject: [PATCH 13/22] Fix: Implement fixes for the noted regressions --- .../decorator/HttpClientDecorator.java | 7 ++----- .../decorator/UriBasedClientDecorator.java | 17 ++++------------- .../decorator/http/HttpResourceDecorator.java | 17 ++++++++++++++++- .../datadog/appsec/gateway/GatewayBridge.java | 9 ++++++--- .../core/propagation/ContextInterpreter.java | 6 ++++-- .../trace/api/gateway/InferredProxySpan.java | 1 + .../instrumentation/api/OtelHttpSemantics.java | 5 ++++- 7 files changed, 37 insertions(+), 25 deletions(-) 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 5a2102f0835..9375a84939f 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 @@ -146,10 +146,6 @@ protected void doOnRequest(final AgentSpan span, final REQUEST request) { onURI(span, url); if (OTEL_SEMANTICS) { OtelHttpSemantics.setClientUrl(span, url); - // url.full may trigger URLAsResourceNameRule; restore the OTel span name afterwards. - if (shouldSetResourceName()) { - OtelHttpSemantics.setMethodResourceName(span, method); - } } else { span.setTag( KnownTags.HTTP_URL_ID, @@ -160,7 +156,8 @@ protected void doOnRequest(final AgentSpan span, final REQUEST request) { span.setTag(DDTags.HTTP_FRAGMENT, url.getFragment()); } } - if (!OTEL_SEMANTICS && shouldSetResourceName()) { + // Explicit path mappings take precedence over the default name in either semantic mode. + if (shouldSetResourceName()) { HTTP_RESOURCE_DECORATOR.withClientPath(span, method, url.getPath()); } // SSRF exploit prevention check 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 143964842b3..9c0fa31cae1 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 @@ -6,7 +6,6 @@ import datadog.context.Context; import datadog.context.propagation.CarrierSetter; import datadog.trace.api.Config; -import datadog.trace.api.KnownTags; import datadog.trace.api.datastreams.DataStreamsContext; import datadog.trace.bootstrap.instrumentation.api.AgentSpan; import datadog.trace.bootstrap.instrumentation.api.OtelHttpSemantics; @@ -20,18 +19,10 @@ public abstract class UriBasedClientDecorator extends ClientDecorator { public void onURI(@Nonnull final AgentSpan span, @Nonnull final URI uri) { String host = uri.getHost(); if (null != host && !host.isEmpty()) { - if (OTEL_SEMANTICS) { - span.setTag(KnownTags.PEER_HOSTNAME_NAME, host); - int serverPort = OtelHttpSemantics.serverPort(uri); - if (serverPort > 0) { - span.setTag(KnownTags.SERVER_PORT_NAME, serverPort); - } - } else { - span.setTag(Tags.PEER_HOSTNAME, host); - int port = uri.getPort(); - if (port > 0) { - setPeerPort(span, port); - } + span.setTag(Tags.PEER_HOSTNAME, host); + int port = OTEL_SEMANTICS ? OtelHttpSemantics.serverPort(uri) : uri.getPort(); + if (port > 0) { + setPeerPort(span, port); } if (Config.get().isHttpClientSplitByDomain() && host.charAt(0) >= 'A') { span.setServiceName(host, component()); 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 d4ebd4cb19c..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,7 +1,9 @@ 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; @@ -17,11 +19,24 @@ public class HttpResourceDecorator { 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) { - if (OTEL_SEMANTICS) { + 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); 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..f0566e3b15f 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 @@ -28,6 +28,7 @@ import com.datadog.appsec.util.BodyParser; import datadog.appsec.api.blocking.BlockingContentType; import datadog.trace.api.Config; +import datadog.trace.api.KnownTags; import datadog.trace.api.ProductTraceSource; import datadog.trace.api.appsec.HttpClientPayload; import datadog.trace.api.appsec.HttpClientRequest; @@ -1114,13 +1115,15 @@ 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 = tags.get(Tags.HTTP_URL); + if (url == null) { + url = tags.get(KnownTags.URL_PATH_NAME); + } if (url != null) { ctx.setHttpUrl(url.toString()); } 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/internal-api/src/main/java/datadog/trace/api/gateway/InferredProxySpan.java b/internal-api/src/main/java/datadog/trace/api/gateway/InferredProxySpan.java index 8ecae2405fe..5a3af61331c 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 @@ -114,6 +114,7 @@ public AgentSpanContext start(AgentSpanContext extracted) { if (domainName != null && !domainName.isEmpty()) { span.setTag(KnownTags.URL_SCHEME_ID, "https"); span.setTag(KnownTags.Inbound.SERVER_ADDRESS_ID, domainName); + span.setTag(KnownTags.Inbound.SERVER_PORT_ID, 443); } } else { span.setTag(KnownTags.HTTP_METHOD_ID, httpMethod); 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 index dfd953018a3..ffa60ec927c 100644 --- 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 @@ -43,7 +43,10 @@ public static void setClientUrl(final AgentSpan span, final URI url) { span.setTag(KnownTags.HTTP_URL_ID, (Object) new ClientUrl(url)); if (Config.get().isHttpClientTagQueryString()) { // QueryObfuscator scrubs this before rebuilding url.full and removes it from client spans. - final String query = url.getRawQuery(); + // Decode rather than using getRawQuery(): the obfuscator matches literal parameter names, so + // a percent-encoded name such as t%6fken would hide the secret from it. This matches the + // query that non-OpenTelemetry client spans already carry in http.url. + final String query = url.getQuery(); if (query != null) { span.setTag(KnownTags.URL_QUERY_ID, query); } From 866cff2ace3a09278e236183f28fc701af8144f9 Mon Sep 17 00:00:00 2001 From: Zach Montoya Date: Mon, 19 Oct 2026 16:26:07 -0700 Subject: [PATCH 14/22] Fix: Address two issues identified by automated code review. - QueryObfuscator: Obfuscate both the DD and OTel query string tags as a preventative measure to make sure we do not store an unobfuscated query string on a span. When OTel semantics mode is enabled, the OTel value will be take precedence as the final recorded value (the DD value will be discarded if present). - Ensure that we record the correct origin port when handling forwarded schemes --- .../decorator/HttpServerDecorator.java | 8 +- ...HttpDecoratorsOtelSemanticsForkedTest.java | 37 +++++++++ .../core/tagprocessor/QueryObfuscator.java | 53 ++++++++----- .../tagprocessor/QueryObfuscatorTest.java | 79 +++++++++++++++++-- .../api/OtelHttpSemantics.java | 19 +++++ .../api/OtelHttpSemanticsTest.java | 17 ++++ 6 files changed, 186 insertions(+), 27 deletions(-) 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 3a406ba258e..02b5541416e 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 @@ -355,6 +355,12 @@ protected void doOnRequest( if (scheme == null) { scheme = url.scheme(); } + int requestPort = url.port(); + if (scheme != null && !scheme.equalsIgnoreCase(url.scheme())) { + // A forwarded scheme can describe a different origin from the backend connection. + // Preserve an explicit public Host port; otherwise use the origin scheme default. + requestPort = OtelHttpSemantics.portFromHost(getRequestHeader(request, "Host")); + } if (valid) { if (scheme != null) { span.setTag(KnownTags.URL_SCHEME_ID, scheme); @@ -375,7 +381,7 @@ protected void doOnRequest( extracted == null ? null : extracted.getXForwardedPort(), scheme, url.host(), - url.port()); + requestPort); } else { if (valid) { span.setTag( 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 index 3c511e0bdb8..dd6fc239c7b 100644 --- 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 @@ -296,6 +296,32 @@ void serverDecoratorUsesForwardedPortWithIpv6Host() throws Exception { 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) { @@ -525,10 +551,16 @@ protected String getResponseHeader(Integer response, String headerName) { private static final class ServerRequest { private final String method; private final URI 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 = method; this.uri = uri; + this.hostHeader = hostHeader; } } @@ -579,6 +611,11 @@ protected URIDataAdapter url(ServerRequest request) { return new URIDefaultDataAdapter(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; 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 5982d936d22..f1535c127ea 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 @@ -124,38 +124,53 @@ private String obfuscate(String query) { @Override public void processTags( TagMap unsafeTags, DDSpanContext spanContext, AppendableSpanLinks spanLinks) { - String queryTag = otelSemanticsEnabled ? DDTags.HTTP_QUERY_OTEL_NAME : DDTags.HTTP_QUERY; + String query = obfuscateTag(unsafeTags, DDTags.HTTP_QUERY); + if (otelSemanticsEnabled) { + String otelQuery = obfuscateTag(unsafeTags, DDTags.HTTP_QUERY_OTEL_NAME); + if (otelQuery != null) { + query = otelQuery; + } + } + if (query == null) { + return; + } + + Object url = unsafeTags.getObject(Tags.HTTP_URL); + if (url instanceof CharSequence) { + unsafeTags.put(Tags.HTTP_URL, appendQuery(url.toString(), query)); + } + + // 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_OTEL_NAME); + } + } + + private String obfuscateTag(TagMap unsafeTags, String queryTag) { Object query = unsafeTags.getObject(queryTag); if (query instanceof CharSequence) { if (failClosed) { unsafeTags.remove(queryTag); - return; + return null; } + final String obfuscated; try { - query = obfuscate(query.toString()); + obfuscated = obfuscate(query.toString()); } catch (Exception e) { // never report a query that could not be obfuscated, and never log it log.debug("Could not obfuscate query string, dropping it", e); unsafeTags.remove(queryTag); - return; + return null; } - unsafeTags.put(queryTag, query); - - Object url = unsafeTags.getObject(Tags.HTTP_URL); - if (url instanceof CharSequence) { - 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(queryTag); - } + unsafeTags.put(queryTag, obfuscated); + return obfuscated; } + return null; } private static String appendQuery(final String url, final String query) { 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 e2223939d18..367239f16bd 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 @@ -154,11 +154,12 @@ void invalidRegexDropsQueryString(boolean otelSemantics) { } @TableTest({ - "Scenario | OTel semantics", - "Datadog | false ", - "OpenTelemetry | true " + "Scenario | OTel semantics | Query tag ", + "Datadog | false | http.query.string", + "OpenTelemetry | true | url.query ", + "Legacy in OTel | true | http.query.string" }) - void obfuscationFailureDropsQueryString(boolean otelSemantics) { + void obfuscationFailureDropsQueryString(boolean otelSemantics, String queryTag) { CharSequence failing = new CharSequence() { @Override @@ -182,7 +183,6 @@ public String toString() { } }; - String queryTag = otelSemantics ? DDTags.HTTP_QUERY_OTEL_NAME : DDTags.HTTP_QUERY; TagMap unsafeTags = assertDoesNotThrow( () -> process(new QueryObfuscator(null, otelSemantics), failing, queryTag)); @@ -201,6 +201,69 @@ void emptyRegexKeepsObfuscationDisabled() { assertEquals("http://site.com/index?" + query, unsafeTags.getObject(Tags.HTTP_URL)); } + @TableTest({ + "Scenario | Legacy query | OTel query | Expected legacy | Expected OTel | Expected URL query ", + "Legacy only | token=legacy | | | | ", + "OTel only | | pwd=otel&view=all | | &view=all | &view=all", + "Both keys | token=legacy&a=1 | pwd=otel&b=2 | &a=1 | &b=2 | &b=2 " + }) + void otelSemanticsObfuscatesBothQueryTags( + String legacyQuery, + String otelQuery, + String expectedLegacy, + String expectedOtel, + String expectedUrlQuery) { + TagMap tags = TagMap.create(); + tags.put(Tags.HTTP_URL, "http://site.com/index#fragment"); + if (legacyQuery != null) { + tags.put(DDTags.HTTP_QUERY, legacyQuery); + } + if (otelQuery != null) { + tags.put(DDTags.HTTP_QUERY_OTEL_NAME, otelQuery); + } + + new QueryObfuscator(null, true).processTags(tags, null, link -> {}); + + assertEquals(expectedLegacy, tags.get(DDTags.HTTP_QUERY)); + assertEquals(expectedOtel, tags.get(DDTags.HTTP_QUERY_OTEL_NAME)); + assertEquals( + "http://site.com/index?" + expectedUrlQuery + "#fragment", tags.get(Tags.HTTP_URL)); + } + + @TableTest({ + "Scenario | Failing tag | Other tag ", + "Legacy fails | http.query.string | url.query ", + "OTel fails | url.query | http.query.string" + }) + void failureInOneQueryTagDoesNotPreventObfuscatingTheOther(String failingTag, String otherTag) { + CharSequence query = mock(CharSequence.class); + when(query.toString()).thenThrow(new IllegalStateException("boom")); + TagMap tags = TagMap.create(); + tags.put(Tags.HTTP_URL, "http://site.com/index#fragment"); + tags.put(failingTag, query); + tags.put(otherTag, "token=secret"); + + assertDoesNotThrow(() -> new QueryObfuscator(null, true).processTags(tags, null, link -> {})); + + assertFalse(tags.containsKey(failingTag)); + assertEquals("", tags.get(otherTag)); + assertEquals("http://site.com/index?#fragment", tags.get(Tags.HTTP_URL)); + } + + @Test + void invalidRegexDropsBothQueryTagsInOtelMode() { + TagMap tags = TagMap.create(); + tags.put(Tags.HTTP_URL, "http://site.com/index#fragment"); + tags.put(DDTags.HTTP_QUERY, "token=legacy"); + tags.put(DDTags.HTTP_QUERY_OTEL_NAME, "pwd=otel"); + + new QueryObfuscator("(?<=a)b", true).processTags(tags, null, link -> {}); + + assertFalse(tags.containsKey(DDTags.HTTP_QUERY)); + assertFalse(tags.containsKey(DDTags.HTTP_QUERY_OTEL_NAME)); + assertEquals("http://site.com/index#fragment", tags.get(Tags.HTTP_URL)); + } + @TableTest({ "spanType | retainQuery", "http | false ", @@ -213,11 +276,13 @@ void otelClientQueryIsOnlyUsedToBuildUrlFull(String spanType, boolean retainQuer TagMap tags = TagMap.create(); tags.put(Tags.HTTP_URL, "http://site.com/index#fragment"); - tags.put(KnownTags.URL_QUERY_NAME, "token=secret"); + tags.put(KnownTags.URL_QUERY_NAME, "token=secret&otel_value=retained"); + tags.put(DDTags.HTTP_QUERY, "pwd=legacy&dd_value=not_kept"); obfuscator.processTags(tags, spanContext, link -> {}); - assertEquals("http://site.com/index?#fragment", tags.get(Tags.HTTP_URL)); + assertEquals("http://site.com/index?&otel_value=retained#fragment", tags.get(Tags.HTTP_URL)); + assertEquals("&otel_value=retained", tags.get(DDTags.HTTP_QUERY)); if (retainQuery) { assertEquals("", tags.get(KnownTags.URL_QUERY_NAME)); return; 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 index ffa60ec927c..4ea044130fa 100644 --- 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 @@ -124,6 +124,25 @@ public static String forwardedValue(final String value) { 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.substring(colon + 1)); + } + public static void setServerAddressAndPort( final AgentSpan span, final String forwardedHostHeader, 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 index 70675c0a2ab..f47e6f979e3 100644 --- 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 @@ -98,6 +98,7 @@ void setClientUrlLazilyStripsQueryAndRedactsCredentials() { 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.URL_QUERY_ID, "token=secret"); } @@ -267,6 +268,22 @@ void setServerAddressAndPortUsesRequestHostAndPort() { verify(span).setTag(KnownTags.Inbound.SERVER_PORT_ID, 8080); } + @TableTest({ + "Scenario | Host | Expected port", + "No host | | -1 ", + "Host only | public.example.com | -1 ", + "Explicit port | public.example.com:9443 | 9443 ", + "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)); + } + @Test void setServerAddressAndPortFallsBackToSchemeDefaultPort() { AgentSpan span = mock(AgentSpan.class); From 5eee031e5056f565ebe09820365693a6e8f9a1f7 Mon Sep 17 00:00:00 2001 From: Zach Montoya Date: Mon, 19 Oct 2026 16:27:39 -0700 Subject: [PATCH 15/22] fix linting issue --- .../src/bootTest/groovy/SpringWebfluxTest.groovy | 1 - 1 file changed, 1 deletion(-) 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 0759b96f64e..9b49386871c 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 @@ -1,5 +1,4 @@ import datadog.trace.agent.test.base.OkHttpWebsocketClient -import datadog.trace.bootstrap.instrumentation.api.OtelHttpSemantics import dd.trace.instrumentation.springwebflux.server.WsHandler import net.bytebuddy.utility.RandomString import org.springframework.beans.factory.annotation.Autowired From 1d4d8a05a88a6d193ab12ea1c311ab4a9b9a23a0 Mon Sep 17 00:00:00 2001 From: Zach Montoya Date: Mon, 19 Oct 2026 16:34:39 -0700 Subject: [PATCH 16/22] Apply spotless formatting --- .../datadog/trace/core/tagprocessor/QueryObfuscatorTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 367239f16bd..893442f8555 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 @@ -281,7 +281,8 @@ void otelClientQueryIsOnlyUsedToBuildUrlFull(String spanType, boolean retainQuer obfuscator.processTags(tags, spanContext, link -> {}); - assertEquals("http://site.com/index?&otel_value=retained#fragment", tags.get(Tags.HTTP_URL)); + assertEquals( + "http://site.com/index?&otel_value=retained#fragment", tags.get(Tags.HTTP_URL)); assertEquals("&otel_value=retained", tags.get(DDTags.HTTP_QUERY)); if (retainQuery) { assertEquals("", tags.get(KnownTags.URL_QUERY_NAME)); From fd65cad1f72f6a8fabd8e1e3db34ec154a5dacb9 Mon Sep 17 00:00:00 2001 From: Zach Montoya Date: Tue, 20 Oct 2026 16:26:58 -0700 Subject: [PATCH 17/22] Fix: Address issues identified by automated code review: - Correct obfuscated-query expectations - Fix the IAST missing-HSTS detection by allowing it to also check the "url.scheme" OTel HTTP server tag to confirm HTTPS, since the "http.url" tag is only emitted with DD semantic conventions --- .../sink/HstsMissingHeaderModuleImpl.java | 14 ++-- .../HstsMissingHeaderModuleUrlSchemeTest.java | 66 +++++++++++++++++++ .../tagprocessor/QueryObfuscatorTest.java | 4 +- 3 files changed, 78 insertions(+), 6 deletions(-) create mode 100644 dd-java-agent/agent-iast/src/test/java/com/datadog/iast/sink/HstsMissingHeaderModuleUrlSchemeTest.java 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-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 893442f8555..20a302a4706 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 @@ -283,9 +283,9 @@ void otelClientQueryIsOnlyUsedToBuildUrlFull(String spanType, boolean retainQuer assertEquals( "http://site.com/index?&otel_value=retained#fragment", tags.get(Tags.HTTP_URL)); - assertEquals("&otel_value=retained", tags.get(DDTags.HTTP_QUERY)); + assertEquals("&dd_value=not_kept", tags.get(DDTags.HTTP_QUERY)); if (retainQuery) { - assertEquals("", tags.get(KnownTags.URL_QUERY_NAME)); + assertEquals("&otel_value=retained", tags.get(KnownTags.URL_QUERY_NAME)); return; } assertNull(tags.get(KnownTags.URL_QUERY_NAME)); From abc1c63cd47c195a56ad310a63205a9e7ac6c192 Mon Sep 17 00:00:00 2001 From: Zach Montoya Date: Tue, 20 Oct 2026 17:20:37 -0700 Subject: [PATCH 18/22] PR feedback: Address the following issues: - In the BaseDecorator.setPeerPort methods (used by both clients and servers), set the tag via the directionless KnownTags.PEER_PORT_ID key. - In HttpServerDecorator, strip the query and fragment from the URL before setting the OTel attribute "url.path". Also, set the OTel attribute "url.query" if a query string is present - In AxwayHTTPPluginDecorator make sure to honor the query opt-out before setting the OTel attribute "url.query" --- .../decorator/BaseDecorator.java | 4 +- .../decorator/HttpServerDecorator.java | 21 +++- ...HttpDecoratorsOtelSemanticsForkedTest.java | 95 ++++++++++++++++++- .../axway/AxwayHTTPPluginDecorator.java | 4 +- 4 files changed, 118 insertions(+), 6 deletions(-) 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 31484e8b21c..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 @@ -223,12 +223,12 @@ public final void onPeerConnection( } public void setPeerPort(AgentSpan span, String port) { - span.setTag(KnownTags.Outbound.SERVER_PORT_ID, port); + span.setTag(KnownTags.PEER_PORT_ID, port); } public void setPeerPort(AgentSpan span, int port) { if (port > UNSET_PORT) { - span.setTag(KnownTags.Outbound.SERVER_PORT_ID, port); + span.setTag(KnownTags.PEER_PORT_ID, port); } } 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 02b5541416e..57e8220ee5e 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 @@ -7,6 +7,7 @@ import static datadog.trace.bootstrap.ActiveSubsystems.APPSEC_ACTIVE; import static datadog.trace.bootstrap.instrumentation.api.AgentSpan.fromContext; import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.traceConfig; +import static datadog.trace.bootstrap.instrumentation.api.URIUtils.decode; import static datadog.trace.bootstrap.instrumentation.decorator.http.HttpResourceDecorator.HTTP_RESOURCE_DECORATOR; import datadog.appsec.api.blocking.BlockingException; @@ -373,7 +374,25 @@ protected void doOnRequest( } span.setTag(KnownTags.URL_PATH_ID, path); } else if (supportsRaw) { - span.setTag(KnownTags.URL_PATH_ID, url.raw()); + String raw = url.raw(); + if (raw != null) { + int fragmentStart = raw.indexOf('#'); + if (fragmentStart >= 0) { + raw = raw.substring(0, fragmentStart); + } + } + int queryStart = raw == null ? -1 : raw.indexOf('?'); + span.setTag( + KnownTags.URL_PATH_ID, queryStart < 0 ? raw : raw.substring(0, queryStart)); + if (queryStart >= 0 && config.isHttpServerTagQueryString()) { + String query = raw.substring(queryStart + 1); + if (!config.isHttpServerRawQueryString()) { + query = decode(query, url.hasPlusEncodedSpaces()); + } + if (!query.isEmpty()) { + span.setTag(KnownTags.URL_QUERY_ID, query); + } + } } OtelHttpSemantics.setServerAddressAndPort( span, 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 index dd6fc239c7b..5f09b1a6cc8 100644 --- 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 @@ -3,6 +3,7 @@ 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; @@ -32,6 +33,7 @@ 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; @@ -192,6 +194,91 @@ void serverDecoratorCapturesAndObfuscatesQueryWhenEnabled() throws Exception { 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.URL_QUERY_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.URL_QUERY_NAME)); + + span.processTagsAndBaggage(NO_OP, OWN_PROPAGATION_TAGS); + + assertEquals(expectedPath, span.getTag(KnownTags.URL_PATH_NAME)); + assertEquals(obfuscatedQuery, span.getTag(KnownTags.URL_QUERY_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.URL_QUERY_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.URL_QUERY_NAME)); + } + @Test void serverDecoratorOmitsClientIpsAndForwardedHeadersByDefault() throws Exception { TestServerDecorator decorator = new TestServerDecorator(); @@ -550,7 +637,7 @@ protected String getResponseHeader(Integer response, String headerName) { private static final class ServerRequest { private final String method; - private final URI uri; + private final URIDataAdapter uri; private final String hostHeader; private ServerRequest(String method, URI uri) { @@ -558,6 +645,10 @@ private ServerRequest(String method, URI uri) { } 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; @@ -608,7 +699,7 @@ protected String method(ServerRequest request) { @Override protected URIDataAdapter url(ServerRequest request) { - return new URIDefaultDataAdapter(request.uri); + return request.uri; } @Override 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 1343aa5854a..ffd75145b9f 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 @@ -228,7 +228,9 @@ private static void setOtelTransactionTags(AgentSpan span, Object stateInstance) if (uri.getPath() != null) { span.setTag(KnownTags.URL_PATH_ID, uri.getPath()); } - if (uri.getRawQuery() != null && !uri.getRawQuery().isEmpty()) { + if (Config.get().isHttpServerTagQueryString() + && uri.getRawQuery() != null + && !uri.getRawQuery().isEmpty()) { span.setTag(KnownTags.URL_QUERY_ID, uri.getRawQuery()); } if (host == null || host.isEmpty()) { From 18c7ce93430ffdff505e22ba6ddec6e308196091 Mon Sep 17 00:00:00 2001 From: Zach Montoya Date: Tue, 20 Oct 2026 17:23:03 -0700 Subject: [PATCH 19/22] PR Feedback: Remove DDTags.HTTP_QUERY_OTEL_NAME (public API) and use KnownTags.URL_QUERY_NAME (OTel "url.query") in its place --- .../instrumentation/decorator/HttpServerDecorator.java | 2 +- .../trace/agent/test/base/HttpClientTest.groovy | 1 - .../trace/agent/test/base/HttpServerTest.groovy | 2 +- .../play26/server/AbstractPlayServerTest.groovy | 2 +- .../test/groovy/server/RatpackHttpServerTest.groovy | 2 +- .../src/main/java/datadog/trace/api/DDTags.java | 1 - .../trace/core/tagprocessor/QueryObfuscator.java | 5 +++-- .../java/datadog/trace/lambda/LambdaAppSecHandler.java | 3 ++- .../trace/core/tagprocessor/QueryObfuscatorTest.java | 10 +++++----- .../datadog/trace/lambda/LambdaAppSecHandlerTest.java | 5 +++-- .../lambda/LambdaHttpOtelSemanticsForkedTest.java | 7 ++++--- 11 files changed, 21 insertions(+), 19 deletions(-) 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 57e8220ee5e..0f04ad4002e 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 @@ -421,7 +421,7 @@ protected void doOnRequest( supportsRaw && config.isHttpServerRawQueryString() ? url.rawQuery() : url.query(); if (OTEL_SEMANTICS) { if (query != null && !query.isEmpty()) { - span.setTag(DDTags.HTTP_QUERY_OTEL_NAME, query); + span.setTag(KnownTags.URL_QUERY_NAME, query); } } else { span.setTag(KnownTags.HTTP_QUERY_STRING_ID, query); 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 244e3fdc650..90cb45d85d0 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 @@ -1036,7 +1036,6 @@ abstract class HttpClientTest extends VersionedNamingTestBase { "$Tags.HTTP_STATUS" status } if (otelSemantics) { - "$DDTags.HTTP_QUERY_OTEL_NAME" null "$KnownTags.URL_QUERY_NAME" null "$DDTags.HTTP_FRAGMENT" null } else if (tagQueryString) { 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 f2be4d853fd..e1b3b769719 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 @@ -2769,7 +2769,7 @@ abstract class HttpServerTest extends WithHttpServer { } if (endpoint.query) { if (otelSemantics) { - "$DDTags.HTTP_QUERY_OTEL_NAME" expectedQueryTag + "$KnownTags.URL_QUERY_NAME" expectedQueryTag } else { "$DDTags.HTTP_QUERY" expectedQueryTag } 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 86217ddcc97..1f4ec4f37c2 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 @@ -160,7 +160,7 @@ class AbstractPlayServerTest extends HttpServerTest { } if (endpoint.query) { if (otelSemantics) { - "$DDTags.HTTP_QUERY_OTEL_NAME" expectedQueryTag + "$KnownTags.URL_QUERY_NAME" expectedQueryTag } else { "$DDTags.HTTP_QUERY" expectedQueryTag } 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 ca2fa0ddf5d..db23561dee3 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 @@ -167,7 +167,7 @@ class RatpackHttpServerTest extends HttpServerTest { "$DDTags.ERROR_TYPE" endpoint.status.toString() } if (endpoint.query) { - tag(otelSemantics ? DDTags.HTTP_QUERY_OTEL_NAME : DDTags.HTTP_QUERY, endpoint.rawQuery) + tag(otelSemantics ? KnownTags.URL_QUERY_NAME : DDTags.HTTP_QUERY, endpoint.rawQuery) } defaultTags() } diff --git a/dd-trace-api/src/main/java/datadog/trace/api/DDTags.java b/dd-trace-api/src/main/java/datadog/trace/api/DDTags.java index f2c34f9a9e0..756048551c9 100644 --- a/dd-trace-api/src/main/java/datadog/trace/api/DDTags.java +++ b/dd-trace-api/src/main/java/datadog/trace/api/DDTags.java @@ -35,7 +35,6 @@ public class DDTags { public static final String PATHWAY_HASH = "pathway.hash"; public static final String HTTP_QUERY = "http.query.string"; - public static final String HTTP_QUERY_OTEL_NAME = "url.query"; public static final String HTTP_FRAGMENT = "http.fragment.string"; public static final String USER_NAME = "user.principal"; 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 f1535c127ea..aad6bf0db01 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 @@ -6,6 +6,7 @@ import datadog.trace.api.Config; import datadog.trace.api.DDSpanTypes; import datadog.trace.api.DDTags; +import datadog.trace.api.KnownTags; import datadog.trace.api.TagMap; import datadog.trace.api.internal.VisibleForTesting; import datadog.trace.bootstrap.instrumentation.api.AppendableSpanLinks; @@ -126,7 +127,7 @@ public void processTags( TagMap unsafeTags, DDSpanContext spanContext, AppendableSpanLinks spanLinks) { String query = obfuscateTag(unsafeTags, DDTags.HTTP_QUERY); if (otelSemanticsEnabled) { - String otelQuery = obfuscateTag(unsafeTags, DDTags.HTTP_QUERY_OTEL_NAME); + String otelQuery = obfuscateTag(unsafeTags, KnownTags.URL_QUERY_NAME); if (otelQuery != null) { query = otelQuery; } @@ -146,7 +147,7 @@ public void processTags( && spanContext != null && spanContext.getSpanType() != null && DDSpanTypes.HTTP_CLIENT.contentEquals(spanContext.getSpanType())) { - unsafeTags.remove(DDTags.HTTP_QUERY_OTEL_NAME); + unsafeTags.remove(KnownTags.URL_QUERY_NAME); } } 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 eafa4636d96..e34addb4fbd 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 @@ -10,6 +10,7 @@ import datadog.logging.RatelimitedLogger; import datadog.trace.api.Config; import datadog.trace.api.DDTags; +import datadog.trace.api.KnownTags; import datadog.trace.api.ProductTraceSource; import datadog.trace.api.TagMap; import datadog.trace.api.appsec.AppSecContext; @@ -56,7 +57,7 @@ public class LambdaAppSecHandler { private static final Logger log = LoggerFactory.getLogger(LambdaAppSecHandler.class); private static final RatelimitedLogger rlLog = new RatelimitedLogger(log, 5, TimeUnit.MINUTES); private static final String HTTP_QUERY_TAG = - Config.get().isTraceOtelSemanticsEnabled() ? DDTags.HTTP_QUERY_OTEL_NAME : DDTags.HTTP_QUERY; + Config.get().isTraceOtelSemanticsEnabled() ? KnownTags.URL_QUERY_NAME : DDTags.HTTP_QUERY; /** * Marks an invocation AppSec did not process because the trigger is not HTTP, or if the event is 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 20a302a4706..3915bc9d8c7 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 @@ -145,7 +145,7 @@ void userCopyOfDefaultPatternAlsoRedactsDelimiter() { "OpenTelemetry | true " }) void invalidRegexDropsQueryString(boolean otelSemantics) { - String queryTag = otelSemantics ? DDTags.HTTP_QUERY_OTEL_NAME : DDTags.HTTP_QUERY; + String queryTag = otelSemantics ? KnownTags.URL_QUERY_NAME : DDTags.HTTP_QUERY; TagMap unsafeTags = process(new QueryObfuscator("(?<=a)b", otelSemantics), "pwd=secret", queryTag); @@ -219,13 +219,13 @@ void otelSemanticsObfuscatesBothQueryTags( tags.put(DDTags.HTTP_QUERY, legacyQuery); } if (otelQuery != null) { - tags.put(DDTags.HTTP_QUERY_OTEL_NAME, otelQuery); + tags.put(KnownTags.URL_QUERY_NAME, otelQuery); } new QueryObfuscator(null, true).processTags(tags, null, link -> {}); assertEquals(expectedLegacy, tags.get(DDTags.HTTP_QUERY)); - assertEquals(expectedOtel, tags.get(DDTags.HTTP_QUERY_OTEL_NAME)); + assertEquals(expectedOtel, tags.get(KnownTags.URL_QUERY_NAME)); assertEquals( "http://site.com/index?" + expectedUrlQuery + "#fragment", tags.get(Tags.HTTP_URL)); } @@ -255,12 +255,12 @@ void invalidRegexDropsBothQueryTagsInOtelMode() { TagMap tags = TagMap.create(); tags.put(Tags.HTTP_URL, "http://site.com/index#fragment"); tags.put(DDTags.HTTP_QUERY, "token=legacy"); - tags.put(DDTags.HTTP_QUERY_OTEL_NAME, "pwd=otel"); + tags.put(KnownTags.URL_QUERY_NAME, "pwd=otel"); new QueryObfuscator("(?<=a)b", true).processTags(tags, null, link -> {}); assertFalse(tags.containsKey(DDTags.HTTP_QUERY)); - assertFalse(tags.containsKey(DDTags.HTTP_QUERY_OTEL_NAME)); + assertFalse(tags.containsKey(KnownTags.URL_QUERY_NAME)); assertEquals("http://site.com/index#fragment", tags.get(Tags.HTTP_URL)); } 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 486ad23c91b..c81e09ef0c1 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 @@ -29,6 +29,7 @@ import datadog.trace.api.Config; import datadog.trace.api.DDTags; +import datadog.trace.api.KnownTags; import datadog.trace.api.ProductTraceSource; import datadog.trace.api.appsec.AppSecContext; import datadog.trace.api.function.TriConsumer; @@ -2468,7 +2469,7 @@ void appliesHttpTagsForRestApiEvent() { assertEquals("GET", tags.get(Tags.HTTP_METHOD)); assertEquals("https://api.example.com/users/42", tags.get(Tags.HTTP_URL)); assertEquals("q=hello", tags.get(DDTags.HTTP_QUERY)); - assertNull(tags.get(DDTags.HTTP_QUERY_OTEL_NAME)); + assertNull(tags.get(KnownTags.URL_QUERY_NAME)); assertEquals("curl/8.1", tags.get(Tags.HTTP_USER_AGENT)); assertEquals("/users/{id}", tags.get(Tags.HTTP_ROUTE)); assertEquals("api.example.com", tags.get(Tags.HTTP_HOSTNAME)); @@ -2565,7 +2566,7 @@ void keepsRepeatedQueryKeysFromTheV2RawQueryString() { // would report a=1%2C2 Map tags = tagsOf(context); assertEquals("a=1&a=2", tags.get(DDTags.HTTP_QUERY)); - assertNull(tags.get(DDTags.HTTP_QUERY_OTEL_NAME)); + assertNull(tags.get(KnownTags.URL_QUERY_NAME)); } @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 index 309676f8f79..1a4e1bcfe9a 100644 --- a/dd-trace-core/src/test/java/datadog/trace/lambda/LambdaHttpOtelSemanticsForkedTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/lambda/LambdaHttpOtelSemanticsForkedTest.java @@ -11,6 +11,7 @@ import datadog.trace.api.DDSpanTypes; import datadog.trace.api.DDTags; +import datadog.trace.api.KnownTags; import datadog.trace.bootstrap.instrumentation.api.TagContext; import datadog.trace.bootstrap.instrumentation.api.Tags; import datadog.trace.common.writer.ListWriter; @@ -64,21 +65,21 @@ void lambdaQueryIsDroppedWhenPatternCannotCompile() { void lambdaOmitsQueryAttributesWhenCaptureIsDisabled() { TagContext parent = lambdaHttpContext(); - assertNull(parent.getTags().get(DDTags.HTTP_QUERY_OTEL_NAME)); + assertNull(parent.getTags().get(KnownTags.URL_QUERY_NAME)); 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_OTEL_NAME)); + assertEquals("token=secret&view=full", parent.getTags().get(KnownTags.URL_QUERY_NAME)); assertNull(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_OTEL_NAME)); + assertEquals(expectedQuery, span.getTag(KnownTags.URL_QUERY_NAME)); assertNull(span.getTag(DDTags.HTTP_QUERY)); assertEquals(expectedUrl, span.getTag(Tags.HTTP_URL).toString()); } From d27a8f17140c29f37f7503eea09ac6b1f48f30e7 Mon Sep 17 00:00:00 2001 From: Zach Montoya Date: Tue, 20 Oct 2026 18:49:39 -0700 Subject: [PATCH 20/22] PR Feedback: Use the TagExtractor infrastructure to simplify setting the url-based tags on http clients and servers, for both Datadog and OTel semantics. Key details include: - HttpServerUrl is created as an abstract class to expose a consistent interface for accessing underlying URL facts from various request objects -- there is no equivalent for client spans as the logic is simple enough that operating on a URL object is sufficient - OtelServerUrl and DatadogServerUrl implement TagExtractor to provide the "OTEL_SEMANTICS branching" for populating http server span tags - DatadogClientUrl and OtelClientUrl implement TagExtractor to provide the "OTEL_SEMANTICS branching" for populating http client span tags - The host and hostPort calculations from OtelHttpSemantics.setServerAddressAndPort are migrated to HttpServerUrl.Origin.resolve - Additional logic from OtelHttpSemantics is ported to the new UrlExtractors, so a handful of tests in OtelHttpSemanticsTest.java are simply ported to HttpUrlExtractorsTest.java - HttpUrlExtractorsTest.java provides testing of the TagExtractor implementations --- .github/CODEOWNERS | 1 + .../decorator/HttpClientDecorator.java | 21 +- .../decorator/HttpServerDecorator.java | 98 +----- .../decorator/UrlConnectionDecorator.java | 16 +- .../decorator/NameKeyedAgentSpan.java | 6 + .../axway/AxwayHTTPPluginDecorator.java | 96 ++---- .../trace/api/gateway/InferredProxySpan.java | 16 +- .../api/OtelHttpSemantics.java | 85 ----- .../api/http/DatadogClientUrl.java | 37 +++ .../api/http/DatadogServerUrl.java | 37 +++ .../api/http/HttpServerUrl.java | 293 ++++++++++++++++++ .../api/http/OtelClientUrl.java | 50 +++ .../api/http/OtelServerUrl.java | 37 +++ .../api/HttpUrlExtractorsTest.java | 282 +++++++++++++++++ .../api/OtelHttpSemanticsTest.java | 138 --------- 15 files changed, 797 insertions(+), 416 deletions(-) create mode 100644 internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/http/DatadogClientUrl.java create mode 100644 internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/http/DatadogServerUrl.java create mode 100644 internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/http/HttpServerUrl.java create mode 100644 internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/http/OtelClientUrl.java create mode 100644 internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/http/OtelServerUrl.java create mode 100644 internal-api/src/test/java/datadog/trace/bootstrap/instrumentation/api/HttpUrlExtractorsTest.java 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/HttpClientDecorator.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/HttpClientDecorator.java index 9375a84939f..292b0a2bcf1 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,7 +7,6 @@ import datadog.appsec.api.blocking.BlockingException; import datadog.trace.api.Config; -import datadog.trace.api.DDTags; import datadog.trace.api.InstrumenterConfig; import datadog.trace.api.KnownTags; import datadog.trace.api.ProductActivation; @@ -25,8 +24,10 @@ import datadog.trace.bootstrap.instrumentation.api.AgentTracer; import datadog.trace.bootstrap.instrumentation.api.InternalSpanTypes; import datadog.trace.bootstrap.instrumentation.api.OtelHttpSemantics; -import datadog.trace.bootstrap.instrumentation.api.URIUtils; +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; @@ -44,6 +45,8 @@ public abstract class HttpClientDecorator extends UriBasedCli 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("/"); @@ -139,23 +142,11 @@ protected void doOnRequest(final AgentSpan span, final REQUEST request) { } } - // Copy of HttpServerDecorator url handling try { final URI url = url(request); if (url != null) { onURI(span, url); - if (OTEL_SEMANTICS) { - OtelHttpSemantics.setClientUrl(span, url); - } else { - span.setTag( - KnownTags.HTTP_URL_ID, - 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()); 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 0f04ad4002e..54fd6c6096a 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 @@ -7,14 +7,12 @@ import static datadog.trace.bootstrap.ActiveSubsystems.APPSEC_ACTIVE; import static datadog.trace.bootstrap.instrumentation.api.AgentSpan.fromContext; import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.traceConfig; -import static datadog.trace.bootstrap.instrumentation.api.URIUtils.decode; import static datadog.trace.bootstrap.instrumentation.decorator.http.HttpResourceDecorator.HTTP_RESOURCE_DECORATOR; import datadog.appsec.api.blocking.BlockingException; 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; @@ -39,10 +37,13 @@ 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; @@ -72,6 +73,8 @@ 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 = !OTEL_SEMANTICS @@ -341,7 +344,6 @@ protected void doOnRequest( span.setTag(KnownTags.HTTP_METHOD_ID, method); } - // Copy of HttpClientDecorator url handling try { final URIDataAdapter url = url(request); if (url != null) { @@ -349,85 +351,15 @@ protected void doOnRequest( boolean encoded = supportsRaw && config.isHttpServerRawResource(); boolean valid = url.isValid(); String path = encoded ? url.rawPath() : url.path(); - if (OTEL_SEMANTICS) { - String scheme = - OtelHttpSemantics.forwardedValue( - extracted == null ? null : extracted.getXForwardedProto()); - if (scheme == null) { - scheme = url.scheme(); - } - int requestPort = url.port(); - if (scheme != null && !scheme.equalsIgnoreCase(url.scheme())) { - // A forwarded scheme can describe a different origin from the backend connection. - // Preserve an explicit public Host port; otherwise use the origin scheme default. - requestPort = OtelHttpSemantics.portFromHost(getRequestHeader(request, "Host")); - } - if (valid) { - if (scheme != null) { - span.setTag(KnownTags.URL_SCHEME_ID, scheme); - } - // Ensure that path components have a leading '/' - if (path == null || path.isEmpty()) { - path = "/"; - } else if (path.charAt(0) != '/') { - path = "/" + path; - } - span.setTag(KnownTags.URL_PATH_ID, path); - } else if (supportsRaw) { - String raw = url.raw(); - if (raw != null) { - int fragmentStart = raw.indexOf('#'); - if (fragmentStart >= 0) { - raw = raw.substring(0, fragmentStart); - } - } - int queryStart = raw == null ? -1 : raw.indexOf('?'); - span.setTag( - KnownTags.URL_PATH_ID, queryStart < 0 ? raw : raw.substring(0, queryStart)); - if (queryStart >= 0 && config.isHttpServerTagQueryString()) { - String query = raw.substring(queryStart + 1); - if (!config.isHttpServerRawQueryString()) { - query = decode(query, url.hasPlusEncodedSpaces()); - } - if (!query.isEmpty()) { - span.setTag(KnownTags.URL_QUERY_ID, query); - } - } - } - OtelHttpSemantics.setServerAddressAndPort( - span, - extracted == null ? null : extracted.getXForwardedHost(), - extracted == null ? null : extracted.getXForwardedPort(), - scheme, - url.host(), - requestPort); - } else { - if (valid) { - span.setTag( - KnownTags.HTTP_URL_ID, - URIUtils.lazyValidURL(url.scheme(), url.host(), url.port(), path)); - } else if (supportsRaw) { - span.setTag(KnownTags.HTTP_URL_ID, URIUtils.lazyInvalidUrl(url.raw())); - } - if (extracted != null && extracted.getXForwardedHost() != null) { - span.setTag(KnownTags.HTTP_HOSTNAME_ID, extracted.getXForwardedHost()); - } else if (url.host() != null) { - span.setTag(KnownTags.HTTP_HOSTNAME_ID, url.host()); - } - } - - if (valid && config.isHttpServerTagQueryString()) { - String query = - supportsRaw && config.isHttpServerRawQueryString() ? url.rawQuery() : url.query(); - if (OTEL_SEMANTICS) { - if (query != null && !query.isEmpty()) { - span.setTag(KnownTags.URL_QUERY_NAME, query); - } - } else { - span.setTag(KnownTags.HTTP_QUERY_STRING_ID, query); - span.setTag(DDTags.HTTP_FRAGMENT, url.fragment()); - } - } + span.setTagsFrom( + HttpServerUrl.fromRequest( + url, + path, + extracted, + 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()); 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 09979ae34fb..6454e76bbb3 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,14 +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.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.net.URL; @@ -18,7 +19,10 @@ import org.slf4j.LoggerFactory; public class UrlConnectionDecorator extends UriBasedClientDecorator { - private static final boolean OTEL_SEMANTICS = Config.get().isTraceOtelSemanticsEnabled(); + private static final TagExtractor URL_TAGS = + Config.get().isTraceOtelSemanticsEnabled() + ? OtelClientUrl.INSTANCE + : DatadogClientUrl.FULL_URL; private static final DDCache CACHE = DDCaches.newFixedSizeCache(16); private static final Function ADDER = @@ -54,11 +58,7 @@ protected String service() { @Override public void onURI(@Nonnull AgentSpan span, @Nonnull URI uri) { super.onURI(span, uri); - if (OTEL_SEMANTICS) { - OtelHttpSemantics.setClientUrl(span, uri); - } else { - span.setTag(KnownTags.HTTP_URL_ID, uri.toString()); - } + span.setTagsFrom(uri, URL_TAGS); } public void onURL(@Nonnull final AgentSpan span, @Nonnull final URL url) { 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/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 ffd75145b9f..3f245d9d9e4 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 @@ -3,15 +3,17 @@ import static java.lang.invoke.MethodType.methodType; import datadog.trace.api.Config; -import datadog.trace.api.KnownTagCodec; 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.OtelHttpSemantics; -import datadog.trace.bootstrap.instrumentation.api.Tags; +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; @@ -27,6 +29,8 @@ public class AxwayHTTPPluginDecorator extends HttpServerDecorator { private static final Logger log = LoggerFactory.getLogger(AxwayHTTPPluginDecorator.class); private static final boolean OTEL_SEMANTICS = Config.get().isTraceOtelSemanticsEnabled(); + private static final TagExtractor URL_TAGS = + OTEL_SEMANTICS ? OtelServerUrl.INSTANCE : DatadogServerUrl.URL_ONLY; public static final CharSequence AXWAY_TRY_TRANSACTION = UTF8BytesString.create("axway.trytransaction"); @@ -203,49 +207,24 @@ protected int status(final Object serverTransaction) { */ public void onTransaction(AgentSpan span, Object stateInstance) { if (span != null) { + String method = getStringFromStateField(stateInstance, methodField_mh); + String host = getStringFromStateField(stateInstance, hostField_mh); + String port = getStringFromStateField(stateInstance, portField_mh); if (OTEL_SEMANTICS) { - setOtelTransactionTags(span, stateInstance); + OtelHttpSemantics.setRequestMethod(span, method); + OtelHttpSemantics.setMethodResourceName(span, method); } else { - setStringTagFromStateField(span, KnownTags.PEER_HOSTNAME_ID, stateInstance, hostField_mh); - setStringTagFromStateField(span, KnownTags.PEER_PORT_ID, stateInstance, portField_mh); - setStringTagFromStateField(span, KnownTags.HTTP_METHOD_ID, stateInstance, methodField_mh); - setURLTagFromUriStateField(span, stateInstance); + 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); } } - private static void setOtelTransactionTags(AgentSpan span, Object stateInstance) { - String method = getStringFromStateField(stateInstance, methodField_mh); - OtelHttpSemantics.setRequestMethod(span, method); - OtelHttpSemantics.setMethodResourceName(span, method); - - URI uri = getUriFromStateField(stateInstance); - String scheme = null; - String host = getStringFromStateField(stateInstance, hostField_mh); - int port = OtelHttpSemantics.parsePort(getStringFromStateField(stateInstance, portField_mh)); - if (uri != null) { - scheme = uri.getScheme(); - if (uri.getPath() != null) { - span.setTag(KnownTags.URL_PATH_ID, uri.getPath()); - } - if (Config.get().isHttpServerTagQueryString() - && uri.getRawQuery() != null - && !uri.getRawQuery().isEmpty()) { - span.setTag(KnownTags.URL_QUERY_ID, uri.getRawQuery()); - } - if (host == null || host.isEmpty()) { - host = uri.getHost(); - } - if (port <= 0) { - port = uri.getPort(); - } - } - if (scheme != null) { - span.setTag(KnownTags.URL_SCHEME_ID, scheme); - } - OtelHttpSemantics.setServerAddressAndPort(span, null, null, scheme, host, port); - } - /** * class hierarchy in package com.vordel.dwe.http : * @@ -274,22 +253,6 @@ private static InetSocketAddress getRemoteAddr(Object obj) { return new InetSocketAddress(0); } - private static void setStringTagFromStateField( - AgentSpan span, long tagId, Object stateInstance, MethodHandle mh) { - String v = ""; - try { - v = (String) mh.invoke(stateInstance); - } catch (Throwable e) { - log.debug( - "Can't invoke '{}' on instance '{}'; ; Tag '{}' not set.", - mh, - stateInstance, - KnownTagCodec.nameOf(tagId), - e); - } - span.setTag(tagId, v); - } - private static String getStringFromStateField(Object stateInstance, MethodHandle mh) { try { Object value = mh.invoke(stateInstance); @@ -299,27 +262,4 @@ private static String getStringFromStateField(Object stateInstance, MethodHandle return null; } } - - private static void setURLTagFromUriStateField(AgentSpan span, Object stateInstance) { - try { - span.setTag(KnownTags.HTTP_URL_ID, 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); - } - } - - private static URI getUriFromStateField(Object stateInstance) { - try { - Object value = uriField_mh.invoke(stateInstance); - return value == null ? null : URI.create(value.toString()); - } catch (Throwable e) { - log.debug("Can't invoke '{}' on instance '{}'; URL not read.", uriField_mh, stateInstance, e); - return null; - } - } } 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 5a3af61331c..c6153045fd6 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 @@ -18,6 +18,10 @@ 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; @@ -39,6 +43,8 @@ public class InferredProxySpan implements ImplicitContextKeyed { 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<>(); @@ -110,18 +116,10 @@ public AgentSpanContext start(AgentSpanContext extracted) { if (OTEL_SEMANTICS) { OtelHttpSemantics.setRequestMethod(span, httpMethod); - span.setTag(KnownTags.URL_PATH_ID, path); - if (domainName != null && !domainName.isEmpty()) { - span.setTag(KnownTags.URL_SCHEME_ID, "https"); - span.setTag(KnownTags.Inbound.SERVER_ADDRESS_ID, domainName); - span.setTag(KnownTags.Inbound.SERVER_PORT_ID, 443); - } } else { span.setTag(KnownTags.HTTP_METHOD_ID, httpMethod); - span.setTag( - KnownTags.HTTP_URL_ID, - domainName != null && !domainName.isEmpty() ? "https://" + domainName + path : path); } + span.setTagsFrom(HttpServerUrl.fromInferredProxy(domainName, path), URL_TAGS); // A concrete path is not a route under the OTel conventions. Keep the legacy fallback only // while Datadog semantics are active. 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 index 4ea044130fa..35c96128eb7 100644 --- 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 @@ -1,6 +1,5 @@ package datadog.trace.bootstrap.instrumentation.api; -import datadog.trace.api.Config; import datadog.trace.api.DDTags; import datadog.trace.api.KnownTags; import datadog.trace.api.cache.RadixTreeCache; @@ -37,22 +36,6 @@ public static void setMethodResourceName(final AgentSpan span, final CharSequenc span.setResourceName(spanNameMethod(method), ResourceNamePriorities.HTTP_PATH_NORMALIZER); } - public static void setClientUrl(final AgentSpan span, final URI url) { - // Use the object tag setter rather than the CharSequence overload, which checks length(), which - // would render the URL immediately. - span.setTag(KnownTags.HTTP_URL_ID, (Object) new ClientUrl(url)); - if (Config.get().isHttpClientTagQueryString()) { - // QueryObfuscator scrubs this before rebuilding url.full and removes it from client spans. - // Decode rather than using getRawQuery(): the obfuscator matches literal parameter names, so - // a percent-encoded name such as t%6fken would hide the secret from it. This matches the - // query that non-OpenTelemetry client spans already carry in http.url. - final String query = url.getQuery(); - if (query != null) { - span.setTag(KnownTags.URL_QUERY_ID, query); - } - } - } - public static String spanNameMethod(final CharSequence method) { final String methodName = method == null ? null : method.toString(); return KNOWN_METHODS.contains(methodName) ? methodName : "HTTP"; @@ -143,53 +126,6 @@ public static int portFromHost(final String host) { return parsePort(host.substring(colon + 1)); } - public static void setServerAddressAndPort( - final AgentSpan span, - final String forwardedHostHeader, - final String forwardedPortHeader, - final String scheme, - final String requestHost, - final int requestPort) { - final String forwardedHost = forwardedValue(forwardedHostHeader); - String serverAddress = requestHost; - int hostPort = -1; - - if (forwardedHost != null) { - serverAddress = forwardedHost; - if (forwardedHost.charAt(0) == '[') { - final int closingBracket = forwardedHost.indexOf(']'); - if (closingBracket > 0) { - serverAddress = forwardedHost.substring(1, closingBracket); - if (closingBracket + 1 < forwardedHost.length() - && forwardedHost.charAt(closingBracket + 1) == ':') { - hostPort = parsePort(forwardedHost.substring(closingBracket + 2)); - } - } - } else { - final int colon = forwardedHost.lastIndexOf(':'); - if (colon > 0 && colon == forwardedHost.indexOf(':')) { - serverAddress = forwardedHost.substring(0, colon); - hostPort = parsePort(forwardedHost.substring(colon + 1)); - } - } - } - - if (serverAddress != null && !serverAddress.isEmpty()) { - span.setTag(KnownTags.Inbound.SERVER_ADDRESS_ID, serverAddress); - } - - int serverPort = parsePort(forwardedValue(forwardedPortHeader)); - if (serverPort <= 0) { - serverPort = hostPort; - } - if (serverPort <= 0) { - serverPort = forwardedHost == null && requestPort > 0 ? requestPort : serverPort(scheme, -1); - } - if (serverPort > 0) { - span.setTag(KnownTags.Inbound.SERVER_PORT_ID, serverPort); - } - } - public static int parsePort(final String value) { if (value == null) { return -1; @@ -208,25 +144,4 @@ public static int parsePort(final String value) { } return port > 0 && port <= 65535 ? port : -1; } - - /** Defers client URL redaction and query removal until the URL is read. */ - 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() { - String value = lazy; - return value != null ? value : withoutQuery(redactedUrl(uri)); - } - } } 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..ff2cef50298 --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/http/DatadogClientUrl.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; +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(false); + public static final DatadogClientUrl FULL_URL = new DatadogClientUrl(true); + + private final boolean fullUrl; + + private DatadogClientUrl(boolean fullUrl) { + this.fullUrl = fullUrl; + } + + @Override + public void extract(URI uri, AgentSpan span) { + if (fullUrl) { + // URLConnection historically records the original URL, including its query and fragment. + span.setTag(KnownTags.HTTP_URL_ID, uri.toString()); + } else { + 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..9a8a65e0d97 --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/http/HttpServerUrl.java @@ -0,0 +1,293 @@ +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 Origin origin; + + 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() { + if (origin == null) { + 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); + } + origin = + Origin.resolve( + scheme, + uri.host(), + port, + extracted == null ? null : extracted.getXForwardedHost(), + extracted == null ? null : extracted.getXForwardedPort()); + } + return origin; + } + } + + private static final class InferredProxy extends HttpServerUrl { + private final String domain; + private final String path; + private Origin origin; + + 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() { + if (origin == null) { + origin = new Origin(domain != null ? "https" : null, domain, domain != null ? 443 : -1); + } + return origin; + } + } + + 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 Origin origin; + + 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() { + if (origin == null) { + 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(); + } + } + origin = new Origin(scheme, address, serverPort(scheme, port)); + } + return origin; + } + } +} 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..b256f5b47db --- /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.URL_QUERY_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..327ed7c5f5d --- /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.URL_QUERY_ID, query); + } + } + } +} 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..9bae186a9f4 --- /dev/null +++ b/internal-api/src/test/java/datadog/trace/bootstrap/instrumentation/api/HttpUrlExtractorsTest.java @@ -0,0 +1,282 @@ +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.URL_QUERY_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.URL_QUERY_ID, expectedQuery); + } + + @Test + void otelClientOmitsQueryTagWhenAbsent() { + OtelClientUrl.INSTANCE.extract(URI.create("http://host/a"), span); + + verify(span, never()).setTag(eq(KnownTags.URL_QUERY_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 datadogUrlConnectionKeepsOriginalUrl() { + String url = "http://u:p@host:80/a?q=1#fragment"; + + DatadogClientUrl.FULL_URL.extract(URI.create(url), span); + + verify(span).setTag(KnownTags.HTTP_URL_ID, url); + verify(span, never()).setTag(eq(KnownTags.HTTP_QUERY_STRING_ID), anyString()); + } + + @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.URL_QUERY_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.URL_QUERY_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.URL_QUERY_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 index f47e6f979e3..74aeb2155cd 100644 --- 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 @@ -3,7 +3,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; @@ -13,12 +12,10 @@ import datadog.trace.api.DDTags; import datadog.trace.api.KnownTags; -import datadog.trace.bootstrap.instrumentation.api.URIUtils.LazyUrl; import java.net.URI; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; -import org.mockito.ArgumentCaptor; import org.tabletest.junit.TableTest; class OtelHttpSemanticsTest { @@ -84,52 +81,6 @@ void setMethodResourceNameUsesHttpForNullMethod() { verify(span).setResourceName("HTTP", ResourceNamePriorities.HTTP_PATH_NORMALIZER); } - @Test - void setClientUrlLazilyStripsQueryAndRedactsCredentials() { - AgentSpan span = mock(AgentSpan.class); - ArgumentCaptor urlCaptor = ArgumentCaptor.forClass(LazyUrl.class); - - OtelHttpSemantics.setClientUrl(span, URI.create("https://u:p@host/a?token=secret#fragment")); - - 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.URL_QUERY_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 setClientUrlDecodesQuerySoTheObfuscatorCanMatchEncodedNames( - String rawUrl, String expectedQuery) { - AgentSpan span = mock(AgentSpan.class); - - OtelHttpSemantics.setClientUrl(span, URI.create(rawUrl)); - - // The obfuscator matches literal parameter names, so the query it receives must be decoded. - verify(span).setTag(KnownTags.URL_QUERY_ID, expectedQuery); - } - - @Test - void setClientUrlOmitsQueryTagWhenAbsent() { - AgentSpan span = mock(AgentSpan.class); - - OtelHttpSemantics.setClientUrl(span, URI.create("http://host/a")); - - verify(span, never()).setTag(eq(KnownTags.URL_QUERY_ID), anyString()); - } - @TableTest({ "Scenario | Raw URL | Expected URL ", "No credentials | http://host:8080/p | http://host:8080/p ", @@ -258,16 +209,6 @@ void serverPortReturnsUnsetForNullScheme() { assertEquals(-1, OtelHttpSemantics.serverPort(null, -1)); } - @Test - void setServerAddressAndPortUsesRequestHostAndPort() { - AgentSpan span = mock(AgentSpan.class); - - OtelHttpSemantics.setServerAddressAndPort(span, null, null, "http", "host", 8080); - - verify(span).setTag(KnownTags.HTTP_HOSTNAME_ID, "host"); - verify(span).setTag(KnownTags.Inbound.SERVER_PORT_ID, 8080); - } - @TableTest({ "Scenario | Host | Expected port", "No host | | -1 ", @@ -283,83 +224,4 @@ void setServerAddressAndPortUsesRequestHostAndPort() { void portFromHostRequiresAnExplicitValidPort(String host, int expectedPort) { assertEquals(expectedPort, OtelHttpSemantics.portFromHost(host)); } - - @Test - void setServerAddressAndPortFallsBackToSchemeDefaultPort() { - AgentSpan span = mock(AgentSpan.class); - - OtelHttpSemantics.setServerAddressAndPort(span, null, null, "https", "host", -1); - - verify(span).setTag(KnownTags.HTTP_HOSTNAME_ID, "host"); - verify(span).setTag(KnownTags.Inbound.SERVER_PORT_ID, 443); - } - - @Test - void setServerAddressAndPortOmitsTagsWhenNothingResolvable() { - AgentSpan span = mock(AgentSpan.class); - - OtelHttpSemantics.setServerAddressAndPort(span, 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 setServerAddressAndPortOmitsAddressWhenRequestHostIsEmpty() { - AgentSpan span = mock(AgentSpan.class); - - OtelHttpSemantics.setServerAddressAndPort(span, 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 setServerAddressAndPortPrefersForwardedPortHeader() { - AgentSpan span = mock(AgentSpan.class); - - OtelHttpSemantics.setServerAddressAndPort(span, "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 setServerAddressAndPortFallsBackFromInvalidForwardedPort( - String forwardedHost, String forwardedPort, int expectedPort) { - AgentSpan span = mock(AgentSpan.class); - - OtelHttpSemantics.setServerAddressAndPort( - span, 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 setServerAddressAndPortParsesForwardedHost( - String forwardedHost, String expectedAddress, int expectedPort) { - AgentSpan span = mock(AgentSpan.class); - - OtelHttpSemantics.setServerAddressAndPort(span, 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); - } } From ba672e9a6417d62e1bfb887f65c844604a1bb48d Mon Sep 17 00:00:00 2001 From: Stuart McCulloch Date: Wed, 21 Oct 2026 15:10:37 +0100 Subject: [PATCH 21/22] Preserve default HTTP behavior with OTel semantics disabled Preserve existing query concatenation for captured URLs with decoded path characters when OTel semantics are disabled. Restore port-based service splitting precedence and skip the OTel-only Host header lookup in that mode. Add regression coverage for all three issues. Each new regression failed before the fixes; all 102 selected query obfuscator and decorator tests now pass, including existing OTel and client path-mapping coverage. --- .../decorator/HttpServerDecorator.java | 3 +- .../decorator/UriBasedClientDecorator.java | 7 +- ...pDecoratorsDatadogSemanticsForkedTest.java | 134 ++++++++++++++++++ .../core/tagprocessor/QueryObfuscator.java | 6 +- .../tagprocessor/QueryObfuscatorTest.java | 17 +++ 5 files changed, 161 insertions(+), 6 deletions(-) create mode 100644 dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/decorator/HttpDecoratorsDatadogSemanticsForkedTest.java 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 54fd6c6096a..0cfbb360a8b 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 @@ -356,7 +356,8 @@ protected void doOnRequest( url, path, extracted, - extracted != null && extracted.getXForwardedProto() != null + // 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); 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 9c0fa31cae1..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 @@ -20,13 +20,14 @@ public void onURI(@Nonnull final AgentSpan span, @Nonnull final URI uri) { String host = uri.getHost(); 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); } - if (Config.get().isHttpClientSplitByDomain() && host.charAt(0) >= 'A') { - span.setServiceName(host, component()); - } } } 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-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 aad6bf0db01..166b4250027 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 @@ -174,8 +174,10 @@ private String obfuscateTag(TagMap unsafeTags, String queryTag) { return null; } - private static String appendQuery(final String url, final String query) { - final int fragment = url.indexOf('#'); + 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; } 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 3915bc9d8c7..394900fdf83 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 @@ -12,8 +12,10 @@ import datadog.trace.api.KnownTags; 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; @@ -29,6 +31,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'", From 53b4e2cd56d33aca327aa8babcfa074296f74426 Mon Sep 17 00:00:00 2001 From: Stuart McCulloch Date: Wed, 21 Oct 2026 15:51:46 +0100 Subject: [PATCH 22/22] Simplify HTTP URL string assembly Use string concatenation and trailing substrings when inserting or removing queries while retaining fragments. Verify query obfuscation, HTTP semantics helpers, URL extractors, and OTel decorators with their existing tests and formatting checks. --- .../datadog/trace/core/tagprocessor/QueryObfuscator.java | 7 +------ .../bootstrap/instrumentation/api/OtelHttpSemantics.java | 5 +---- 2 files changed, 2 insertions(+), 10 deletions(-) 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 166b4250027..d4bd2ef0c7e 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 @@ -181,11 +181,6 @@ private String appendQuery(final String url, final String query) { if (fragment < 0) { return url + "?" + query; } - return new StringBuilder(url.length() + query.length() + 1) - .append(url, 0, fragment) - .append('?') - .append(query) - .append(url, fragment, url.length()) - .toString(); + return url.substring(0, fragment) + "?" + query + url.substring(fragment); } } 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 index 35c96128eb7..91c920495fc 100644 --- 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 @@ -69,10 +69,7 @@ public static String withoutQuery(final String url) { if (fragment < 0) { return url.substring(0, query); } - return new StringBuilder(url.length() - (fragment - query)) - .append(url, 0, query) - .append(url, fragment, url.length()) - .toString(); + return url.substring(0, query) + url.substring(fragment); } public static void setErrorType(final AgentSpan span, final int status) {