Skip to content

Commit b3676e3

Browse files
committed
Inferred Routes and Resource Renaming: new APM trace metrics tag
Signed-off-by: sezen.leblay <[email protected]>
1 parent e42a5b2 commit b3676e3

File tree

13 files changed

+905
-4
lines changed

13 files changed

+905
-4
lines changed
Submodule integrations-core updated 4981 files

dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,12 @@ public final class ConfigDefaults {
168168
"datadog.trace.*:org.apache.commons.*:org.mockito.*";
169169
static final boolean DEFAULT_CIVISIBILITY_GIT_UPLOAD_ENABLED = true;
170170
static final boolean DEFAULT_CIVISIBILITY_GIT_UNSHALLOW_ENABLED = true;
171+
172+
// HTTP Endpoint Tagging feature flags
173+
static final boolean DEFAULT_RESOURCE_RENAMING_ENABLED =
174+
false; // Default enablement of resource renaming
175+
static final boolean DEFAULT_RESOURCE_RENAMING_ALWAYS_SIMPLIFIED_ENDPOINT =
176+
false; // Manual disablement of resource renaming
171177
static final long DEFAULT_CIVISIBILITY_GIT_COMMAND_TIMEOUT_MILLIS = 30_000;
172178
static final long DEFAULT_CIVISIBILITY_BACKEND_API_TIMEOUT_MILLIS = 30_000;
173179
static final long DEFAULT_CIVISIBILITY_GIT_UPLOAD_TIMEOUT_MILLIS = 60_000;

dd-trace-api/src/main/java/datadog/trace/api/config/TracerConfig.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ public final class TracerConfig {
6666
"trace.http.resource.remove-trailing-slash";
6767
public static final String TRACE_HTTP_SERVER_PATH_RESOURCE_NAME_MAPPING =
6868
"trace.http.server.path-resource-name-mapping";
69+
70+
// HTTP Endpoint Tagging feature flags
71+
public static final String TRACE_RESOURCE_RENAMING_ENABLED = "trace.resource.renaming.enabled";
72+
public static final String TRACE_RESOURCE_RENAMING_ALWAYS_SIMPLIFIED_ENDPOINT =
73+
"trace.resource.renaming.always-simplified-endpoint";
6974
public static final String TRACE_HTTP_CLIENT_PATH_RESOURCE_NAME_MAPPING =
7075
"trace.http.client.path-resource-name-mapping";
7176
// Use TRACE_HTTP_SERVER_ERROR_STATUSES instead

dd-trace-core/src/main/java/datadog/trace/common/metrics/ConflatingMetricsAggregator.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import static datadog.communication.ddagent.DDAgentFeaturesDiscovery.V6_METRICS_ENDPOINT;
44
import static datadog.trace.api.DDTags.BASE_SERVICE;
55
import static datadog.trace.api.Functions.UTF8_ENCODE;
6+
import static datadog.trace.bootstrap.instrumentation.api.Tags.HTTP_ENDPOINT;
7+
import static datadog.trace.bootstrap.instrumentation.api.Tags.HTTP_METHOD;
68
import static datadog.trace.bootstrap.instrumentation.api.Tags.SPAN_KIND;
79
import static datadog.trace.bootstrap.instrumentation.api.Tags.SPAN_KIND_CLIENT;
810
import static datadog.trace.bootstrap.instrumentation.api.Tags.SPAN_KIND_CONSUMER;
@@ -306,6 +308,8 @@ private boolean spanKindEligible(CoreSpan<?> span) {
306308

307309
private boolean publish(CoreSpan<?> span, boolean isTopLevel) {
308310
final CharSequence spanKind = span.getTag(SPAN_KIND, "");
311+
final CharSequence httpMethod = span.getTag(HTTP_METHOD, "");
312+
final CharSequence httpEndpoint = span.getTag(HTTP_ENDPOINT, "");
309313
MetricKey newKey =
310314
new MetricKey(
311315
span.getResourceName(),
@@ -317,7 +321,9 @@ private boolean publish(CoreSpan<?> span, boolean isTopLevel) {
317321
span.getParentId() == 0,
318322
SPAN_KINDS.computeIfAbsent(
319323
spanKind, UTF8BytesString::create), // save repeated utf8 conversions
320-
getPeerTags(span, spanKind.toString()));
324+
getPeerTags(span, spanKind.toString()),
325+
httpMethod,
326+
httpEndpoint);
321327
boolean isNewKey = false;
322328
MetricKey key = keys.putIfAbsent(newKey, newKey);
323329
if (null == key) {
Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
package datadog.trace.common.metrics;
2+
3+
import static datadog.trace.bootstrap.instrumentation.api.Tags.HTTP_ENDPOINT;
4+
import static datadog.trace.bootstrap.instrumentation.api.Tags.HTTP_ROUTE;
5+
import static datadog.trace.bootstrap.instrumentation.api.Tags.HTTP_URL;
6+
7+
import datadog.trace.core.CoreSpan;
8+
import java.util.ArrayList;
9+
import java.util.List;
10+
import java.util.regex.Pattern;
11+
import org.slf4j.Logger;
12+
import org.slf4j.LoggerFactory;
13+
14+
/**
15+
* Utility class for HTTP endpoint tagging logic. Handles route eligibility checks and URL path
16+
* parameterization for trace metrics.
17+
*
18+
* <p>This implementation ensures: 1. Only applies to HTTP service entry spans (server spans) 2.
19+
* Limits cardinality through URL path parameterization 3. Uses http.route when available and
20+
* eligible (90% accuracy constraint) 4. Provides failsafe endpoint computation from http.url
21+
*/
22+
public final class HttpEndpointTagging {
23+
24+
private static final Logger log = LoggerFactory.getLogger(HttpEndpointTagging.class);
25+
26+
private static final Pattern URL_PATTERN =
27+
Pattern.compile("^(?<protocol>[a-z]+://(?<host>[^?/]+))?(?<path>/[^?]*)(?<query>(\\?).*)?$");
28+
29+
// Applied in order - first match wins
30+
private static final Pattern PARAM_INT_PATTERN = Pattern.compile("[1-9][0-9]+");
31+
private static final Pattern PARAM_INT_ID_PATTERN = Pattern.compile("(?=.*[0-9].*)[0-9._-]{3,}");
32+
private static final Pattern PARAM_HEX_PATTERN = Pattern.compile("(?=.*[0-9].*)[A-Fa-f0-9]{6,}");
33+
private static final Pattern PARAM_HEX_ID_PATTERN =
34+
Pattern.compile("(?=.*[0-9].*)[A-Fa-f0-9._-]{6,}");
35+
private static final Pattern PARAM_STR_PATTERN = Pattern.compile(".{20,}|.*[%&'()*+,:=@].*");
36+
37+
private static final int MAX_PATH_ELEMENTS = 8;
38+
39+
private HttpEndpointTagging() {
40+
// Utility class - no instantiation
41+
}
42+
43+
/**
44+
* Determines if an HTTP route is eligible for use as endpoint tag. Routes must meet accuracy
45+
* requirements (90% constraint) to be considered eligible.
46+
*
47+
* @param route the HTTP route to check
48+
* @return true if route is eligible, false otherwise
49+
*/
50+
public static boolean isRouteEligible(String route) {
51+
if (route == null || route.trim().isEmpty()) {
52+
return false;
53+
}
54+
55+
route = route.trim();
56+
57+
// Route must start with / to be a valid path
58+
if (!route.startsWith("/")) {
59+
return false;
60+
}
61+
62+
// Reject overly generic routes that don't provide meaningful endpoint information
63+
if ("/".equals(route) || "/*".equals(route) || "*".equals(route)) {
64+
return false;
65+
}
66+
67+
// Reject routes that are just wildcards
68+
if (route.matches("^[*/]+$")) {
69+
return false;
70+
}
71+
72+
// Route is eligible for endpoint tagging
73+
return true;
74+
}
75+
76+
/**
77+
* Parameterizes a URL path by replacing dynamic segments with {param:type} tokens. Splits path on
78+
* '/', discards empty elements, keeps first 8 elements, and applies regex patterns in order.
79+
*
80+
* @param path the URL path to parameterize
81+
* @return parameterized path with dynamic segments replaced by {param:type} tokens
82+
*/
83+
public static String parameterizeUrlPath(String path) {
84+
if (path == null) {
85+
return null;
86+
}
87+
88+
if (path.isEmpty() || "/".equals(path)) {
89+
return path;
90+
}
91+
92+
int queryIndex = path.indexOf('?');
93+
if (queryIndex != -1) {
94+
path = path.substring(0, queryIndex);
95+
}
96+
97+
int fragmentIndex = path.indexOf('#');
98+
if (fragmentIndex != -1) {
99+
path = path.substring(0, fragmentIndex);
100+
}
101+
102+
String[] allSegments = path.split("/");
103+
List<String> nonEmptySegments = new ArrayList<>();
104+
105+
for (String segment : allSegments) {
106+
if (!segment.isEmpty()) {
107+
nonEmptySegments.add(segment);
108+
}
109+
}
110+
111+
List<String> segments =
112+
nonEmptySegments.size() > MAX_PATH_ELEMENTS
113+
? nonEmptySegments.subList(0, MAX_PATH_ELEMENTS)
114+
: nonEmptySegments;
115+
116+
StringBuilder result = new StringBuilder();
117+
for (String segment : segments) {
118+
result.append("/");
119+
120+
// First match wins
121+
if (PARAM_INT_PATTERN.matcher(segment).matches()) {
122+
result.append("{param:int}");
123+
} else if (PARAM_INT_ID_PATTERN.matcher(segment).matches()) {
124+
result.append("{param:int_id}");
125+
} else if (PARAM_HEX_PATTERN.matcher(segment).matches()) {
126+
result.append("{param:hex}");
127+
} else if (PARAM_HEX_ID_PATTERN.matcher(segment).matches()) {
128+
result.append("{param:hex_id}");
129+
} else if (PARAM_STR_PATTERN.matcher(segment).matches()) {
130+
result.append("{param:str}");
131+
} else {
132+
result.append(segment);
133+
}
134+
}
135+
136+
String parameterized = result.toString();
137+
return parameterized.isEmpty() ? "/" : parameterized;
138+
}
139+
140+
/**
141+
* Computes endpoint from HTTP URL using regex parsing. Returns '/' when URL is unavailable or
142+
* invalid.
143+
*
144+
* @param url the HTTP URL to process
145+
* @return parameterized endpoint path or '/'
146+
*/
147+
public static String computeEndpointFromUrl(String url) {
148+
if (url == null || url.trim().isEmpty()) {
149+
return "/";
150+
}
151+
152+
java.util.regex.Matcher matcher = URL_PATTERN.matcher(url.trim());
153+
if (!matcher.matches()) {
154+
log.debug("Failed to parse URL for endpoint computation: {}", url);
155+
return "/";
156+
}
157+
158+
String path = matcher.group("path");
159+
if (path == null || path.isEmpty()) {
160+
return "/";
161+
}
162+
163+
return parameterizeUrlPath(path);
164+
}
165+
166+
/**
167+
* Sets the HTTP endpoint tag on a span if conditions are met. Only applies to HTTP service entry
168+
* spans when: 1. http.route is missing, empty, or not eligible 2. http.url is available for
169+
* endpoint computation
170+
*
171+
* <p>This method is designed for testing and backward compatibility. Production usage should
172+
* integrate with feature flags and span kind checks.
173+
*
174+
* @param span The span to potentially tag
175+
*/
176+
public static void setEndpointTag(CoreSpan<?> span) {
177+
Object route = span.getTag(HTTP_ROUTE);
178+
179+
// If route exists and is eligible, don't set endpoint tag
180+
if (route != null && isRouteEligible(route.toString())) {
181+
return;
182+
}
183+
184+
// Try to compute endpoint from URL
185+
Object url = span.getTag(HTTP_URL);
186+
if (url != null) {
187+
String endpoint = computeEndpointFromUrl(url.toString());
188+
if (endpoint != null) {
189+
span.setTag(HTTP_ENDPOINT, endpoint);
190+
}
191+
}
192+
}
193+
194+
/**
195+
* Sets the HTTP endpoint tag on a span context based on configuration flags. This overload
196+
* accepts DDSpanContext for use in TagInterceptor and other core components.
197+
*
198+
* @param spanContext The span context to potentially tag
199+
* @param config The tracer configuration containing feature flags
200+
*/
201+
public static void setEndpointTag(
202+
datadog.trace.core.DDSpanContext spanContext, datadog.trace.api.Config config) {
203+
if (!config.isResourceRenamingEnabled()) {
204+
return;
205+
}
206+
207+
Object route = spanContext.unsafeGetTag(HTTP_ROUTE);
208+
boolean shouldUseRoute = false;
209+
210+
// Check if we should use route (when not forcing simplified endpoints)
211+
if (!config.isResourceRenamingAlwaysSimplifiedEndpoint()
212+
&& route != null
213+
&& isRouteEligible(route.toString())) {
214+
shouldUseRoute = true;
215+
}
216+
217+
// If we should use route and not set endpoint tag, return early
218+
if (shouldUseRoute) {
219+
return;
220+
}
221+
222+
// Try to compute endpoint from URL
223+
Object url = spanContext.unsafeGetTag(HTTP_URL);
224+
if (url != null) {
225+
String endpoint = computeEndpointFromUrl(url.toString());
226+
if (endpoint != null) {
227+
spanContext.setTag(HTTP_ENDPOINT, endpoint);
228+
}
229+
}
230+
}
231+
232+
/**
233+
* Sets the HTTP endpoint tag on a span based on configuration flags. This is the production
234+
* method that respects feature flags.
235+
*
236+
* @param span The span to potentially tag
237+
* @param config The tracer configuration containing feature flags
238+
*/
239+
public static void setEndpointTag(CoreSpan<?> span, datadog.trace.api.Config config) {
240+
if (!config.isResourceRenamingEnabled()) {
241+
return;
242+
}
243+
244+
Object route = span.getTag(HTTP_ROUTE);
245+
boolean shouldUseRoute = false;
246+
247+
// Check if we should use route (when not forcing simplified endpoints)
248+
if (!config.isResourceRenamingAlwaysSimplifiedEndpoint()
249+
&& route != null
250+
&& isRouteEligible(route.toString())) {
251+
shouldUseRoute = true;
252+
}
253+
254+
// If we should use route and not set endpoint tag, return early
255+
if (shouldUseRoute) {
256+
return;
257+
}
258+
259+
// Try to compute endpoint from URL
260+
Object url = span.getTag(HTTP_URL);
261+
if (url != null) {
262+
String endpoint = computeEndpointFromUrl(url.toString());
263+
if (endpoint != null) {
264+
span.setTag(HTTP_ENDPOINT, endpoint);
265+
}
266+
}
267+
}
268+
}

dd-trace-core/src/main/java/datadog/trace/common/metrics/MetricKey.java

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ public final class MetricKey {
1818
private final boolean isTraceRoot;
1919
private final UTF8BytesString spanKind;
2020
private final List<UTF8BytesString> peerTags;
21+
private final UTF8BytesString httpMethod;
22+
private final UTF8BytesString httpEndpoint;
2123

2224
public MetricKey(
2325
CharSequence resource,
@@ -28,7 +30,9 @@ public MetricKey(
2830
boolean synthetics,
2931
boolean isTraceRoot,
3032
CharSequence spanKind,
31-
List<UTF8BytesString> peerTags) {
33+
List<UTF8BytesString> peerTags,
34+
CharSequence httpMethod,
35+
CharSequence httpEndpoint) {
3236
this.resource = null == resource ? EMPTY : UTF8BytesString.create(resource);
3337
this.service = null == service ? EMPTY : UTF8BytesString.create(service);
3438
this.operationName = null == operationName ? EMPTY : UTF8BytesString.create(operationName);
@@ -38,6 +42,8 @@ public MetricKey(
3842
this.isTraceRoot = isTraceRoot;
3943
this.spanKind = null == spanKind ? EMPTY : UTF8BytesString.create(spanKind);
4044
this.peerTags = peerTags == null ? Collections.emptyList() : peerTags;
45+
this.httpMethod = null == httpMethod ? EMPTY : UTF8BytesString.create(httpMethod);
46+
this.httpEndpoint = null == httpEndpoint ? EMPTY : UTF8BytesString.create(httpEndpoint);
4147

4248
// Unrolled polynomial hashcode to avoid varargs allocation
4349
// and eliminate data dependency between iterations as in Arrays.hashCode.
@@ -55,6 +61,8 @@ public MetricKey(
5561
+ 29791 * this.operationName.hashCode()
5662
+ 961 * this.type.hashCode()
5763
+ 31 * httpStatusCode
64+
+ 29 * this.httpMethod.hashCode()
65+
+ 27 * this.httpEndpoint.hashCode()
5866
+ (this.synthetics ? 1 : 0);
5967
}
6068

@@ -94,6 +102,14 @@ public List<UTF8BytesString> getPeerTags() {
94102
return peerTags;
95103
}
96104

105+
public UTF8BytesString getHttpMethod() {
106+
return httpMethod;
107+
}
108+
109+
public UTF8BytesString getHttpEndpoint() {
110+
return httpEndpoint;
111+
}
112+
97113
@Override
98114
public boolean equals(Object o) {
99115
if (this == o) {
@@ -110,7 +126,9 @@ public boolean equals(Object o) {
110126
&& type.equals(metricKey.type)
111127
&& isTraceRoot == metricKey.isTraceRoot
112128
&& spanKind.equals(metricKey.spanKind)
113-
&& peerTags.equals(metricKey.peerTags);
129+
&& peerTags.equals(metricKey.peerTags)
130+
&& httpMethod.equals(metricKey.httpMethod)
131+
&& httpEndpoint.equals(metricKey.httpEndpoint);
114132
}
115133
return false;
116134
}

0 commit comments

Comments
 (0)