From 5ff30b64b6e165413d19f5b23b7bb0eefaa57a0a Mon Sep 17 00:00:00 2001 From: "sezen.leblay" Date: Wed, 10 Sep 2025 16:19:32 +0200 Subject: [PATCH 1/7] Inferred Routes and Resource Renaming: new APM trace metrics tag Signed-off-by: sezen.leblay --- .../HttpEndpointTaggingSmokeTest.groovy | 252 ++++++++++++++++ .../datadog/trace/api/ConfigDefaults.java | 6 + .../trace/api/config/TracerConfig.java | 5 + .../metrics/ConflatingMetricsAggregator.java | 8 +- .../common/metrics/HttpEndpointTagging.java | 268 ++++++++++++++++++ .../trace/common/metrics/MetricKey.java | 22 +- .../main/java/datadog/trace/core/DDSpan.java | 6 + .../HttpEndpointTaggingConfigTest.groovy | 49 ++++ .../HttpEndpointTaggingIntegrationTest.groovy | 90 ++++++ .../metrics/HttpEndpointTaggingTest.groovy | 214 ++++++++++++++ .../trace/common/metrics/MetricKeyTest.java | 213 ++++++++++++++ .../main/java/datadog/trace/api/Config.java | 25 ++ .../bootstrap/instrumentation/api/Tags.java | 1 + 13 files changed, 1156 insertions(+), 3 deletions(-) create mode 100644 dd-smoke-tests/springboot/src/test/groovy/datadog/smoketest/HttpEndpointTaggingSmokeTest.groovy create mode 100644 dd-trace-core/src/main/java/datadog/trace/common/metrics/HttpEndpointTagging.java create mode 100644 dd-trace-core/src/test/groovy/datadog/trace/common/metrics/HttpEndpointTaggingConfigTest.groovy create mode 100644 dd-trace-core/src/test/groovy/datadog/trace/common/metrics/HttpEndpointTaggingIntegrationTest.groovy create mode 100644 dd-trace-core/src/test/groovy/datadog/trace/common/metrics/HttpEndpointTaggingTest.groovy create mode 100644 dd-trace-core/src/test/java/datadog/trace/common/metrics/MetricKeyTest.java diff --git a/dd-smoke-tests/springboot/src/test/groovy/datadog/smoketest/HttpEndpointTaggingSmokeTest.groovy b/dd-smoke-tests/springboot/src/test/groovy/datadog/smoketest/HttpEndpointTaggingSmokeTest.groovy new file mode 100644 index 00000000000..0654293e277 --- /dev/null +++ b/dd-smoke-tests/springboot/src/test/groovy/datadog/smoketest/HttpEndpointTaggingSmokeTest.groovy @@ -0,0 +1,252 @@ +package datadog.smoketest + +import okhttp3.Request +import java.util.concurrent.atomic.AtomicInteger +import java.util.regex.Pattern + +class HttpEndpointTaggingSmokeTest extends AbstractServerSmokeTest { + + @Override + ProcessBuilder createProcessBuilder() { + String springBootShadowJar = System.getProperty("datadog.smoketest.springboot.shadowJar.path") + + List command = new ArrayList<>() + command.add(javaPath()) + command.addAll(defaultJavaProperties) + command.addAll((String[]) [ + "-Ddd.writer.type=MultiWriter:TraceStructureWriter:${output.getAbsolutePath()}:includeResource,DDAgentWriter", + "-Ddd.trace.resource.renaming.enabled=true", + "-jar", + springBootShadowJar, + "--server.port=${httpPort}" + ]) + ProcessBuilder processBuilder = new ProcessBuilder(command) + processBuilder.directory(new File(buildDirectory)) + } + + @Override + File createTemporaryFile() { + return File.createTempFile("http-endpoint-tagging-trace", "out") + } + + @Override + protected Set expectedTraces() { + return [ + Pattern.quote("[servlet.request:GET /greeting[spring.handler:IastWebController.greeting]]") + ] + } + + @Override + protected Set assertTraceCounts(Set expected, Map traceCounts) { + List remaining = expected.collect { Pattern.compile(it) }.toList() + for (def i = remaining.size() - 1; i >= 0; i--) { + for (Map.Entry entry : traceCounts.entrySet()) { + if (entry.getValue() > 0 && remaining.get(i).matcher(entry.getKey()).matches()) { + remaining.remove(i) + break + } + } + } + return remaining.collect { it.pattern() }.toSet() + } + + def "test basic HTTP endpoint tagging"() { + setup: + String url = "http://localhost:${httpPort}/greeting" + def request = new Request.Builder().url(url).get().build() + + when: + def response = client.newCall(request).execute() + + then: + def responseBodyStr = response.body().string() + responseBodyStr != null + responseBodyStr.contains("Sup Dawg") + response.code() == 200 + waitForTraceCount(1) + } + + def "test URL parameterization for numeric IDs"() { + setup: + String url = "http://localhost:${httpPort}/users/123" + def request = new Request.Builder().url(url).get().build() + + when: + def response = client.newCall(request).execute() + + then: + // May return 404 since endpoint doesn't exist, but span should still be created + response.code() in [200, 404] + waitForTraceCount(1) + } + + def "test URL parameterization for hex patterns"() { + setup: + String url = "http://localhost:${httpPort}/session/abc123def456" + def request = new Request.Builder().url(url).get().build() + + when: + def response = client.newCall(request).execute() + + then: + // May return 404 since endpoint doesn't exist, but span should still be created + response.code() in [200, 404] + waitForTraceCount(1) + } + + def "test int_id pattern parameterization"() { + setup: + String url = "http://localhost:${httpPort}/api/versions/12.34.56" + def request = new Request.Builder().url(url).get().build() + + when: + def response = client.newCall(request).execute() + + then: + response.code() in [200, 404] + waitForTraceCount(1) + } + + def "test hex_id pattern parameterization"() { + setup: + String url = "http://localhost:${httpPort}/api/tokens/550e8400-e29b-41d4-a716-446655440000" + def request = new Request.Builder().url(url).get().build() + + when: + def response = client.newCall(request).execute() + + then: + response.code() in [200, 404] + waitForTraceCount(1) + } + + def "test str pattern parameterization for long strings"() { + setup: + String url = "http://localhost:${httpPort}/files/very-long-filename-that-exceeds-twenty-characters.pdf" + def request = new Request.Builder().url(url).get().build() + + when: + def response = client.newCall(request).execute() + + then: + response.code() in [200, 404] + waitForTraceCount(1) + } + + def "test str pattern parameterization for special characters"() { + setup: + String url = "http://localhost:${httpPort}/search/query%20with%20spaces" + def request = new Request.Builder().url(url).get().build() + + when: + def response = client.newCall(request).execute() + + then: + response.code() in [200, 404] + waitForTraceCount(1) + } + + def "test mixed URL patterns with multiple segments"() { + setup: + String url = "http://localhost:${httpPort}/api/users/123/orders/abc456def/items/789" + def request = new Request.Builder().url(url).get().build() + + when: + def response = client.newCall(request).execute() + + then: + response.code() in [200, 404] + waitForTraceCount(1) + } + + def "test URL with query parameters"() { + setup: + String url = "http://localhost:${httpPort}/api/users/123?filter=active&limit=10" + def request = new Request.Builder().url(url).get().build() + + when: + def response = client.newCall(request).execute() + + then: + response.code() in [200, 404] + waitForTraceCount(1) + } + + def "test URL with trailing slash"() { + setup: + String url = "http://localhost:${httpPort}/api/users/456/" + def request = new Request.Builder().url(url).get().build() + + when: + def response = client.newCall(request).execute() + + then: + response.code() in [200, 404] + waitForTraceCount(1) + } + + def "test static paths are preserved"() { + setup: + String url = "http://localhost:${httpPort}/health" + def request = new Request.Builder().url(url).get().build() + + when: + def response = client.newCall(request).execute() + + then: + response.code() in [200, 404] + waitForTraceCount(1) + } + + def "test root path handling"() { + setup: + String url = "http://localhost:${httpPort}/" + def request = new Request.Builder().url(url).get().build() + + when: + def response = client.newCall(request).execute() + + then: + response.code() in [200, 404] + waitForTraceCount(1) + } + + def "test pattern precedence - int pattern wins over int_id"() { + setup: + String url = "http://localhost:${httpPort}/api/items/12345" + def request = new Request.Builder().url(url).get().build() + + when: + def response = client.newCall(request).execute() + + then: + response.code() in [200, 404] + waitForTraceCount(1) + } + + def "test pattern precedence - hex pattern wins over hex_id"() { + setup: + String url = "http://localhost:${httpPort}/api/hashes/abc123def" + def request = new Request.Builder().url(url).get().build() + + when: + def response = client.newCall(request).execute() + + then: + response.code() in [200, 404] + waitForTraceCount(1) + } + + def "test edge case - short segments not parameterized"() { + setup: + String url = "http://localhost:${httpPort}/api/x/y" + def request = new Request.Builder().url(url).get().build() + + when: + def response = client.newCall(request).execute() + + then: + response.code() in [200, 404] + waitForTraceCount(1) + } +} 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 ad94c6bed52..25331f0297c 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 @@ -168,6 +168,12 @@ public final class ConfigDefaults { "datadog.trace.*:org.apache.commons.*:org.mockito.*"; static final boolean DEFAULT_CIVISIBILITY_GIT_UPLOAD_ENABLED = true; static final boolean DEFAULT_CIVISIBILITY_GIT_UNSHALLOW_ENABLED = true; + + // HTTP Endpoint Tagging feature flags + static final boolean DEFAULT_RESOURCE_RENAMING_ENABLED = + false; // Default enablement of resource renaming + static final boolean DEFAULT_RESOURCE_RENAMING_ALWAYS_SIMPLIFIED_ENDPOINT = + false; // Manual disablement of resource renaming static final long DEFAULT_CIVISIBILITY_GIT_COMMAND_TIMEOUT_MILLIS = 30_000; static final long DEFAULT_CIVISIBILITY_BACKEND_API_TIMEOUT_MILLIS = 30_000; static final long DEFAULT_CIVISIBILITY_GIT_UPLOAD_TIMEOUT_MILLIS = 60_000; diff --git a/dd-trace-api/src/main/java/datadog/trace/api/config/TracerConfig.java b/dd-trace-api/src/main/java/datadog/trace/api/config/TracerConfig.java index 0300cef2070..44ec0dd8c05 100644 --- a/dd-trace-api/src/main/java/datadog/trace/api/config/TracerConfig.java +++ b/dd-trace-api/src/main/java/datadog/trace/api/config/TracerConfig.java @@ -66,6 +66,11 @@ public final class TracerConfig { "trace.http.resource.remove-trailing-slash"; public static final String TRACE_HTTP_SERVER_PATH_RESOURCE_NAME_MAPPING = "trace.http.server.path-resource-name-mapping"; + + // HTTP Endpoint Tagging feature flags + public static final String TRACE_RESOURCE_RENAMING_ENABLED = "trace.resource.renaming.enabled"; + public static final String TRACE_RESOURCE_RENAMING_ALWAYS_SIMPLIFIED_ENDPOINT = + "trace.resource.renaming.always-simplified-endpoint"; public static final String TRACE_HTTP_CLIENT_PATH_RESOURCE_NAME_MAPPING = "trace.http.client.path-resource-name-mapping"; // Use TRACE_HTTP_SERVER_ERROR_STATUSES instead diff --git a/dd-trace-core/src/main/java/datadog/trace/common/metrics/ConflatingMetricsAggregator.java b/dd-trace-core/src/main/java/datadog/trace/common/metrics/ConflatingMetricsAggregator.java index d2fd5e12a93..aa95f91987d 100644 --- a/dd-trace-core/src/main/java/datadog/trace/common/metrics/ConflatingMetricsAggregator.java +++ b/dd-trace-core/src/main/java/datadog/trace/common/metrics/ConflatingMetricsAggregator.java @@ -3,6 +3,8 @@ import static datadog.communication.ddagent.DDAgentFeaturesDiscovery.V6_METRICS_ENDPOINT; import static datadog.trace.api.DDTags.BASE_SERVICE; import static datadog.trace.api.Functions.UTF8_ENCODE; +import static datadog.trace.bootstrap.instrumentation.api.Tags.HTTP_ENDPOINT; +import static datadog.trace.bootstrap.instrumentation.api.Tags.HTTP_METHOD; 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; @@ -307,6 +309,8 @@ private boolean spanKindEligible(CoreSpan span) { private boolean publish(CoreSpan span, boolean isTopLevel) { final CharSequence spanKind = span.getTag(SPAN_KIND, ""); + final CharSequence httpMethod = span.getTag(HTTP_METHOD, ""); + final CharSequence httpEndpoint = span.getTag(HTTP_ENDPOINT, ""); MetricKey newKey = new MetricKey( span.getResourceName(), @@ -318,7 +322,9 @@ private boolean publish(CoreSpan span, boolean isTopLevel) { span.getParentId() == 0, SPAN_KINDS.computeIfAbsent( spanKind, UTF8BytesString::create), // save repeated utf8 conversions - getPeerTags(span, spanKind.toString())); + getPeerTags(span, spanKind.toString()), + httpMethod, + httpEndpoint); boolean isNewKey = false; MetricKey key = keys.putIfAbsent(newKey, newKey); if (null == key) { diff --git a/dd-trace-core/src/main/java/datadog/trace/common/metrics/HttpEndpointTagging.java b/dd-trace-core/src/main/java/datadog/trace/common/metrics/HttpEndpointTagging.java new file mode 100644 index 00000000000..3d9a9a74642 --- /dev/null +++ b/dd-trace-core/src/main/java/datadog/trace/common/metrics/HttpEndpointTagging.java @@ -0,0 +1,268 @@ +package datadog.trace.common.metrics; + +import static datadog.trace.bootstrap.instrumentation.api.Tags.HTTP_ENDPOINT; +import static datadog.trace.bootstrap.instrumentation.api.Tags.HTTP_ROUTE; +import static datadog.trace.bootstrap.instrumentation.api.Tags.HTTP_URL; + +import datadog.trace.core.CoreSpan; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Utility class for HTTP endpoint tagging logic. Handles route eligibility checks and URL path + * parameterization for trace metrics. + * + *

This implementation ensures: 1. Only applies to HTTP service entry spans (server spans) 2. + * Limits cardinality through URL path parameterization 3. Uses http.route when available and + * eligible (90% accuracy constraint) 4. Provides failsafe endpoint computation from http.url + */ +public final class HttpEndpointTagging { + + private static final Logger log = LoggerFactory.getLogger(HttpEndpointTagging.class); + + private static final Pattern URL_PATTERN = + Pattern.compile("^(?[a-z]+://(?[^?/]+))?(?/[^?]*)(?(\\?).*)?$"); + + // Applied in order - first match wins + private static final Pattern PARAM_INT_PATTERN = Pattern.compile("[1-9][0-9]+"); + private static final Pattern PARAM_INT_ID_PATTERN = Pattern.compile("(?=.*[0-9].*)[0-9._-]{3,}"); + private static final Pattern PARAM_HEX_PATTERN = Pattern.compile("(?=.*[0-9].*)[A-Fa-f0-9]{6,}"); + private static final Pattern PARAM_HEX_ID_PATTERN = + Pattern.compile("(?=.*[0-9].*)[A-Fa-f0-9._-]{6,}"); + private static final Pattern PARAM_STR_PATTERN = Pattern.compile(".{20,}|.*[%&'()*+,:=@].*"); + + private static final int MAX_PATH_ELEMENTS = 8; + + private HttpEndpointTagging() { + // Utility class - no instantiation + } + + /** + * Determines if an HTTP route is eligible for use as endpoint tag. Routes must meet accuracy + * requirements (90% constraint) to be considered eligible. + * + * @param route the HTTP route to check + * @return true if route is eligible, false otherwise + */ + public static boolean isRouteEligible(String route) { + if (route == null || route.trim().isEmpty()) { + return false; + } + + route = route.trim(); + + // Route must start with / to be a valid path + if (!route.startsWith("/")) { + return false; + } + + // Reject overly generic routes that don't provide meaningful endpoint information + if ("/".equals(route) || "/*".equals(route) || "*".equals(route)) { + return false; + } + + // Reject routes that are just wildcards + if (route.matches("^[*/]+$")) { + return false; + } + + // Route is eligible for endpoint tagging + return true; + } + + /** + * Parameterizes a URL path by replacing dynamic segments with {param:type} tokens. Splits path on + * '/', discards empty elements, keeps first 8 elements, and applies regex patterns in order. + * + * @param path the URL path to parameterize + * @return parameterized path with dynamic segments replaced by {param:type} tokens + */ + public static String parameterizeUrlPath(String path) { + if (path == null) { + return null; + } + + if (path.isEmpty() || "/".equals(path)) { + return path; + } + + int queryIndex = path.indexOf('?'); + if (queryIndex != -1) { + path = path.substring(0, queryIndex); + } + + int fragmentIndex = path.indexOf('#'); + if (fragmentIndex != -1) { + path = path.substring(0, fragmentIndex); + } + + String[] allSegments = path.split("/"); + List nonEmptySegments = new ArrayList<>(); + + for (String segment : allSegments) { + if (!segment.isEmpty()) { + nonEmptySegments.add(segment); + } + } + + List segments = + nonEmptySegments.size() > MAX_PATH_ELEMENTS + ? nonEmptySegments.subList(0, MAX_PATH_ELEMENTS) + : nonEmptySegments; + + StringBuilder result = new StringBuilder(); + for (String segment : segments) { + result.append("/"); + + // First match wins + if (PARAM_INT_PATTERN.matcher(segment).matches()) { + result.append("{param:int}"); + } else if (PARAM_INT_ID_PATTERN.matcher(segment).matches()) { + result.append("{param:int_id}"); + } else if (PARAM_HEX_PATTERN.matcher(segment).matches()) { + result.append("{param:hex}"); + } else if (PARAM_HEX_ID_PATTERN.matcher(segment).matches()) { + result.append("{param:hex_id}"); + } else if (PARAM_STR_PATTERN.matcher(segment).matches()) { + result.append("{param:str}"); + } else { + result.append(segment); + } + } + + String parameterized = result.toString(); + return parameterized.isEmpty() ? "/" : parameterized; + } + + /** + * Computes endpoint from HTTP URL using regex parsing. Returns '/' when URL is unavailable or + * invalid. + * + * @param url the HTTP URL to process + * @return parameterized endpoint path or '/' + */ + public static String computeEndpointFromUrl(String url) { + if (url == null || url.trim().isEmpty()) { + return "/"; + } + + java.util.regex.Matcher matcher = URL_PATTERN.matcher(url.trim()); + if (!matcher.matches()) { + log.debug("Failed to parse URL for endpoint computation: {}", url); + return "/"; + } + + String path = matcher.group("path"); + if (path == null || path.isEmpty()) { + return "/"; + } + + return parameterizeUrlPath(path); + } + + /** + * Sets the HTTP endpoint tag on a span if conditions are met. Only applies to HTTP service entry + * spans when: 1. http.route is missing, empty, or not eligible 2. http.url is available for + * endpoint computation + * + *

This method is designed for testing and backward compatibility. Production usage should + * integrate with feature flags and span kind checks. + * + * @param span The span to potentially tag + */ + public static void setEndpointTag(CoreSpan span) { + Object route = span.getTag(HTTP_ROUTE); + + // If route exists and is eligible, don't set endpoint tag + if (route != null && isRouteEligible(route.toString())) { + return; + } + + // Try to compute endpoint from URL + Object url = span.getTag(HTTP_URL); + if (url != null) { + String endpoint = computeEndpointFromUrl(url.toString()); + if (endpoint != null) { + span.setTag(HTTP_ENDPOINT, endpoint); + } + } + } + + /** + * Sets the HTTP endpoint tag on a span context based on configuration flags. This overload + * accepts DDSpanContext for use in TagInterceptor and other core components. + * + * @param spanContext The span context to potentially tag + * @param config The tracer configuration containing feature flags + */ + public static void setEndpointTag( + datadog.trace.core.DDSpanContext spanContext, datadog.trace.api.Config config) { + if (!config.isResourceRenamingEnabled()) { + return; + } + + Object route = spanContext.unsafeGetTag(HTTP_ROUTE); + boolean shouldUseRoute = false; + + // Check if we should use route (when not forcing simplified endpoints) + if (!config.isResourceRenamingAlwaysSimplifiedEndpoint() + && route != null + && isRouteEligible(route.toString())) { + shouldUseRoute = true; + } + + // If we should use route and not set endpoint tag, return early + if (shouldUseRoute) { + return; + } + + // Try to compute endpoint from URL + Object url = spanContext.unsafeGetTag(HTTP_URL); + if (url != null) { + String endpoint = computeEndpointFromUrl(url.toString()); + if (endpoint != null) { + spanContext.setTag(HTTP_ENDPOINT, endpoint); + } + } + } + + /** + * Sets the HTTP endpoint tag on a span based on configuration flags. This is the production + * method that respects feature flags. + * + * @param span The span to potentially tag + * @param config The tracer configuration containing feature flags + */ + public static void setEndpointTag(CoreSpan span, datadog.trace.api.Config config) { + if (!config.isResourceRenamingEnabled()) { + return; + } + + Object route = span.getTag(HTTP_ROUTE); + boolean shouldUseRoute = false; + + // Check if we should use route (when not forcing simplified endpoints) + if (!config.isResourceRenamingAlwaysSimplifiedEndpoint() + && route != null + && isRouteEligible(route.toString())) { + shouldUseRoute = true; + } + + // If we should use route and not set endpoint tag, return early + if (shouldUseRoute) { + return; + } + + // Try to compute endpoint from URL + Object url = span.getTag(HTTP_URL); + if (url != null) { + String endpoint = computeEndpointFromUrl(url.toString()); + if (endpoint != null) { + span.setTag(HTTP_ENDPOINT, endpoint); + } + } + } +} diff --git a/dd-trace-core/src/main/java/datadog/trace/common/metrics/MetricKey.java b/dd-trace-core/src/main/java/datadog/trace/common/metrics/MetricKey.java index 73aca7d6daf..bd786b46a8b 100644 --- a/dd-trace-core/src/main/java/datadog/trace/common/metrics/MetricKey.java +++ b/dd-trace-core/src/main/java/datadog/trace/common/metrics/MetricKey.java @@ -18,6 +18,8 @@ public final class MetricKey { private final boolean isTraceRoot; private final UTF8BytesString spanKind; private final List peerTags; + private final UTF8BytesString httpMethod; + private final UTF8BytesString httpEndpoint; public MetricKey( CharSequence resource, @@ -28,7 +30,9 @@ public MetricKey( boolean synthetics, boolean isTraceRoot, CharSequence spanKind, - List peerTags) { + List peerTags, + CharSequence httpMethod, + CharSequence httpEndpoint) { this.resource = null == resource ? EMPTY : UTF8BytesString.create(resource); this.service = null == service ? EMPTY : UTF8BytesString.create(service); this.operationName = null == operationName ? EMPTY : UTF8BytesString.create(operationName); @@ -38,6 +42,8 @@ public MetricKey( this.isTraceRoot = isTraceRoot; this.spanKind = null == spanKind ? EMPTY : UTF8BytesString.create(spanKind); this.peerTags = peerTags == null ? Collections.emptyList() : peerTags; + this.httpMethod = null == httpMethod ? EMPTY : UTF8BytesString.create(httpMethod); + this.httpEndpoint = null == httpEndpoint ? EMPTY : UTF8BytesString.create(httpEndpoint); // Unrolled polynomial hashcode to avoid varargs allocation // and eliminate data dependency between iterations as in Arrays.hashCode. @@ -55,6 +61,8 @@ public MetricKey( + 29791 * this.operationName.hashCode() + 961 * this.type.hashCode() + 31 * httpStatusCode + + 29 * this.httpMethod.hashCode() + + 27 * this.httpEndpoint.hashCode() + (this.synthetics ? 1 : 0); } @@ -94,6 +102,14 @@ public List getPeerTags() { return peerTags; } + public UTF8BytesString getHttpMethod() { + return httpMethod; + } + + public UTF8BytesString getHttpEndpoint() { + return httpEndpoint; + } + @Override public boolean equals(Object o) { if (this == o) { @@ -110,7 +126,9 @@ public boolean equals(Object o) { && type.equals(metricKey.type) && isTraceRoot == metricKey.isTraceRoot && spanKind.equals(metricKey.spanKind) - && peerTags.equals(metricKey.peerTags); + && peerTags.equals(metricKey.peerTags) + && httpMethod.equals(metricKey.httpMethod) + && httpEndpoint.equals(metricKey.httpEndpoint); } return false; } diff --git a/dd-trace-core/src/main/java/datadog/trace/core/DDSpan.java b/dd-trace-core/src/main/java/datadog/trace/core/DDSpan.java index 05a1ec50eb8..f87ca5627bb 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/DDSpan.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/DDSpan.java @@ -149,6 +149,12 @@ private void finishAndAddToTrace(final long durationNano) { if (DURATION_NANO_UPDATER.compareAndSet(this, 0, Math.max(1, durationNano))) { setLongRunningVersion(-this.longRunningVersion); this.metrics.onSpanFinished(); + + // Apply HTTP endpoint tagging for HTTP server spans before publishing + // This ensures endpoint tagging is applied when all tags are available + datadog.trace.common.metrics.HttpEndpointTagging.setEndpointTag( + context, datadog.trace.api.Config.get()); + TraceCollector.PublishState publishState = context.getTraceCollector().onPublish(this); log.debug("Finished span ({}): {}", publishState, this); } else { diff --git a/dd-trace-core/src/test/groovy/datadog/trace/common/metrics/HttpEndpointTaggingConfigTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/common/metrics/HttpEndpointTaggingConfigTest.groovy new file mode 100644 index 00000000000..7122a938433 --- /dev/null +++ b/dd-trace-core/src/test/groovy/datadog/trace/common/metrics/HttpEndpointTaggingConfigTest.groovy @@ -0,0 +1,49 @@ +package datadog.trace.common.metrics + +import datadog.trace.api.Config +import datadog.trace.test.util.DDSpecification + +class HttpEndpointTaggingConfigTest extends DDSpecification { + + def "should be disabled by default"() { + expect: + !Config.get().isResourceRenamingEnabled() + } + + def "should be enabled with DD_TRACE_RESOURCE_RENAMING_ENABLED=true"() { + setup: + injectSysConfig("dd.trace.resource.renaming.enabled", "true") + + expect: + Config.get().isResourceRenamingEnabled() + } + + def "should be disabled with DD_TRACE_RESOURCE_RENAMING_ENABLED=false"() { + setup: + injectSysConfig("dd.trace.resource.renaming.enabled", "false") + + expect: + !Config.get().isResourceRenamingEnabled() + } + + def "should support simplified endpoint override with DD_TRACE_RESOURCE_RENAMING_ALWAYS_SIMPLIFIED_ENDPOINT=true"() { + setup: + injectSysConfig("dd.trace.resource.renaming.always-simplified-endpoint", "true") + + expect: + Config.get().isResourceRenamingAlwaysSimplifiedEndpoint() + } + + def "should support simplified endpoint override with DD_TRACE_RESOURCE_RENAMING_ALWAYS_SIMPLIFIED_ENDPOINT=1"() { + setup: + injectSysConfig("dd.trace.resource.renaming.always-simplified-endpoint", "1") + + expect: + Config.get().isResourceRenamingAlwaysSimplifiedEndpoint() + } + + def "should not enable simplified endpoint override by default"() { + expect: + !Config.get().isResourceRenamingAlwaysSimplifiedEndpoint() + } +} diff --git a/dd-trace-core/src/test/groovy/datadog/trace/common/metrics/HttpEndpointTaggingIntegrationTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/common/metrics/HttpEndpointTaggingIntegrationTest.groovy new file mode 100644 index 00000000000..d56ae1c3521 --- /dev/null +++ b/dd-trace-core/src/test/groovy/datadog/trace/common/metrics/HttpEndpointTaggingIntegrationTest.groovy @@ -0,0 +1,90 @@ +package datadog.trace.common.metrics + +import datadog.trace.test.util.DDSpecification + +class HttpEndpointTaggingIntegrationTest extends DDSpecification { + + def "should test http.route vs http.endpoint priority logic"() { + expect: "route eligibility determines priority" + HttpEndpointTagging.isRouteEligible("/api/users/{id}") + !HttpEndpointTagging.isRouteEligible("*") + !HttpEndpointTagging.isRouteEligible("/*") + !HttpEndpointTagging.isRouteEligible("/") + } + + def "should test endpoint computation from URLs for stats bucket enrichment"() { + expect: "URLs are parameterized for cardinality control" + HttpEndpointTagging.computeEndpointFromUrl("http://api.com/users/123") == "/users/{param:int}" + HttpEndpointTagging.computeEndpointFromUrl("http://api.com/sessions/abc123def") == "/sessions/{param:hex}" + HttpEndpointTagging.computeEndpointFromUrl("http://api.com/tokens/550e8400-e29b-41d4-a716-446655440000") == "/tokens/{param:hex_id}" + HttpEndpointTagging.computeEndpointFromUrl("http://api.com/files/very-long-filename-that-exceeds-twenty-characters.pdf") == "/files/{param:str}" + } + + def "should test aggregation key enhancement with HTTP tags"() { + setup: + def key1 = new MetricKey( + "GET /users/{param:int}", + "web-service", + "servlet.request", + "web", + 200, + false, + true, + "server", + [], + "GET", + "/users/{param:int}" + ) + + def key2 = new MetricKey( + "GET /users/{param:int}", + "web-service", + "servlet.request", + "web", + 200, + false, + true, + "server", + [], + "GET", + "/users/{param:int}" + ) + + def key3 = new MetricKey( + "GET /users/{param:int}", + "web-service", + "servlet.request", + "web", + 200, + false, + true, + "server", + [], + "POST", // Different method + "/users/{param:int}" + ) + + expect: "HTTP method and endpoint are part of aggregation key" + key1.equals(key2) + !key1.equals(key3) // Different HTTP method should create different key + key1.hashCode() == key2.hashCode() + key1.hashCode() != key3.hashCode() + } + + def "should test backend reliability scenario"() { + expect: "eligible routes should be preferred over URL parameterization" + HttpEndpointTagging.isRouteEligible("/api/v1/users/{userId}/orders/{orderId}") + HttpEndpointTagging.isRouteEligible("/health/check") + HttpEndpointTagging.isRouteEligible("/api/*") // This is eligible because it contains non-wildcard content + !HttpEndpointTagging.isRouteEligible("*") + !HttpEndpointTagging.isRouteEligible("/*") // This should be ineligible + !HttpEndpointTagging.isRouteEligible("/") // This should be ineligible + } + + def "should provide cardinality control for service entry spans"() { + expect: "service entry spans get parameterized endpoints for identification" + HttpEndpointTagging.computeEndpointFromUrl("http://api.service.com/health/check") == "/health/check" + HttpEndpointTagging.computeEndpointFromUrl("http://api.service.com/metrics") == "/metrics" + HttpEndpointTagging.computeEndpointFromUrl("http://api.service.com/api/users/123/profile") == "/api/users/{param:int}/profile" + } +} diff --git a/dd-trace-core/src/test/groovy/datadog/trace/common/metrics/HttpEndpointTaggingTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/common/metrics/HttpEndpointTaggingTest.groovy new file mode 100644 index 00000000000..9666324834b --- /dev/null +++ b/dd-trace-core/src/test/groovy/datadog/trace/common/metrics/HttpEndpointTaggingTest.groovy @@ -0,0 +1,214 @@ +package datadog.trace.common.metrics + +import datadog.trace.test.util.DDSpecification + +class HttpEndpointTaggingTest extends DDSpecification { + + def "should accept valid routes"() { + expect: + HttpEndpointTagging.isRouteEligible("/api/users/{id}") + HttpEndpointTagging.isRouteEligible("/v1/orders/{orderId}/items") + HttpEndpointTagging.isRouteEligible("/users") + HttpEndpointTagging.isRouteEligible("/health") + HttpEndpointTagging.isRouteEligible("/api/v2/products/{productId}") + } + + def "should reject invalid routes"() { + expect: + !HttpEndpointTagging.isRouteEligible(null) + !HttpEndpointTagging.isRouteEligible("") + !HttpEndpointTagging.isRouteEligible(" ") + !HttpEndpointTagging.isRouteEligible("not-a-path") + !HttpEndpointTagging.isRouteEligible("ftp://example.com") + !HttpEndpointTagging.isRouteEligible("relative/path") + } + + def "should parameterize UUID segments with hex_id token"() { + expect: + HttpEndpointTagging.parameterizeUrlPath("/api/users/123e4567-e89b-12d3-a456-426614174000") == "/api/users/{param:hex_id}" + HttpEndpointTagging.parameterizeUrlPath("/api/users/550e8400-e29b-41d4-a716-446655440000/orders") == "/api/users/{param:hex_id}/orders" + HttpEndpointTagging.parameterizeUrlPath("/api/users/f47ac10b-58cc-4372-a567-0e02b2c3d479/profile") == "/api/users/{param:hex_id}/profile" + } + + def "should parameterize numeric segments with int token"() { + expect: + HttpEndpointTagging.parameterizeUrlPath("/api/users/12345") == "/api/users/{param:int}" + HttpEndpointTagging.parameterizeUrlPath("/api/orders/67890/items/123") == "/api/orders/{param:int}/items/{param:int}" + HttpEndpointTagging.parameterizeUrlPath("/api/products/999") == "/api/products/{param:int}" + } + + def "should parameterize hex segments with hex token"() { + expect: + HttpEndpointTagging.parameterizeUrlPath("/api/sessions/abc123def456") == "/api/sessions/{param:hex}" + HttpEndpointTagging.parameterizeUrlPath("/api/tokens/deadbeef") == "/api/tokens/deadbeef" // No digits, so no match + HttpEndpointTagging.parameterizeUrlPath("/api/hashes/1a2b3c4d5e6f") == "/api/hashes/{param:hex}" + } + + def "should parameterize int_id segments with int_id token"() { + expect: + // Pattern: (?=.*[0-9].*)[0-9._-]{3,} → {param:int_id} + // This pattern matches strings that: + // 1. Have at least one digit ((?=.*[0-9].*) lookahead) + // 2. Contain only digits, dots, underscores, or dashes ([0-9._-]) + // 3. Are at least 3 characters long ({3,}) + + // Test cases that should match int_id pattern + HttpEndpointTagging.parameterizeUrlPath("/api/orders/0.99") == "/api/orders/{param:int_id}" // Starts with 0, has dot, 3+ chars + HttpEndpointTagging.parameterizeUrlPath("/api/versions/0123") == "/api/versions/{param:int_id}" // Starts with 0, 3+ chars + HttpEndpointTagging.parameterizeUrlPath("/api/refs/12.34") == "/api/refs/{param:int_id}" // Has dot, 3+ chars + HttpEndpointTagging.parameterizeUrlPath("/api/items/1.2.3") == "/api/items/{param:int_id}" // Has dots, won't match int pattern first + HttpEndpointTagging.parameterizeUrlPath("/api/ids/123-456") == "/api/ids/{param:int_id}" // Has dash, 3+ chars + HttpEndpointTagging.parameterizeUrlPath("/api/codes/9_8_7") == "/api/codes/{param:int_id}" // Has underscores, 3+ chars + + // Test cases that should NOT match int_id pattern + HttpEndpointTagging.parameterizeUrlPath("/api/users/abc") == "/api/users/abc" // No digits + HttpEndpointTagging.parameterizeUrlPath("/api/users/10") == "/api/users/{param:int}" // Matches int pattern first (starts with 1, has digits) + HttpEndpointTagging.parameterizeUrlPath("/api/mixed/12a") == "/api/mixed/12a" // Contains letter 'a', not allowed in int_id + + // Test edge cases for int_id pattern + HttpEndpointTagging.parameterizeUrlPath("/api/test/01") == "/api/test/01" // Starts with 0, only 2 chars, too short for int_id + HttpEndpointTagging.parameterizeUrlPath("/api/test/012") == "/api/test/{param:int_id}" // Starts with 0, 3+ chars, matches int_id + } + + + def "should parameterize hex_id segments with hex_id token"() { + expect: + // Pattern: (?=.*[0-9].*)[A-Fa-f0-9._-]{6,} → {param:hex_id} + // This pattern matches strings that: + // 1. Have at least one digit ((?=.*[0-9].*) lookahead) + // 2. Contain only hex digits, dots, underscores, or dashes ([A-Fa-f0-9._-]) + // 3. Are at least 6 characters long ({6,}) + + // Test cases that should match hex_id pattern + HttpEndpointTagging.parameterizeUrlPath("/api/sessions/abc123-def456") == "/api/sessions/{param:hex_id}" // Has digits, hex chars, delimiters, 6+ chars + HttpEndpointTagging.parameterizeUrlPath("/api/tokens/deadbeef.123") == "/api/tokens/{param:hex_id}" // Has digits, hex chars, delimiters, 6+ chars + HttpEndpointTagging.parameterizeUrlPath("/api/hashes/1a2b3c_4d5e6f") == "/api/hashes/{param:hex_id}" // Has digits, hex chars, delimiters, 6+ chars + HttpEndpointTagging.parameterizeUrlPath("/api/uuids/550e8400-e29b-41d4-a716-446655440000") == "/api/uuids/{param:hex_id}" // UUID format + HttpEndpointTagging.parameterizeUrlPath("/api/keys/abc123def") == "/api/keys/{param:hex}" // Matches hex pattern first (pure hex chars), not hex_id + + // Test cases that should NOT match hex_id pattern + HttpEndpointTagging.parameterizeUrlPath("/api/pure/abcdef") == "/api/pure/abcdef" // No digits, so no match + HttpEndpointTagging.parameterizeUrlPath("/api/short/a1b2c") == "/api/short/a1b2c" // Only 5 chars, less than required 6 + HttpEndpointTagging.parameterizeUrlPath("/api/invalid/123xyz") == "/api/invalid/123xyz" // Contains 'x', 'y', 'z' which are not in [A-Fa-f0-9._-] + + // Test cases that might match other patterns first + HttpEndpointTagging.parameterizeUrlPath("/api/numbers/123456") == "/api/numbers/{param:int}" // Matches int pattern first (starts with 1, all digits) + HttpEndpointTagging.parameterizeUrlPath("/api/tokens/deadbeef") == "/api/tokens/deadbeef" + // Should not match if less than 6 characters + HttpEndpointTagging.parameterizeUrlPath("/api/tokens/abc12") == "/api/tokens/abc12" + } + + + def "should parameterize mixed segments correctly"() { + expect: + HttpEndpointTagging.parameterizeUrlPath("/api/users/123/orders/abc456def/items/789") == "/api/users/{param:int}/orders/{param:hex}/items/{param:int}" + HttpEndpointTagging.parameterizeUrlPath("/api/v1/users/550e8400-e29b-41d4-a716-446655440000/sessions/deadbeef") == "/api/v1/users/{param:hex_id}/sessions/deadbeef" // deadbeef has no digits + } + + def "should preserve static segments"() { + expect: + HttpEndpointTagging.parameterizeUrlPath("/api/users") == "/api/users" + HttpEndpointTagging.parameterizeUrlPath("/health") == "/health" + } + + def "should reject ineligible routes according to specification"() { + expect: + !HttpEndpointTagging.isRouteEligible(null) + !HttpEndpointTagging.isRouteEligible("") + !HttpEndpointTagging.isRouteEligible(" ") + !HttpEndpointTagging.isRouteEligible("*") + !HttpEndpointTagging.isRouteEligible("/*") + !HttpEndpointTagging.isRouteEligible("/") + !HttpEndpointTagging.isRouteEligible("**") + !HttpEndpointTagging.isRouteEligible("*/") + !HttpEndpointTagging.isRouteEligible("no-leading-slash") + + HttpEndpointTagging.isRouteEligible("/api/users") + HttpEndpointTagging.isRouteEligible("/health") + HttpEndpointTagging.isRouteEligible("/api/v1/users/{id}") + } + + def "should handle edge cases"() { + expect: + HttpEndpointTagging.parameterizeUrlPath("/") == "/" + HttpEndpointTagging.parameterizeUrlPath("") == "" + HttpEndpointTagging.parameterizeUrlPath(null) == null + HttpEndpointTagging.parameterizeUrlPath("/api/users/123/") == "/api/users/{param:int}" + } + + def "should handle query parameters"() { + expect: + HttpEndpointTagging.parameterizeUrlPath("/api/users/123?param=value") == "/api/users/{param:int}" + HttpEndpointTagging.parameterizeUrlPath("/api/search?q=test&limit=10") == "/api/search" + HttpEndpointTagging.parameterizeUrlPath("/api/users/abc123?filter=active") == "/api/users/{param:hex}" + } + + def "should handle fragments"() { + expect: + HttpEndpointTagging.parameterizeUrlPath("/api/users/123#section") == "/api/users/{param:int}" + HttpEndpointTagging.parameterizeUrlPath("/api/docs#introduction") == "/api/docs" + } + + def "should compute endpoint from valid URLs"() { + expect: + HttpEndpointTagging.computeEndpointFromUrl("http://example.com/api/users/123") == "/api/users/{param:int}" + HttpEndpointTagging.computeEndpointFromUrl("https://api.example.com/api/orders/456") == "/api/orders/{param:int}" + HttpEndpointTagging.computeEndpointFromUrl("http://localhost:8080/health") == "/health" + HttpEndpointTagging.computeEndpointFromUrl("https://example.com:443/api/v1/users/789?param=value") == "/api/v1/users/{param:int}" + } + + def "should return default endpoint for invalid URLs"() { + expect: + HttpEndpointTagging.computeEndpointFromUrl(null) == "/" + HttpEndpointTagging.computeEndpointFromUrl("") == "/" + HttpEndpointTagging.computeEndpointFromUrl(" ") == "/" + HttpEndpointTagging.computeEndpointFromUrl("not-a-url") == "/" + HttpEndpointTagging.computeEndpointFromUrl("http://") == "/" + HttpEndpointTagging.computeEndpointFromUrl("://example.com/path") == "/" + HttpEndpointTagging.computeEndpointFromUrl("http:///path") == "/" + HttpEndpointTagging.computeEndpointFromUrl("ftp://example.com/file.txt") == "/file.txt" // FTP URL matches pattern and extracts path + } + + def "should return root path for root URLs"() { + expect: + HttpEndpointTagging.computeEndpointFromUrl("http://example.com/") == "/" + HttpEndpointTagging.computeEndpointFromUrl("https://example.com") == "/" + } + + def "should handle complex paths"() { + expect: + HttpEndpointTagging.parameterizeUrlPath("/api/v2/users/123/orders/abc456/items/789/reviews/def123") == "/api/v2/users/{param:int}/orders/{param:hex}/items/{param:int}" // Limited to 8 elements: api,v2,users,123,orders,abc456,items,789 + HttpEndpointTagging.parameterizeUrlPath("/files/2023/12/document123.pdf") == "/files/{param:int}/{param:int}/document123.pdf" // document123.pdf is <20 chars and no special chars + HttpEndpointTagging.parameterizeUrlPath("/api/sessions/550e8400-e29b-41d4-a716-446655440000/refresh") == "/api/sessions/{param:hex_id}/refresh" + } + + def "should preserve static segments in complex paths"() { + expect: + HttpEndpointTagging.parameterizeUrlPath("/api/v1/users/123/profile/settings") == "/api/v1/users/{param:int}/profile/settings" + HttpEndpointTagging.parameterizeUrlPath("/admin/dashboard/users/456/edit") == "/admin/dashboard/users/{param:int}/edit" + HttpEndpointTagging.parameterizeUrlPath("/public/assets/images/user-789/avatar.png") == "/public/assets/images/user-789/avatar.png" // user-789 doesn't match int_id pattern + } + + def "should limit cardinality through parameterization"() { + expect: + HttpEndpointTagging.parameterizeUrlPath("/api/users/1") == "/api/users/1" // Single digit not matched by [1-9][0-9]+ + HttpEndpointTagging.parameterizeUrlPath("/api/users/99") == "/api/users/{param:int}" + HttpEndpointTagging.parameterizeUrlPath("/api/users/123456789") == "/api/users/{param:int}" + + // Test that hex strings with digits are parameterized to limit cardinality + HttpEndpointTagging.parameterizeUrlPath("/api/tokens/abcdef") == "/api/tokens/abcdef" // No digits, no match + HttpEndpointTagging.parameterizeUrlPath("/api/tokens/123abc") == "/api/tokens/{param:hex}" + HttpEndpointTagging.parameterizeUrlPath("/api/tokens/deadbeef123") == "/api/tokens/{param:hex}" + } + + def "should handle empty path elements correctly"() { + expect: + // Test paths with double slashes (empty elements) + HttpEndpointTagging.parameterizeUrlPath("/api//users/123") == "/api/users/{param:int}" // Empty element discarded + HttpEndpointTagging.parameterizeUrlPath("//api/users/123//") == "/api/users/{param:int}" // Multiple empty elements discarded + HttpEndpointTagging.parameterizeUrlPath("/api/v1//users//123//orders//456") == "/api/v1/users/{param:int}/orders/{param:int}" // All empty elements discarded + + // Test 8-element limit with empty elements mixed in + HttpEndpointTagging.parameterizeUrlPath("/a//b//c//d//e//f//g//h//i//j") == "/a/b/c/d/e/f/g/h" // Only first 8 non-empty elements kept + } +} diff --git a/dd-trace-core/src/test/java/datadog/trace/common/metrics/MetricKeyTest.java b/dd-trace-core/src/test/java/datadog/trace/common/metrics/MetricKeyTest.java new file mode 100644 index 00000000000..bb2be4f865f --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/common/metrics/MetricKeyTest.java @@ -0,0 +1,213 @@ +package datadog.trace.common.metrics; + +import static org.junit.Assert.*; + +import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; +import java.util.Collections; +import org.junit.Test; + +public class MetricKeyTest { + + @Test + public void testMetricKeyWithHttpMethodAndEndpoint() { + MetricKey key = + new MetricKey( + "GET /api/users/?", + "my-service", + "http.request", + "web", + 200, + false, + true, + "server", + Collections.emptyList(), + "GET", + "/api/users/?"); + + assertEquals(UTF8BytesString.create("GET /api/users/?"), key.getResource()); + assertEquals(UTF8BytesString.create("my-service"), key.getService()); + assertEquals(UTF8BytesString.create("http.request"), key.getOperationName()); + assertEquals(UTF8BytesString.create("web"), key.getType()); + assertEquals(200, key.getHttpStatusCode()); + assertFalse(key.isSynthetics()); + assertTrue(key.isTraceRoot()); + assertEquals(UTF8BytesString.create("server"), key.getSpanKind()); + assertEquals(Collections.emptyList(), key.getPeerTags()); + assertEquals(UTF8BytesString.create("GET"), key.getHttpMethod()); + assertEquals(UTF8BytesString.create("/api/users/?"), key.getHttpEndpoint()); + } + + @Test + public void testMetricKeyWithNullHttpMethodAndEndpoint() { + MetricKey key = + new MetricKey( + "resource", + "service", + "operation", + "type", + 0, + false, + false, + "client", + Collections.emptyList(), + null, + null); + + assertEquals(UTF8BytesString.EMPTY, key.getHttpMethod()); + assertEquals(UTF8BytesString.EMPTY, key.getHttpEndpoint()); + } + + @Test + public void testMetricKeyEqualsWithHttpMethodAndEndpoint() { + MetricKey key1 = + new MetricKey( + "resource", + "service", + "operation", + "type", + 200, + false, + true, + "server", + Collections.emptyList(), + "POST", + "/api/orders/?"); + + MetricKey key2 = + new MetricKey( + "resource", + "service", + "operation", + "type", + 200, + false, + true, + "server", + Collections.emptyList(), + "POST", + "/api/orders/?"); + + assertEquals(key1, key2); + assertEquals(key1.hashCode(), key2.hashCode()); + } + + @Test + public void testMetricKeyNotEqualsWithDifferentHttpMethod() { + MetricKey key1 = + new MetricKey( + "resource", + "service", + "operation", + "type", + 200, + false, + true, + "server", + Collections.emptyList(), + "GET", + "/api/users/?"); + + MetricKey key2 = + new MetricKey( + "resource", + "service", + "operation", + "type", + 200, + false, + true, + "server", + Collections.emptyList(), + "POST", + "/api/users/?"); + + assertNotEquals(key1, key2); + } + + @Test + public void testMetricKeyNotEqualsWithDifferentHttpEndpoint() { + MetricKey key1 = + new MetricKey( + "resource", + "service", + "operation", + "type", + 200, + false, + true, + "server", + Collections.emptyList(), + "GET", + "/api/users/?"); + + MetricKey key2 = + new MetricKey( + "resource", + "service", + "operation", + "type", + 200, + false, + true, + "server", + Collections.emptyList(), + "GET", + "/api/orders/?"); + + assertNotEquals(key1, key2); + } + + @Test + public void testMetricKeyHashCodeIncludesHttpMethodAndEndpoint() { + MetricKey key1 = + new MetricKey( + "resource", + "service", + "operation", + "type", + 200, + false, + true, + "server", + Collections.emptyList(), + "GET", + "/api/users/?"); + + MetricKey key2 = + new MetricKey( + "resource", + "service", + "operation", + "type", + 200, + false, + true, + "server", + Collections.emptyList(), + "POST", + "/api/users/?"); + + // Hash codes should be different when HTTP method differs + assertNotEquals(key1.hashCode(), key2.hashCode()); + } + + @Test + public void testMetricKeyWithEmptyHttpMethodAndEndpoint() { + MetricKey key = + new MetricKey( + "resource", + "service", + "operation", + "type", + 0, + false, + false, + "client", + Collections.emptyList(), + "", + ""); + + assertEquals(UTF8BytesString.create(""), key.getHttpMethod()); + assertEquals(UTF8BytesString.create(""), key.getHttpEndpoint()); + } +} 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 bb4e4f65d12..e1449e097b9 100644 --- a/internal-api/src/main/java/datadog/trace/api/Config.java +++ b/internal-api/src/main/java/datadog/trace/api/Config.java @@ -121,6 +121,8 @@ import static datadog.trace.api.ConfigDefaults.DEFAULT_REMOTE_CONFIG_POLL_INTERVAL_SECONDS; import static datadog.trace.api.ConfigDefaults.DEFAULT_REMOTE_CONFIG_TARGETS_KEY; import static datadog.trace.api.ConfigDefaults.DEFAULT_REMOTE_CONFIG_TARGETS_KEY_ID; +import static datadog.trace.api.ConfigDefaults.DEFAULT_RESOURCE_RENAMING_ALWAYS_SIMPLIFIED_ENDPOINT; +import static datadog.trace.api.ConfigDefaults.DEFAULT_RESOURCE_RENAMING_ENABLED; import static datadog.trace.api.ConfigDefaults.DEFAULT_RUM_MAJOR_VERSION; import static datadog.trace.api.ConfigDefaults.DEFAULT_SCOPE_DEPTH_LIMIT; import static datadog.trace.api.ConfigDefaults.DEFAULT_SCOPE_ITERATION_KEEP_ALIVE; @@ -621,6 +623,8 @@ import static datadog.trace.api.config.TracerConfig.TRACE_REMOVE_INTEGRATION_SERVICE_NAMES_ENABLED; import static datadog.trace.api.config.TracerConfig.TRACE_REPORT_HOSTNAME; import static datadog.trace.api.config.TracerConfig.TRACE_RESOLVER_ENABLED; +import static datadog.trace.api.config.TracerConfig.TRACE_RESOURCE_RENAMING_ALWAYS_SIMPLIFIED_ENDPOINT; +import static datadog.trace.api.config.TracerConfig.TRACE_RESOURCE_RENAMING_ENABLED; import static datadog.trace.api.config.TracerConfig.TRACE_SAMPLE_RATE; import static datadog.trace.api.config.TracerConfig.TRACE_SAMPLING_OPERATION_RULES; import static datadog.trace.api.config.TracerConfig.TRACE_SAMPLING_RULES; @@ -810,6 +814,10 @@ public static String getHostName() { private final Map httpServerPathResourceNameMapping; private final Map httpClientPathResourceNameMapping; private final boolean httpResourceRemoveTrailingSlash; + + // HTTP Endpoint Tagging feature flags + private final boolean resourceRenamingEnabled; + private final boolean resourceRenamingAlwaysSimplifiedEndpoint; private final boolean httpClientTagQueryString; private final boolean httpClientTagHeaders; private final boolean httpClientSplitByDomain; @@ -1541,6 +1549,15 @@ private Config(final ConfigProvider configProvider, final InstrumenterConfig ins TRACE_HTTP_RESOURCE_REMOVE_TRAILING_SLASH, DEFAULT_TRACE_HTTP_RESOURCE_REMOVE_TRAILING_SLASH); + // HTTP Endpoint Tagging feature flags + resourceRenamingEnabled = + configProvider.getBoolean( + TRACE_RESOURCE_RENAMING_ENABLED, DEFAULT_RESOURCE_RENAMING_ENABLED); + resourceRenamingAlwaysSimplifiedEndpoint = + configProvider.getBoolean( + TRACE_RESOURCE_RENAMING_ALWAYS_SIMPLIFIED_ENDPOINT, + DEFAULT_RESOURCE_RENAMING_ALWAYS_SIMPLIFIED_ENDPOINT); + httpServerErrorStatuses = configProvider.getIntegerRange( TRACE_HTTP_SERVER_ERROR_STATUSES, @@ -3064,6 +3081,14 @@ public boolean isHttpServerRouteBasedNaming() { return httpServerRouteBasedNaming; } + public boolean isResourceRenamingEnabled() { + return resourceRenamingEnabled; + } + + public boolean isResourceRenamingAlwaysSimplifiedEndpoint() { + return resourceRenamingAlwaysSimplifiedEndpoint; + } + public boolean isHttpClientTagQueryString() { return httpClientTagQueryString; } diff --git a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/Tags.java b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/Tags.java index 3eaa1e292cc..5f4cd800b44 100644 --- a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/Tags.java +++ b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/Tags.java @@ -19,6 +19,7 @@ public class Tags { public static final String HTTP_ROUTE = "http.route"; public static final String HTTP_STATUS = "http.status_code"; public static final String HTTP_METHOD = "http.method"; + public static final String HTTP_ENDPOINT = "http.endpoint"; public static final String HTTP_FORWARDED = "http.forwarded"; public static final String HTTP_FORWARDED_PROTO = "http.forwarded.proto"; public static final String HTTP_FORWARDED_HOST = "http.forwarded.host"; From feab335e3d116b44bf0da9ad7eacb885605b9f0c Mon Sep 17 00:00:00 2001 From: "sezen.leblay" Date: Wed, 8 Oct 2025 16:42:37 +0200 Subject: [PATCH 2/7] build issue Signed-off-by: sezen.leblay --- .../common/metrics/AggregateMetricTest.groovy | 4 +- .../ConflatingMetricAggregatorTest.groovy | 64 ++++++++++++++----- .../SerializingMetricWriterTest.groovy | 10 ++- 3 files changed, 58 insertions(+), 20 deletions(-) diff --git a/dd-trace-core/src/test/groovy/datadog/trace/common/metrics/AggregateMetricTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/common/metrics/AggregateMetricTest.groovy index 3c7a247cae3..ae09ded402e 100644 --- a/dd-trace-core/src/test/groovy/datadog/trace/common/metrics/AggregateMetricTest.groovy +++ b/dd-trace-core/src/test/groovy/datadog/trace/common/metrics/AggregateMetricTest.groovy @@ -52,7 +52,7 @@ class AggregateMetricTest extends DDSpecification { given: AggregateMetric aggregate = new AggregateMetric().recordDurations(3, new AtomicLongArray(0L, 0L, 0L | ERROR_TAG | TOP_LEVEL_TAG)) - Batch batch = new Batch().reset(new MetricKey("foo", "bar", "qux", "type", 0, false, true, "corge", [UTF8BytesString.create("grault:quux")])) + Batch batch = new Batch().reset(new MetricKey("foo", "bar", "qux", "type", 0, false, true, "corge", [UTF8BytesString.create("grault:quux")], "GET", "/api/endpoint")) batch.add(0L, 10) batch.add(0L, 10) batch.add(0L, 10) @@ -127,7 +127,7 @@ class AggregateMetricTest extends DDSpecification { def "consistent under concurrent attempts to read and write"() { given: AggregateMetric aggregate = new AggregateMetric() - MetricKey key = new MetricKey("foo", "bar", "qux", "type", 0, false, true, "corge", [UTF8BytesString.create("grault:quux")]) + MetricKey key = new MetricKey("foo", "bar", "qux", "type", 0, false, true, "corge", [UTF8BytesString.create("grault:quux")], "GET", "/api/endpoint") BlockingDeque queue = new LinkedBlockingDeque<>(1000) ExecutorService reader = Executors.newSingleThreadExecutor() int writerCount = 10 diff --git a/dd-trace-core/src/test/groovy/datadog/trace/common/metrics/ConflatingMetricAggregatorTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/common/metrics/ConflatingMetricAggregatorTest.groovy index 52c1bb34de1..78dfb741102 100644 --- a/dd-trace-core/src/test/groovy/datadog/trace/common/metrics/ConflatingMetricAggregatorTest.groovy +++ b/dd-trace-core/src/test/groovy/datadog/trace/common/metrics/ConflatingMetricAggregatorTest.groovy @@ -128,7 +128,9 @@ class ConflatingMetricAggregatorTest extends DDSpecification { false, false, "baz", - [] + [], + null, + null ), _) >> { MetricKey key, AggregateMetric value -> value.getHitCount() == 1 && value.getTopLevelCount() == 1 && value.getDuration() == 100 } @@ -170,7 +172,9 @@ class ConflatingMetricAggregatorTest extends DDSpecification { false, false, kind, - [] + [], + null, + null ), { AggregateMetric aggregateMetric -> aggregateMetric.getHitCount() == 1 && aggregateMetric.getTopLevelCount() == 0 && aggregateMetric.getDuration() == 100 }) @@ -224,7 +228,9 @@ class ConflatingMetricAggregatorTest extends DDSpecification { false, false, "client", - [UTF8BytesString.create("country:france")] + [UTF8BytesString.create("country:france")], + null, + null ), { AggregateMetric aggregateMetric -> aggregateMetric.getHitCount() == 1 && aggregateMetric.getTopLevelCount() == 0 && aggregateMetric.getDuration() == 100 }) @@ -238,7 +244,9 @@ class ConflatingMetricAggregatorTest extends DDSpecification { false, false, "client", - [UTF8BytesString.create("country:france"), UTF8BytesString.create("georegion:europe")] + [UTF8BytesString.create("country:france"), UTF8BytesString.create("georegion:europe")], + null, + null ), { AggregateMetric aggregateMetric -> aggregateMetric.getHitCount() == 1 && aggregateMetric.getTopLevelCount() == 0 && aggregateMetric.getDuration() == 100 }) @@ -281,7 +289,9 @@ class ConflatingMetricAggregatorTest extends DDSpecification { false, false, kind, - expectedPeerTags + expectedPeerTags, + null, + null ), { AggregateMetric aggregateMetric -> aggregateMetric.getHitCount() == 1 && aggregateMetric.getTopLevelCount() == 0 && aggregateMetric.getDuration() == 100 }) @@ -329,7 +339,9 @@ class ConflatingMetricAggregatorTest extends DDSpecification { false, false, "baz", - [] + [], + null, + null ), { AggregateMetric value -> value.getHitCount() == 1 && value.getTopLevelCount() == topLevelCount && value.getDuration() == 100 }) @@ -384,7 +396,9 @@ class ConflatingMetricAggregatorTest extends DDSpecification { false, false, "baz", - [] + [], + null, + null ), { AggregateMetric value -> value.getHitCount() == count && value.getDuration() == count * duration }) @@ -397,7 +411,9 @@ class ConflatingMetricAggregatorTest extends DDSpecification { false, false, "baz", - [] + [], + null, + null ), { AggregateMetric value -> value.getHitCount() == count && value.getDuration() == count * duration * 2 }) @@ -446,7 +462,9 @@ class ConflatingMetricAggregatorTest extends DDSpecification { false, false, "baz", - [] + [], + null, + null ), _) >> { MetricKey key, AggregateMetric value -> value.getHitCount() == 1 && value.getDuration() == duration } @@ -460,7 +478,9 @@ class ConflatingMetricAggregatorTest extends DDSpecification { false, false, "baz", - [] + [], + null, + null ), _) 1 * writer.finishBucket() >> { latch.countDown() } @@ -505,7 +525,9 @@ class ConflatingMetricAggregatorTest extends DDSpecification { false, false, "baz", - [] + [], + null, + null ), { AggregateMetric value -> value.getHitCount() == 1 && value.getDuration() == duration }) @@ -536,7 +558,9 @@ class ConflatingMetricAggregatorTest extends DDSpecification { false, false, "baz", - [] + [], + null, + null ), { AggregateMetric value -> value.getHitCount() == 1 && value.getDuration() == duration }) @@ -550,7 +574,9 @@ class ConflatingMetricAggregatorTest extends DDSpecification { false, false, "baz", - [] + [], + null, + null ), _) 1 * writer.finishBucket() >> { latch.countDown() } @@ -595,7 +621,9 @@ class ConflatingMetricAggregatorTest extends DDSpecification { false, false, "quux", - [] + [], + null, + null ), { AggregateMetric value -> value.getHitCount() == 1 && value.getDuration() == duration }) @@ -650,7 +678,9 @@ class ConflatingMetricAggregatorTest extends DDSpecification { false, true, "garply", - [] + [], + null, + null ), { AggregateMetric value -> value.getHitCount() == 1 && value.getDuration() == duration }) @@ -852,7 +882,9 @@ class ConflatingMetricAggregatorTest extends DDSpecification { false, true, "", - [] + [], + null, + null ), { AggregateMetric aggregateMetric -> aggregateMetric.getHitCount() == 1 && aggregateMetric.getTopLevelCount() == 1 && aggregateMetric.getDuration() == 100 }) diff --git a/dd-trace-core/src/test/groovy/datadog/trace/common/metrics/SerializingMetricWriterTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/common/metrics/SerializingMetricWriterTest.groovy index 64874c62ab7..9da581805fa 100644 --- a/dd-trace-core/src/test/groovy/datadog/trace/common/metrics/SerializingMetricWriterTest.groovy +++ b/dd-trace-core/src/test/groovy/datadog/trace/common/metrics/SerializingMetricWriterTest.groovy @@ -57,7 +57,9 @@ class SerializingMetricWriterTest extends DDSpecification { UTF8BytesString.create("country:canada"), UTF8BytesString.create("georegion:amer"), UTF8BytesString.create("peer.service:remote-service") - ] + ], + null, + null ), new AggregateMetric().recordDurations(10, new AtomicLongArray(1L)) ), @@ -76,6 +78,8 @@ class SerializingMetricWriterTest extends DDSpecification { UTF8BytesString.create("georegion:amer"), UTF8BytesString.create("peer.service:remote-service") ], + null, + null ), new AggregateMetric().recordDurations(9, new AtomicLongArray(1L)) ) @@ -91,7 +95,9 @@ class SerializingMetricWriterTest extends DDSpecification { false, false, "producer", - [UTF8BytesString.create("messaging.destination:dest" + i)] + [UTF8BytesString.create("messaging.destination:dest" + i)], + null, + null ), new AggregateMetric().recordDurations(10, new AtomicLongArray(1L)) ) From 3f778ee471f9182f98b9500d6afcf4425477b1ab Mon Sep 17 00:00:00 2001 From: "sezen.leblay" Date: Thu, 9 Oct 2025 11:46:51 +0200 Subject: [PATCH 3/7] comments Signed-off-by: sezen.leblay --- .../trace/common/metrics/MetricKey.java | 4 +- .../main/java/datadog/trace/core/DDSpan.java | 5 -- .../HttpEndpointPostProcessor.java | 30 ++++++++++++ .../tagprocessor}/HttpEndpointTagging.java | 46 ++++++++----------- .../TagsPostProcessorFactory.java | 3 +- .../HttpEndpointTaggingConfigTest.groovy | 2 +- .../HttpEndpointTaggingIntegrationTest.groovy | 3 +- .../HttpEndpointTaggingTest.groovy | 2 +- 8 files changed, 58 insertions(+), 37 deletions(-) create mode 100644 dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/HttpEndpointPostProcessor.java rename dd-trace-core/src/main/java/datadog/trace/{common/metrics => core/tagprocessor}/HttpEndpointTagging.java (89%) rename dd-trace-core/src/test/groovy/datadog/trace/{common/metrics => core/tagprocessor}/HttpEndpointTaggingConfigTest.groovy (97%) rename dd-trace-core/src/test/groovy/datadog/trace/{common/metrics => core/tagprocessor}/HttpEndpointTaggingIntegrationTest.groovy (97%) rename dd-trace-core/src/test/groovy/datadog/trace/{common/metrics => core/tagprocessor}/HttpEndpointTaggingTest.groovy (99%) diff --git a/dd-trace-core/src/main/java/datadog/trace/common/metrics/MetricKey.java b/dd-trace-core/src/main/java/datadog/trace/common/metrics/MetricKey.java index bd786b46a8b..e5fb9bd1652 100644 --- a/dd-trace-core/src/main/java/datadog/trace/common/metrics/MetricKey.java +++ b/dd-trace-core/src/main/java/datadog/trace/common/metrics/MetricKey.java @@ -61,8 +61,8 @@ public MetricKey( + 29791 * this.operationName.hashCode() + 961 * this.type.hashCode() + 31 * httpStatusCode - + 29 * this.httpMethod.hashCode() - + 27 * this.httpEndpoint.hashCode() + + -1796951359 * this.httpMethod.hashCode() + + 129082719 * this.httpEndpoint.hashCode() + (this.synthetics ? 1 : 0); } diff --git a/dd-trace-core/src/main/java/datadog/trace/core/DDSpan.java b/dd-trace-core/src/main/java/datadog/trace/core/DDSpan.java index f87ca5627bb..35610b9674d 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/DDSpan.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/DDSpan.java @@ -150,11 +150,6 @@ private void finishAndAddToTrace(final long durationNano) { setLongRunningVersion(-this.longRunningVersion); this.metrics.onSpanFinished(); - // Apply HTTP endpoint tagging for HTTP server spans before publishing - // This ensures endpoint tagging is applied when all tags are available - datadog.trace.common.metrics.HttpEndpointTagging.setEndpointTag( - context, datadog.trace.api.Config.get()); - TraceCollector.PublishState publishState = context.getTraceCollector().onPublish(this); log.debug("Finished span ({}): {}", publishState, this); } else { 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 new file mode 100644 index 00000000000..61dcaa35516 --- /dev/null +++ b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/HttpEndpointPostProcessor.java @@ -0,0 +1,30 @@ +package datadog.trace.core.tagprocessor; + +import datadog.trace.api.Config; +import datadog.trace.api.TagMap; +import datadog.trace.bootstrap.instrumentation.api.AgentSpanLink; +import datadog.trace.core.DDSpanContext; +import java.util.List; + +/** + * TagsPostProcessor that applies HTTP endpoint tagging logic to spans. This processor computes and + * sets the http.endpoint tag based on http.route and http.url tags when appropriate. + */ +public final class HttpEndpointPostProcessor extends TagsPostProcessor { + private final Config config; + + public HttpEndpointPostProcessor() { + this(Config.get()); + } + + // Visible for testing + HttpEndpointPostProcessor(Config config) { + this.config = config; + } + + @Override + public void processTags( + TagMap unsafeTags, DDSpanContext spanContext, List spanLinks) { + HttpEndpointTagging.setEndpointTag(spanContext, config); + } +} diff --git a/dd-trace-core/src/main/java/datadog/trace/common/metrics/HttpEndpointTagging.java b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/HttpEndpointTagging.java similarity index 89% rename from dd-trace-core/src/main/java/datadog/trace/common/metrics/HttpEndpointTagging.java rename to dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/HttpEndpointTagging.java index 3d9a9a74642..b28d3a136e2 100644 --- a/dd-trace-core/src/main/java/datadog/trace/common/metrics/HttpEndpointTagging.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/HttpEndpointTagging.java @@ -1,19 +1,22 @@ -package datadog.trace.common.metrics; +package datadog.trace.core.tagprocessor; import static datadog.trace.bootstrap.instrumentation.api.Tags.HTTP_ENDPOINT; import static datadog.trace.bootstrap.instrumentation.api.Tags.HTTP_ROUTE; import static datadog.trace.bootstrap.instrumentation.api.Tags.HTTP_URL; +import datadog.trace.api.Config; import datadog.trace.core.CoreSpan; +import datadog.trace.core.DDSpanContext; import java.util.ArrayList; import java.util.List; +import java.util.regex.Matcher; import java.util.regex.Pattern; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Utility class for HTTP endpoint tagging logic. Handles route eligibility checks and URL path - * parameterization for trace metrics. + * parameterization for spans. * *

This implementation ensures: 1. Only applies to HTTP service entry spans (server spans) 2. * Limits cardinality through URL path parameterization 3. Uses http.route when available and @@ -48,12 +51,16 @@ private HttpEndpointTagging() { * @return true if route is eligible, false otherwise */ public static boolean isRouteEligible(String route) { - if (route == null || route.trim().isEmpty()) { + if (route == null) { return false; } route = route.trim(); + if (route.isEmpty()) { + return false; + } + // Route must start with / to be a valid path if (!route.startsWith("/")) { return false; @@ -64,11 +71,6 @@ public static boolean isRouteEligible(String route) { return false; } - // Reject routes that are just wildcards - if (route.matches("^[*/]+$")) { - return false; - } - // Route is eligible for endpoint tagging return true; } @@ -145,11 +147,16 @@ public static String parameterizeUrlPath(String path) { * @return parameterized endpoint path or '/' */ public static String computeEndpointFromUrl(String url) { - if (url == null || url.trim().isEmpty()) { + if (url == null) { return "/"; } - java.util.regex.Matcher matcher = URL_PATTERN.matcher(url.trim()); + url = url.trim(); + if (url.isEmpty()) { + return "/"; + } + + Matcher matcher = URL_PATTERN.matcher(url); if (!matcher.matches()) { log.debug("Failed to parse URL for endpoint computation: {}", url); return "/"; @@ -193,29 +200,22 @@ public static void setEndpointTag(CoreSpan span) { /** * Sets the HTTP endpoint tag on a span context based on configuration flags. This overload - * accepts DDSpanContext for use in TagInterceptor and other core components. + * accepts DDSpanContext for use in tag post-processors and other core components. * * @param spanContext The span context to potentially tag * @param config The tracer configuration containing feature flags */ - public static void setEndpointTag( - datadog.trace.core.DDSpanContext spanContext, datadog.trace.api.Config config) { + public static void setEndpointTag(DDSpanContext spanContext, Config config) { if (!config.isResourceRenamingEnabled()) { return; } Object route = spanContext.unsafeGetTag(HTTP_ROUTE); - boolean shouldUseRoute = false; // Check if we should use route (when not forcing simplified endpoints) if (!config.isResourceRenamingAlwaysSimplifiedEndpoint() && route != null && isRouteEligible(route.toString())) { - shouldUseRoute = true; - } - - // If we should use route and not set endpoint tag, return early - if (shouldUseRoute) { return; } @@ -236,23 +236,17 @@ && isRouteEligible(route.toString())) { * @param span The span to potentially tag * @param config The tracer configuration containing feature flags */ - public static void setEndpointTag(CoreSpan span, datadog.trace.api.Config config) { + public static void setEndpointTag(CoreSpan span, Config config) { if (!config.isResourceRenamingEnabled()) { return; } Object route = span.getTag(HTTP_ROUTE); - boolean shouldUseRoute = false; // Check if we should use route (when not forcing simplified endpoints) if (!config.isResourceRenamingAlwaysSimplifiedEndpoint() && route != null && isRouteEligible(route.toString())) { - shouldUseRoute = true; - } - - // If we should use route and not set endpoint tag, return early - if (shouldUseRoute) { return; } diff --git a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/TagsPostProcessorFactory.java b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/TagsPostProcessorFactory.java index 2687211e5b2..7ddc0f91e47 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/TagsPostProcessorFactory.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/TagsPostProcessorFactory.java @@ -13,8 +13,9 @@ private static class Lazy { private static TagsPostProcessor lazyProcessor = createLazyChain(); private static TagsPostProcessor createEagerChain() { - final List processors = new ArrayList<>(2); + final List processors = new ArrayList<>(3); processors.add(new PeerServiceCalculator()); + processors.add(new HttpEndpointPostProcessor()); if (addBaseService) { processors.add(new BaseServiceAdder(Config.get().getServiceName())); } diff --git a/dd-trace-core/src/test/groovy/datadog/trace/common/metrics/HttpEndpointTaggingConfigTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/HttpEndpointTaggingConfigTest.groovy similarity index 97% rename from dd-trace-core/src/test/groovy/datadog/trace/common/metrics/HttpEndpointTaggingConfigTest.groovy rename to dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/HttpEndpointTaggingConfigTest.groovy index 7122a938433..a75ec5be0c9 100644 --- a/dd-trace-core/src/test/groovy/datadog/trace/common/metrics/HttpEndpointTaggingConfigTest.groovy +++ b/dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/HttpEndpointTaggingConfigTest.groovy @@ -1,4 +1,4 @@ -package datadog.trace.common.metrics +package datadog.trace.core.tagprocessor import datadog.trace.api.Config import datadog.trace.test.util.DDSpecification diff --git a/dd-trace-core/src/test/groovy/datadog/trace/common/metrics/HttpEndpointTaggingIntegrationTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/HttpEndpointTaggingIntegrationTest.groovy similarity index 97% rename from dd-trace-core/src/test/groovy/datadog/trace/common/metrics/HttpEndpointTaggingIntegrationTest.groovy rename to dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/HttpEndpointTaggingIntegrationTest.groovy index d56ae1c3521..b801d4a5e78 100644 --- a/dd-trace-core/src/test/groovy/datadog/trace/common/metrics/HttpEndpointTaggingIntegrationTest.groovy +++ b/dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/HttpEndpointTaggingIntegrationTest.groovy @@ -1,5 +1,6 @@ -package datadog.trace.common.metrics +package datadog.trace.core.tagprocessor +import datadog.trace.common.metrics.MetricKey import datadog.trace.test.util.DDSpecification class HttpEndpointTaggingIntegrationTest extends DDSpecification { diff --git a/dd-trace-core/src/test/groovy/datadog/trace/common/metrics/HttpEndpointTaggingTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/HttpEndpointTaggingTest.groovy similarity index 99% rename from dd-trace-core/src/test/groovy/datadog/trace/common/metrics/HttpEndpointTaggingTest.groovy rename to dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/HttpEndpointTaggingTest.groovy index 9666324834b..d791bcfc918 100644 --- a/dd-trace-core/src/test/groovy/datadog/trace/common/metrics/HttpEndpointTaggingTest.groovy +++ b/dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/HttpEndpointTaggingTest.groovy @@ -1,4 +1,4 @@ -package datadog.trace.common.metrics +package datadog.trace.core.tagprocessor import datadog.trace.test.util.DDSpecification From 95703db6dc6ff0fb6550b8d03f1cfb197b1f0b56 Mon Sep 17 00:00:00 2001 From: "sezen.leblay" Date: Fri, 10 Oct 2025 10:05:49 +0200 Subject: [PATCH 4/7] test ko Signed-off-by: sezen.leblay --- .../src/traceAgentTest/groovy/MetricsIntegrationTest.groovy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dd-trace-core/src/traceAgentTest/groovy/MetricsIntegrationTest.groovy b/dd-trace-core/src/traceAgentTest/groovy/MetricsIntegrationTest.groovy index 9984b9700d0..7a7b90b5348 100644 --- a/dd-trace-core/src/traceAgentTest/groovy/MetricsIntegrationTest.groovy +++ b/dd-trace-core/src/traceAgentTest/groovy/MetricsIntegrationTest.groovy @@ -35,11 +35,11 @@ class MetricsIntegrationTest extends AbstractTraceAgentTest { ) writer.startBucket(2, System.nanoTime(), SECONDS.toNanos(10)) writer.add( - new MetricKey("resource1", "service1", "operation1", "sql", 0, false, true, "xyzzy", [UTF8BytesString.create("grault:quux")]), + new MetricKey("resource1", "service1", "operation1", "sql", 0, false, true, "xyzzy", [UTF8BytesString.create("grault:quux")], "", ""), new AggregateMetric().recordDurations(5, new AtomicLongArray(2, 1, 2, 250, 4, 5)) ) writer.add( - new MetricKey("resource2", "service2", "operation2", "web", 200, false, true, "xyzzy", [UTF8BytesString.create("grault:quux")]), + new MetricKey("resource2", "service2", "operation2", "web", 200, false, true, "xyzzy", [UTF8BytesString.create("grault:quux")], "GET", "/api"), new AggregateMetric().recordDurations(10, new AtomicLongArray(1, 1, 200, 2, 3, 4, 5, 6, 7, 8, 9)) ) writer.finishBucket() From 97ee9c3e1e1231fa5361e2e43224007875d65325 Mon Sep 17 00:00:00 2001 From: "sezen.leblay" Date: Fri, 10 Oct 2025 10:45:03 +0200 Subject: [PATCH 5/7] test ko Signed-off-by: sezen.leblay --- .../datadog/trace/core/tagprocessor/HttpEndpointTagging.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/HttpEndpointTagging.java b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/HttpEndpointTagging.java index b28d3a136e2..0ee3d28a2fe 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/HttpEndpointTagging.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/HttpEndpointTagging.java @@ -36,6 +36,7 @@ public final class HttpEndpointTagging { private static final Pattern PARAM_HEX_ID_PATTERN = Pattern.compile("(?=.*[0-9].*)[A-Fa-f0-9._-]{6,}"); private static final Pattern PARAM_STR_PATTERN = Pattern.compile(".{20,}|.*[%&'()*+,:=@].*"); + private static final Pattern SLASH_PATTERN = Pattern.compile("/", Pattern.LITERAL); private static final int MAX_PATH_ELEMENTS = 8; @@ -101,7 +102,7 @@ public static String parameterizeUrlPath(String path) { path = path.substring(0, fragmentIndex); } - String[] allSegments = path.split("/"); + String[] allSegments = SLASH_PATTERN.split(path); List nonEmptySegments = new ArrayList<>(); for (String segment : allSegments) { From a13e396ede8523ed3c1b774695e2722e2111ed3a Mon Sep 17 00:00:00 2001 From: "sezen.leblay" Date: Mon, 13 Oct 2025 06:14:32 -0400 Subject: [PATCH 6/7] comments Signed-off-by: sezen.leblay --- .../metrics/ConflatingMetricsAggregator.java | 44 ++++++++++++++++--- .../HttpEndpointPostProcessor.java | 3 +- .../tagprocessor/HttpEndpointTagging.java | 35 +++++++++++++++ .../TagsPostProcessorFactory.java | 4 +- .../ConflatingMetricAggregatorTest.groovy | 38 ++++++++-------- .../common/metrics/FootprintForkedTest.groovy | 3 +- 6 files changed, 99 insertions(+), 28 deletions(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/common/metrics/ConflatingMetricsAggregator.java b/dd-trace-core/src/main/java/datadog/trace/common/metrics/ConflatingMetricsAggregator.java index 23d2ca9dde9..91d4da26c80 100644 --- a/dd-trace-core/src/main/java/datadog/trace/common/metrics/ConflatingMetricsAggregator.java +++ b/dd-trace-core/src/main/java/datadog/trace/common/metrics/ConflatingMetricsAggregator.java @@ -102,6 +102,7 @@ public final class ConflatingMetricsAggregator implements MetricsAggregator, Eve private final TimeUnit reportingIntervalTimeUnit; private final DDAgentFeaturesDiscovery features; private final HealthMetrics healthMetrics; + private final boolean resourceRenamingEnabled; private volatile AgentTaskScheduler.Scheduled cancellation; @@ -122,7 +123,8 @@ public ConflatingMetricsAggregator( false, DEFAULT_HEADERS), config.getTracerMetricsMaxAggregates(), - config.getTracerMetricsMaxPending()); + config.getTracerMetricsMaxPending(), + config.isResourceRenamingEnabled()); } ConflatingMetricsAggregator( @@ -142,7 +144,30 @@ public ConflatingMetricsAggregator( maxAggregates, queueSize, 10, - SECONDS); + SECONDS, + false); + } + + ConflatingMetricsAggregator( + WellKnownTags wellKnownTags, + Set ignoredResources, + DDAgentFeaturesDiscovery features, + HealthMetrics healthMetric, + Sink sink, + int maxAggregates, + int queueSize, + boolean resourceRenamingEnabled) { + this( + wellKnownTags, + ignoredResources, + features, + healthMetric, + sink, + maxAggregates, + queueSize, + 10, + SECONDS, + resourceRenamingEnabled); } ConflatingMetricsAggregator( @@ -154,7 +179,8 @@ public ConflatingMetricsAggregator( int maxAggregates, int queueSize, long reportingInterval, - TimeUnit timeUnit) { + TimeUnit timeUnit, + boolean resourceRenamingEnabled) { this( ignoredResources, features, @@ -164,7 +190,8 @@ public ConflatingMetricsAggregator( maxAggregates, queueSize, reportingInterval, - timeUnit); + timeUnit, + resourceRenamingEnabled); } ConflatingMetricsAggregator( @@ -176,7 +203,8 @@ public ConflatingMetricsAggregator( int maxAggregates, int queueSize, long reportingInterval, - TimeUnit timeUnit) { + TimeUnit timeUnit, + boolean resourceRenamingEnabled) { this.ignoredResources = ignoredResources; this.inbox = new MpscCompoundQueue<>(queueSize); this.batchPool = new SpmcArrayQueue<>(maxAggregates); @@ -185,6 +213,7 @@ public ConflatingMetricsAggregator( this.features = features; this.healthMetrics = healthMetric; this.sink = sink; + this.resourceRenamingEnabled = resourceRenamingEnabled; this.aggregator = new Aggregator( metricWriter, @@ -309,8 +338,9 @@ private boolean spanKindEligible(CoreSpan span) { private boolean publish(CoreSpan span, boolean isTopLevel) { final CharSequence spanKind = span.getTag(SPAN_KIND, ""); - final CharSequence httpMethod = span.getTag(HTTP_METHOD, ""); - final CharSequence httpEndpoint = span.getTag(HTTP_ENDPOINT, ""); + // Only include HTTP tags in metric key when resource renaming is enabled + final CharSequence httpMethod = resourceRenamingEnabled ? span.getTag(HTTP_METHOD, "") : ""; + final CharSequence httpEndpoint = resourceRenamingEnabled ? span.getTag(HTTP_ENDPOINT, "") : ""; MetricKey newKey = new MetricKey( span.getResourceName(), 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 61dcaa35516..fb120130abb 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 @@ -25,6 +25,7 @@ public HttpEndpointPostProcessor() { @Override public void processTags( TagMap unsafeTags, DDSpanContext spanContext, List spanLinks) { - HttpEndpointTagging.setEndpointTag(spanContext, config); + // Use direct map access for better performance + HttpEndpointTagging.setEndpointTag(unsafeTags, config); } } diff --git a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/HttpEndpointTagging.java b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/HttpEndpointTagging.java index 0ee3d28a2fe..db03f94283a 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/HttpEndpointTagging.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/HttpEndpointTagging.java @@ -205,7 +205,10 @@ public static void setEndpointTag(CoreSpan span) { * * @param spanContext The span context to potentially tag * @param config The tracer configuration containing feature flags + * @deprecated Use {@link #setEndpointTag(java.util.Map, Config)} for better performance in + * post-processors */ + @Deprecated public static void setEndpointTag(DDSpanContext spanContext, Config config) { if (!config.isResourceRenamingEnabled()) { return; @@ -230,6 +233,38 @@ && isRouteEligible(route.toString())) { } } + /** + * Sets the HTTP endpoint tag directly on the unsafe tags map based on configuration flags. This + * method is optimized for use in TagPostProcessors where direct map access is more efficient than + * using getTag/setTag methods. + * + * @param unsafeTags The unsafe tags map to potentially modify + * @param config The tracer configuration containing feature flags + */ + public static void setEndpointTag(java.util.Map unsafeTags, Config config) { + if (!config.isResourceRenamingEnabled()) { + return; + } + + Object route = unsafeTags.get(HTTP_ROUTE); + + // Check if we should use route (when not forcing simplified endpoints) + if (!config.isResourceRenamingAlwaysSimplifiedEndpoint() + && route != null + && isRouteEligible(route.toString())) { + return; + } + + // Try to compute endpoint from URL + Object url = unsafeTags.get(HTTP_URL); + if (url != null) { + String endpoint = computeEndpointFromUrl(url.toString()); + if (endpoint != null) { + unsafeTags.put(HTTP_ENDPOINT, endpoint); + } + } + } + /** * Sets the HTTP endpoint tag on a span based on configuration flags. This is the production * method that respects feature flags. diff --git a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/TagsPostProcessorFactory.java b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/TagsPostProcessorFactory.java index 7ddc0f91e47..d0e0015acaf 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/TagsPostProcessorFactory.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/TagsPostProcessorFactory.java @@ -15,7 +15,9 @@ private static class Lazy { private static TagsPostProcessor createEagerChain() { final List processors = new ArrayList<>(3); processors.add(new PeerServiceCalculator()); - processors.add(new HttpEndpointPostProcessor()); + if (Config.get().isResourceRenamingEnabled()) { + processors.add(new HttpEndpointPostProcessor()); + } if (addBaseService) { processors.add(new BaseServiceAdder(Config.get().getServiceName())); } diff --git a/dd-trace-core/src/test/groovy/datadog/trace/common/metrics/ConflatingMetricAggregatorTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/common/metrics/ConflatingMetricAggregatorTest.groovy index 78dfb741102..41a2f07ea31 100644 --- a/dd-trace-core/src/test/groovy/datadog/trace/common/metrics/ConflatingMetricAggregatorTest.groovy +++ b/dd-trace-core/src/test/groovy/datadog/trace/common/metrics/ConflatingMetricAggregatorTest.groovy @@ -44,7 +44,8 @@ class ConflatingMetricAggregatorTest extends DDSpecification { 10, queueSize, 1, - MILLISECONDS + MILLISECONDS, + false ) aggregator.start() @@ -74,7 +75,8 @@ class ConflatingMetricAggregatorTest extends DDSpecification { 10, queueSize, 1, - MILLISECONDS + MILLISECONDS, + false ) aggregator.start() @@ -104,7 +106,7 @@ class ConflatingMetricAggregatorTest extends DDSpecification { features.supportsMetrics() >> true features.peerTags() >> [] ConflatingMetricsAggregator aggregator = new ConflatingMetricsAggregator(empty, - features, HealthMetrics.NO_OP, sink, writer, 10, queueSize, reportingInterval, SECONDS) + features, HealthMetrics.NO_OP, sink, writer, 10, queueSize, reportingInterval, SECONDS, false) aggregator.start() when: @@ -148,7 +150,7 @@ class ConflatingMetricAggregatorTest extends DDSpecification { features.supportsMetrics() >> true features.peerTags() >> [] ConflatingMetricsAggregator aggregator = new ConflatingMetricsAggregator(empty, - features, HealthMetrics.NO_OP, sink, writer, 10, queueSize, reportingInterval, SECONDS) + features, HealthMetrics.NO_OP, sink, writer, 10, queueSize, reportingInterval, SECONDS, false) aggregator.start() when: @@ -201,7 +203,7 @@ class ConflatingMetricAggregatorTest extends DDSpecification { features.supportsMetrics() >> true features.peerTags() >>> [["country"], ["country", "georegion"],] ConflatingMetricsAggregator aggregator = new ConflatingMetricsAggregator(empty, - features, HealthMetrics.NO_OP, sink, writer, 10, queueSize, reportingInterval, SECONDS) + features, HealthMetrics.NO_OP, sink, writer, 10, queueSize, reportingInterval, SECONDS, false) aggregator.start() when: @@ -264,7 +266,7 @@ class ConflatingMetricAggregatorTest extends DDSpecification { features.supportsMetrics() >> true features.peerTags() >> ["peer.hostname", "_dd.base_service"] ConflatingMetricsAggregator aggregator = new ConflatingMetricsAggregator(empty, - features, HealthMetrics.NO_OP, sink, writer, 10, queueSize, reportingInterval, SECONDS) + features, HealthMetrics.NO_OP, sink, writer, 10, queueSize, reportingInterval, SECONDS, false) aggregator.start() when: @@ -315,7 +317,7 @@ class ConflatingMetricAggregatorTest extends DDSpecification { features.supportsMetrics() >> true features.peerTags() >> [] ConflatingMetricsAggregator aggregator = new ConflatingMetricsAggregator(empty, features, HealthMetrics.NO_OP, - sink, writer, 10, queueSize, reportingInterval, SECONDS) + sink, writer, 10, queueSize, reportingInterval, SECONDS, false) aggregator.start() when: @@ -365,7 +367,7 @@ class ConflatingMetricAggregatorTest extends DDSpecification { features.supportsMetrics() >> true features.peerTags() >> [] ConflatingMetricsAggregator aggregator = new ConflatingMetricsAggregator(empty, - features, HealthMetrics.NO_OP, sink, writer, 10, queueSize, reportingInterval, SECONDS) + features, HealthMetrics.NO_OP, sink, writer, 10, queueSize, reportingInterval, SECONDS, false) long duration = 100 List trace = [ new SimpleSpan("service", "operation", "resource", "type", true, false, false, 0, duration, HTTP_OK).setTag(SPAN_KIND, "baz"), @@ -434,7 +436,7 @@ class ConflatingMetricAggregatorTest extends DDSpecification { features.supportsMetrics() >> true features.peerTags() >> [] ConflatingMetricsAggregator aggregator = new ConflatingMetricsAggregator(empty, - features, HealthMetrics.NO_OP, sink, writer, maxAggregates, queueSize, reportingInterval, SECONDS) + features, HealthMetrics.NO_OP, sink, writer, maxAggregates, queueSize, reportingInterval, SECONDS, false) long duration = 100 aggregator.start() @@ -497,7 +499,7 @@ class ConflatingMetricAggregatorTest extends DDSpecification { features.supportsMetrics() >> true features.peerTags() >> [] ConflatingMetricsAggregator aggregator = new ConflatingMetricsAggregator(empty, - features, HealthMetrics.NO_OP, sink, writer, maxAggregates, queueSize, reportingInterval, SECONDS) + features, HealthMetrics.NO_OP, sink, writer, maxAggregates, queueSize, reportingInterval, SECONDS, false) long duration = 100 aggregator.start() @@ -593,7 +595,7 @@ class ConflatingMetricAggregatorTest extends DDSpecification { features.supportsMetrics() >> true features.peerTags() >> [] ConflatingMetricsAggregator aggregator = new ConflatingMetricsAggregator(empty, - features, HealthMetrics.NO_OP, sink, writer, maxAggregates, queueSize, reportingInterval, SECONDS) + features, HealthMetrics.NO_OP, sink, writer, maxAggregates, queueSize, reportingInterval, SECONDS, false) long duration = 100 aggregator.start() @@ -651,7 +653,7 @@ class ConflatingMetricAggregatorTest extends DDSpecification { features.supportsMetrics() >> true features.peerTags() >> [] ConflatingMetricsAggregator aggregator = new ConflatingMetricsAggregator(empty, - features, HealthMetrics.NO_OP, sink, writer, maxAggregates, queueSize, 1, SECONDS) + features, HealthMetrics.NO_OP, sink, writer, maxAggregates, queueSize, 1, SECONDS, false) long duration = 100 aggregator.start() @@ -700,7 +702,7 @@ class ConflatingMetricAggregatorTest extends DDSpecification { features.supportsMetrics() >> true features.peerTags() >> [] ConflatingMetricsAggregator aggregator = new ConflatingMetricsAggregator(empty, - features, HealthMetrics.NO_OP, sink, writer, maxAggregates, queueSize, 1, SECONDS) + features, HealthMetrics.NO_OP, sink, writer, maxAggregates, queueSize, 1, SECONDS, false) long duration = 100 aggregator.start() @@ -739,7 +741,7 @@ class ConflatingMetricAggregatorTest extends DDSpecification { features.supportsMetrics() >> true features.peerTags() >> [] ConflatingMetricsAggregator aggregator = new ConflatingMetricsAggregator(empty, - features, HealthMetrics.NO_OP, sink, writer, maxAggregates, queueSize, 1, SECONDS) + features, HealthMetrics.NO_OP, sink, writer, maxAggregates, queueSize, 1, SECONDS, false) long duration = 100 aggregator.start() @@ -770,7 +772,7 @@ class ConflatingMetricAggregatorTest extends DDSpecification { Sink sink = Stub(Sink) DDAgentFeaturesDiscovery features = Mock(DDAgentFeaturesDiscovery) ConflatingMetricsAggregator aggregator = new ConflatingMetricsAggregator(empty, - features, HealthMetrics.NO_OP, sink, writer, maxAggregates, queueSize, 1, SECONDS) + features, HealthMetrics.NO_OP, sink, writer, maxAggregates, queueSize, 1, SECONDS, false) aggregator.start() when: @@ -792,7 +794,7 @@ class ConflatingMetricAggregatorTest extends DDSpecification { features.supportsMetrics() >> false features.peerTags() >> [] ConflatingMetricsAggregator aggregator = new ConflatingMetricsAggregator(empty, - features, HealthMetrics.NO_OP, sink, writer, 10, queueSize, 200, MILLISECONDS) + features, HealthMetrics.NO_OP, sink, writer, 10, queueSize, 200, MILLISECONDS, false) final spans = [ new SimpleSpan("service", "operation", "resource", "type", false, true, false, 0, 10, HTTP_OK) ] @@ -824,7 +826,7 @@ class ConflatingMetricAggregatorTest extends DDSpecification { DDAgentFeaturesDiscovery features = Mock(DDAgentFeaturesDiscovery) features.supportsMetrics() >> true ConflatingMetricsAggregator aggregator = new ConflatingMetricsAggregator(empty, - features, HealthMetrics.NO_OP, sink, writer, maxAggregates, queueSize, 1, SECONDS) + features, HealthMetrics.NO_OP, sink, writer, maxAggregates, queueSize, 1, SECONDS, false) when: def async = CompletableFuture.supplyAsync(new Supplier() { @@ -857,7 +859,7 @@ class ConflatingMetricAggregatorTest extends DDSpecification { DDAgentFeaturesDiscovery features = Mock(DDAgentFeaturesDiscovery) features.supportsMetrics() >> true ConflatingMetricsAggregator aggregator = new ConflatingMetricsAggregator(empty, - features, HealthMetrics.NO_OP, sink, writer, 10, queueSize, reportingInterval, SECONDS) + features, HealthMetrics.NO_OP, sink, writer, 10, queueSize, reportingInterval, SECONDS, false) aggregator.start() when: diff --git a/dd-trace-core/src/test/groovy/datadog/trace/common/metrics/FootprintForkedTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/common/metrics/FootprintForkedTest.groovy index 4a96460d604..716e07c6160 100644 --- a/dd-trace-core/src/test/groovy/datadog/trace/common/metrics/FootprintForkedTest.groovy +++ b/dd-trace-core/src/test/groovy/datadog/trace/common/metrics/FootprintForkedTest.groovy @@ -39,7 +39,8 @@ class FootprintForkedTest extends DDSpecification { 1000, 1000, 100, - SECONDS) + SECONDS, + false) // Removing the 'features' as it's a mock, and mocks are heavyweight, e.g. around 22MiB def baseline = footprint(aggregator, features) aggregator.start() From 3afb7dfade4f75026c2b3a860b28a88d417a8287 Mon Sep 17 00:00:00 2001 From: "sezen.leblay" Date: Tue, 14 Oct 2025 14:34:57 -0400 Subject: [PATCH 7/7] jacoco Signed-off-by: sezen.leblay --- .../HttpEndpointPostProcessorTest.groovy | 159 ++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/HttpEndpointPostProcessorTest.groovy diff --git a/dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/HttpEndpointPostProcessorTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/HttpEndpointPostProcessorTest.groovy new file mode 100644 index 00000000000..166125c9622 --- /dev/null +++ b/dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/HttpEndpointPostProcessorTest.groovy @@ -0,0 +1,159 @@ +package datadog.trace.core.tagprocessor + +import datadog.trace.api.Config +import datadog.trace.bootstrap.instrumentation.api.Tags +import datadog.trace.core.DDSpanContext +import datadog.trace.test.util.DDSpecification + +class HttpEndpointPostProcessorTest extends DDSpecification { + + def "should not set http.endpoint when resource renaming is disabled"() { + setup: + def config = Mock(Config) + config.isResourceRenamingEnabled() >> false + + def processor = new HttpEndpointPostProcessor(config) + def spanContext = Mock(DDSpanContext) + + when: + def tags = [(Tags.HTTP_URL): "http://example.com/users/123"] + def enrichedTags = processor.processTags(tags, spanContext, []) + + then: + enrichedTags[Tags.HTTP_ENDPOINT] == null + } + + def "should set http.endpoint from URL when route is missing"() { + setup: + def config = Mock(Config) + config.isResourceRenamingEnabled() >> true + config.isResourceRenamingAlwaysSimplifiedEndpoint() >> false + + def processor = new HttpEndpointPostProcessor(config) + def spanContext = Mock(DDSpanContext) + + when: + def tags = [(Tags.HTTP_URL): "http://example.com/users/123"] + def enrichedTags = processor.processTags(tags, spanContext, []) + + then: + enrichedTags[Tags.HTTP_ENDPOINT] == "/users/{param:int}" + } + + def "should not set http.endpoint when http.route is eligible"() { + setup: + def config = Mock(Config) + config.isResourceRenamingEnabled() >> true + config.isResourceRenamingAlwaysSimplifiedEndpoint() >> false + + def processor = new HttpEndpointPostProcessor(config) + def spanContext = Mock(DDSpanContext) + + when: + def tags = [ + (Tags.HTTP_ROUTE): "/api/users/{id}", + (Tags.HTTP_URL): "http://example.com/api/users/12345" + ] + def enrichedTags = processor.processTags(tags, spanContext, []) + + then: + // When route is eligible, http.endpoint is NOT set + enrichedTags[Tags.HTTP_ENDPOINT] == null + } + + def "should set http.endpoint from URL when http.route is not eligible"() { + setup: + def config = Mock(Config) + config.isResourceRenamingEnabled() >> true + config.isResourceRenamingAlwaysSimplifiedEndpoint() >> false + + def processor = new HttpEndpointPostProcessor(config) + def spanContext = Mock(DDSpanContext) + + when: + def tags = [ + (Tags.HTTP_ROUTE): ineligibleRoute, + (Tags.HTTP_URL): "http://example.com/api/users/12345" + ] + def enrichedTags = processor.processTags(tags, spanContext, []) + + then: + enrichedTags[Tags.HTTP_ENDPOINT] == "/api/users/{param:int}" + + where: + ineligibleRoute | _ + "/" | _ + "/*" | _ + "*" | _ + } + + def "should set http.endpoint from URL in always simplified endpoint mode"() { + setup: + def config = Mock(Config) + config.isResourceRenamingEnabled() >> true + config.isResourceRenamingAlwaysSimplifiedEndpoint() >> true + + def processor = new HttpEndpointPostProcessor(config) + def spanContext = Mock(DDSpanContext) + + when: + def tags = [ + (Tags.HTTP_ROUTE): "/api/users/{id}", + (Tags.HTTP_URL): "http://example.com/api/users/12345" + ] + def enrichedTags = processor.processTags(tags, spanContext, []) + + then: + // In always simplified mode, URL parameterization is used instead of route + enrichedTags[Tags.HTTP_ENDPOINT] == "/api/users/{param:int}" + } + + def "should handle various URL patterns"() { + setup: + def config = Mock(Config) + config.isResourceRenamingEnabled() >> true + config.isResourceRenamingAlwaysSimplifiedEndpoint() >> false + + def processor = new HttpEndpointPostProcessor(config) + def spanContext = Mock(DDSpanContext) + + when: + def enrichedTags = processor.processTags([(Tags.HTTP_URL): url], spanContext, []) + + then: + enrichedTags[Tags.HTTP_ENDPOINT] == expectedEndpoint + + where: + url | expectedEndpoint + "http://example.com/" | "/" + "http://example.com/api/users/123" | "/api/users/{param:int}" + "http://example.com/api/resource/abc123def456" | "/api/resource/{param:hex}" + "http://example.com/api/resource/abc-123-def" | "/api/resource/{param:hex_id}" + "http://example.com/api/resource/123.456.789" | "/api/resource/{param:int_id}" + "http://example.com/api/very-long-string-over-twenty-chars" | "/api/{param:str}" + } + + def "should not set http.endpoint when URL is missing"() { + setup: + def config = Mock(Config) + config.isResourceRenamingEnabled() >> true + + def processor = new HttpEndpointPostProcessor(config) + def spanContext = Mock(DDSpanContext) + + when: + def enrichedTags = processor.processTags([:], spanContext, []) + + then: + // When URL is missing, http.endpoint is not set + enrichedTags[Tags.HTTP_ENDPOINT] == null + } + + def "should use default constructor"() { + when: + def processor = new HttpEndpointPostProcessor() + + then: + processor != null + } +}