Skip to content

Commit 5ff30b6

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

File tree

13 files changed

+1156
-3
lines changed

13 files changed

+1156
-3
lines changed
Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
package datadog.smoketest
2+
3+
import okhttp3.Request
4+
import java.util.concurrent.atomic.AtomicInteger
5+
import java.util.regex.Pattern
6+
7+
class HttpEndpointTaggingSmokeTest extends AbstractServerSmokeTest {
8+
9+
@Override
10+
ProcessBuilder createProcessBuilder() {
11+
String springBootShadowJar = System.getProperty("datadog.smoketest.springboot.shadowJar.path")
12+
13+
List<String> command = new ArrayList<>()
14+
command.add(javaPath())
15+
command.addAll(defaultJavaProperties)
16+
command.addAll((String[]) [
17+
"-Ddd.writer.type=MultiWriter:TraceStructureWriter:${output.getAbsolutePath()}:includeResource,DDAgentWriter",
18+
"-Ddd.trace.resource.renaming.enabled=true",
19+
"-jar",
20+
springBootShadowJar,
21+
"--server.port=${httpPort}"
22+
])
23+
ProcessBuilder processBuilder = new ProcessBuilder(command)
24+
processBuilder.directory(new File(buildDirectory))
25+
}
26+
27+
@Override
28+
File createTemporaryFile() {
29+
return File.createTempFile("http-endpoint-tagging-trace", "out")
30+
}
31+
32+
@Override
33+
protected Set<String> expectedTraces() {
34+
return [
35+
Pattern.quote("[servlet.request:GET /greeting[spring.handler:IastWebController.greeting]]")
36+
]
37+
}
38+
39+
@Override
40+
protected Set<String> assertTraceCounts(Set<String> expected, Map<String, AtomicInteger> traceCounts) {
41+
List<Pattern> remaining = expected.collect { Pattern.compile(it) }.toList()
42+
for (def i = remaining.size() - 1; i >= 0; i--) {
43+
for (Map.Entry<String, AtomicInteger> entry : traceCounts.entrySet()) {
44+
if (entry.getValue() > 0 && remaining.get(i).matcher(entry.getKey()).matches()) {
45+
remaining.remove(i)
46+
break
47+
}
48+
}
49+
}
50+
return remaining.collect { it.pattern() }.toSet()
51+
}
52+
53+
def "test basic HTTP endpoint tagging"() {
54+
setup:
55+
String url = "http://localhost:${httpPort}/greeting"
56+
def request = new Request.Builder().url(url).get().build()
57+
58+
when:
59+
def response = client.newCall(request).execute()
60+
61+
then:
62+
def responseBodyStr = response.body().string()
63+
responseBodyStr != null
64+
responseBodyStr.contains("Sup Dawg")
65+
response.code() == 200
66+
waitForTraceCount(1)
67+
}
68+
69+
def "test URL parameterization for numeric IDs"() {
70+
setup:
71+
String url = "http://localhost:${httpPort}/users/123"
72+
def request = new Request.Builder().url(url).get().build()
73+
74+
when:
75+
def response = client.newCall(request).execute()
76+
77+
then:
78+
// May return 404 since endpoint doesn't exist, but span should still be created
79+
response.code() in [200, 404]
80+
waitForTraceCount(1)
81+
}
82+
83+
def "test URL parameterization for hex patterns"() {
84+
setup:
85+
String url = "http://localhost:${httpPort}/session/abc123def456"
86+
def request = new Request.Builder().url(url).get().build()
87+
88+
when:
89+
def response = client.newCall(request).execute()
90+
91+
then:
92+
// May return 404 since endpoint doesn't exist, but span should still be created
93+
response.code() in [200, 404]
94+
waitForTraceCount(1)
95+
}
96+
97+
def "test int_id pattern parameterization"() {
98+
setup:
99+
String url = "http://localhost:${httpPort}/api/versions/12.34.56"
100+
def request = new Request.Builder().url(url).get().build()
101+
102+
when:
103+
def response = client.newCall(request).execute()
104+
105+
then:
106+
response.code() in [200, 404]
107+
waitForTraceCount(1)
108+
}
109+
110+
def "test hex_id pattern parameterization"() {
111+
setup:
112+
String url = "http://localhost:${httpPort}/api/tokens/550e8400-e29b-41d4-a716-446655440000"
113+
def request = new Request.Builder().url(url).get().build()
114+
115+
when:
116+
def response = client.newCall(request).execute()
117+
118+
then:
119+
response.code() in [200, 404]
120+
waitForTraceCount(1)
121+
}
122+
123+
def "test str pattern parameterization for long strings"() {
124+
setup:
125+
String url = "http://localhost:${httpPort}/files/very-long-filename-that-exceeds-twenty-characters.pdf"
126+
def request = new Request.Builder().url(url).get().build()
127+
128+
when:
129+
def response = client.newCall(request).execute()
130+
131+
then:
132+
response.code() in [200, 404]
133+
waitForTraceCount(1)
134+
}
135+
136+
def "test str pattern parameterization for special characters"() {
137+
setup:
138+
String url = "http://localhost:${httpPort}/search/query%20with%20spaces"
139+
def request = new Request.Builder().url(url).get().build()
140+
141+
when:
142+
def response = client.newCall(request).execute()
143+
144+
then:
145+
response.code() in [200, 404]
146+
waitForTraceCount(1)
147+
}
148+
149+
def "test mixed URL patterns with multiple segments"() {
150+
setup:
151+
String url = "http://localhost:${httpPort}/api/users/123/orders/abc456def/items/789"
152+
def request = new Request.Builder().url(url).get().build()
153+
154+
when:
155+
def response = client.newCall(request).execute()
156+
157+
then:
158+
response.code() in [200, 404]
159+
waitForTraceCount(1)
160+
}
161+
162+
def "test URL with query parameters"() {
163+
setup:
164+
String url = "http://localhost:${httpPort}/api/users/123?filter=active&limit=10"
165+
def request = new Request.Builder().url(url).get().build()
166+
167+
when:
168+
def response = client.newCall(request).execute()
169+
170+
then:
171+
response.code() in [200, 404]
172+
waitForTraceCount(1)
173+
}
174+
175+
def "test URL with trailing slash"() {
176+
setup:
177+
String url = "http://localhost:${httpPort}/api/users/456/"
178+
def request = new Request.Builder().url(url).get().build()
179+
180+
when:
181+
def response = client.newCall(request).execute()
182+
183+
then:
184+
response.code() in [200, 404]
185+
waitForTraceCount(1)
186+
}
187+
188+
def "test static paths are preserved"() {
189+
setup:
190+
String url = "http://localhost:${httpPort}/health"
191+
def request = new Request.Builder().url(url).get().build()
192+
193+
when:
194+
def response = client.newCall(request).execute()
195+
196+
then:
197+
response.code() in [200, 404]
198+
waitForTraceCount(1)
199+
}
200+
201+
def "test root path handling"() {
202+
setup:
203+
String url = "http://localhost:${httpPort}/"
204+
def request = new Request.Builder().url(url).get().build()
205+
206+
when:
207+
def response = client.newCall(request).execute()
208+
209+
then:
210+
response.code() in [200, 404]
211+
waitForTraceCount(1)
212+
}
213+
214+
def "test pattern precedence - int pattern wins over int_id"() {
215+
setup:
216+
String url = "http://localhost:${httpPort}/api/items/12345"
217+
def request = new Request.Builder().url(url).get().build()
218+
219+
when:
220+
def response = client.newCall(request).execute()
221+
222+
then:
223+
response.code() in [200, 404]
224+
waitForTraceCount(1)
225+
}
226+
227+
def "test pattern precedence - hex pattern wins over hex_id"() {
228+
setup:
229+
String url = "http://localhost:${httpPort}/api/hashes/abc123def"
230+
def request = new Request.Builder().url(url).get().build()
231+
232+
when:
233+
def response = client.newCall(request).execute()
234+
235+
then:
236+
response.code() in [200, 404]
237+
waitForTraceCount(1)
238+
}
239+
240+
def "test edge case - short segments not parameterized"() {
241+
setup:
242+
String url = "http://localhost:${httpPort}/api/x/y"
243+
def request = new Request.Builder().url(url).get().build()
244+
245+
when:
246+
def response = client.newCall(request).execute()
247+
248+
then:
249+
response.code() in [200, 404]
250+
waitForTraceCount(1)
251+
}
252+
}

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;
@@ -307,6 +309,8 @@ private boolean spanKindEligible(CoreSpan<?> span) {
307309

308310
private boolean publish(CoreSpan<?> span, boolean isTopLevel) {
309311
final CharSequence spanKind = span.getTag(SPAN_KIND, "");
312+
final CharSequence httpMethod = span.getTag(HTTP_METHOD, "");
313+
final CharSequence httpEndpoint = span.getTag(HTTP_ENDPOINT, "");
310314
MetricKey newKey =
311315
new MetricKey(
312316
span.getResourceName(),
@@ -318,7 +322,9 @@ private boolean publish(CoreSpan<?> span, boolean isTopLevel) {
318322
span.getParentId() == 0,
319323
SPAN_KINDS.computeIfAbsent(
320324
spanKind, UTF8BytesString::create), // save repeated utf8 conversions
321-
getPeerTags(span, spanKind.toString()));
325+
getPeerTags(span, spanKind.toString()),
326+
httpMethod,
327+
httpEndpoint);
322328
boolean isNewKey = false;
323329
MetricKey key = keys.putIfAbsent(newKey, newKey);
324330
if (null == key) {

0 commit comments

Comments
 (0)