Skip to content

Commit 7e905c8

Browse files
authored
Enable Inferred Proxy Span Support (#9958)
1 parent 256a096 commit 7e905c8

File tree

4 files changed

+260
-4
lines changed

4 files changed

+260
-4
lines changed

dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/HttpServerDecorator.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -544,7 +544,7 @@ public Context beforeFinish(Context context) {
544544
}
545545

546546
// Close Serverless Gateway Inferred Span if any
547-
// finishInferredProxySpan(context);
547+
finishInferredProxySpan(context);
548548

549549
return super.beforeFinish(context);
550550
}

dd-java-agent/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/src/test/groovy/test/boot/SpringBootBasedTest.groovy

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import datadog.trace.agent.test.base.HttpServer
55
import datadog.trace.agent.test.base.HttpServerTest
66
import datadog.trace.api.DDSpanTypes
77
import datadog.trace.api.DDTags
8+
import datadog.trace.api.config.TracerConfig
89
import datadog.trace.api.iast.IastContext
910
import datadog.trace.api.iast.InstrumentationBridge
1011
import datadog.trace.api.iast.SourceTypes
@@ -61,6 +62,7 @@ class SpringBootBasedTest extends HttpServerTest<ConfigurableApplicationContext>
6162
protected void configurePreAgent() {
6263
super.configurePreAgent()
6364
injectSysConfig('dd.iast.enabled', 'true')
65+
injectSysConfig(TracerConfig.TRACE_INFERRED_PROXY_SERVICES_ENABLED, 'true')
6466
}
6567

6668
@Override
@@ -479,4 +481,121 @@ class SpringBootBasedTest extends HttpServerTest<ConfigurableApplicationContext>
479481
}
480482
}
481483
}
484+
485+
def "test inferred proxy span is finished"() {
486+
setup:
487+
def request = request(SUCCESS, "GET", null)
488+
.header("x-dd-proxy", "aws-apigateway")
489+
.header("x-dd-proxy-request-time-ms", "12345")
490+
.header("x-dd-proxy-path", "/success")
491+
.header("x-dd-proxy-httpmethod", "GET")
492+
.header("x-dd-proxy-domain-name", "api.example.com")
493+
.header("x-dd-proxy-stage", "test")
494+
.build()
495+
496+
when:
497+
def response = client.newCall(request).execute()
498+
499+
then:
500+
response.code() == SUCCESS.status
501+
502+
and:
503+
// Verify that inferred proxy span was created and finished
504+
// It should appear in the trace as an additional span
505+
assertTraces(1) {
506+
trace(spanCount(SUCCESS) + 1) {
507+
sortSpansByStart()
508+
// The inferred proxy span should be the first span (earliest start time)
509+
// Verify it exists and was finished (appears in trace)
510+
// Operation name is the proxy system name (aws.apigateway), not inferred_proxy
511+
span {
512+
operationName "aws.apigateway"
513+
serviceName "api.example.com"
514+
// Resource Name: httpmethod + " " + path
515+
resourceName "GET /success"
516+
spanType "web"
517+
parent()
518+
tags {
519+
"$Tags.COMPONENT" "aws-apigateway"
520+
"$Tags.HTTP_METHOD" "GET"
521+
"$Tags.HTTP_URL" "api.example.com/success"
522+
"$Tags.HTTP_ROUTE" "/success"
523+
"stage" "test"
524+
"_dd.inferred_span" 1
525+
// Standard tags that are automatically added
526+
"_dd.agent_psr" Number
527+
"_dd.base_service" String
528+
"_dd.dsm.enabled" Number
529+
"_dd.profiling.ctx" String
530+
"_dd.profiling.enabled" Number
531+
"_dd.trace_span_attribute_schema" Number
532+
"_dd.tracer_host" String
533+
"_sample_rate" Number
534+
"language" "jvm"
535+
"process_id" Number
536+
"runtime-id" String
537+
"thread.id" Number
538+
"thread.name" String
539+
}
540+
}
541+
// Server span should be a child of the inferred proxy span
542+
// When there's an inferred proxy span parent, the server span inherits the parent's service name
543+
span {
544+
// Service name is inherited from the inferred proxy span parent
545+
serviceName "api.example.com"
546+
operationName operation()
547+
resourceName expectedResourceName(SUCCESS, "GET", address)
548+
spanType DDSpanTypes.HTTP_SERVER
549+
errored false
550+
childOfPrevious()
551+
tags {
552+
"$Tags.COMPONENT" component
553+
"$Tags.SPAN_KIND" Tags.SPAN_KIND_SERVER
554+
"$Tags.PEER_HOST_IPV4" "127.0.0.1"
555+
"$Tags.PEER_PORT" Integer
556+
"$Tags.HTTP_CLIENT_IP" "127.0.0.1"
557+
"$Tags.HTTP_HOSTNAME" address.host
558+
"$Tags.HTTP_URL" String
559+
"$Tags.HTTP_METHOD" "GET"
560+
"$Tags.HTTP_STATUS" SUCCESS.status
561+
"$Tags.HTTP_USER_AGENT" String
562+
"$Tags.HTTP_ROUTE" "/success"
563+
"servlet.context" "/boot-context"
564+
"servlet.path" "/success"
565+
defaultTags()
566+
}
567+
}
568+
if (hasHandlerSpan()) {
569+
// Handler span inherits service name from inferred proxy span parent
570+
it.span {
571+
serviceName "api.example.com"
572+
operationName "spring.handler"
573+
resourceName "TestController.success"
574+
spanType DDSpanTypes.HTTP_SERVER
575+
errored false
576+
childOfPrevious()
577+
tags {
578+
"$Tags.COMPONENT" SpringWebHttpServerDecorator.DECORATE.component()
579+
"$Tags.SPAN_KIND" Tags.SPAN_KIND_SERVER
580+
defaultTags()
581+
}
582+
}
583+
}
584+
// Controller span also inherits service name
585+
it.span {
586+
serviceName "api.example.com"
587+
operationName "controller"
588+
resourceName "controller"
589+
errored false
590+
childOfPrevious()
591+
tags {
592+
defaultTags()
593+
}
594+
}
595+
if (hasResponseSpan(SUCCESS)) {
596+
responseSpan(it, SUCCESS)
597+
}
598+
}
599+
}
600+
}
482601
}

internal-api/src/main/java/datadog/trace/api/gateway/InferredProxySpan.java

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@
22

33
import static datadog.context.ContextKey.named;
44
import static datadog.trace.api.DDTags.SPAN_TYPE;
5+
import static datadog.trace.bootstrap.instrumentation.api.ResourceNamePriorities.MANUAL_INSTRUMENTATION;
56
import static datadog.trace.bootstrap.instrumentation.api.Tags.COMPONENT;
67
import static datadog.trace.bootstrap.instrumentation.api.Tags.HTTP_METHOD;
8+
import static datadog.trace.bootstrap.instrumentation.api.Tags.HTTP_ROUTE;
79
import static datadog.trace.bootstrap.instrumentation.api.Tags.HTTP_URL;
810

911
import datadog.context.Context;
1012
import datadog.context.ContextKey;
1113
import datadog.context.ImplicitContextKeyed;
14+
import datadog.trace.api.Config;
1215
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
1316
import datadog.trace.bootstrap.instrumentation.api.AgentSpanContext;
1417
import datadog.trace.bootstrap.instrumentation.api.AgentTracer;
@@ -69,15 +72,45 @@ public AgentSpanContext start(AgentSpanContext extracted) {
6972

7073
String proxySystem = header(PROXY_SYSTEM);
7174
String proxy = SUPPORTED_PROXIES.get(proxySystem);
75+
String httpMethod = header(PROXY_HTTP_METHOD);
76+
String path = header(PROXY_PATH);
77+
String domainName = header(PROXY_DOMAIN_NAME);
78+
7279
AgentSpan span = AgentTracer.get().startSpan(INSTRUMENTATION_NAME, proxy, extracted, startTime);
73-
span.setServiceName(header(PROXY_DOMAIN_NAME));
80+
81+
// Service: value of x-dd-proxy-domain-name or global config if not found
82+
String serviceName =
83+
domainName != null && !domainName.isEmpty() ? domainName : Config.get().getServiceName();
84+
span.setServiceName(serviceName);
85+
86+
// Component: aws-apigateway
7487
span.setTag(COMPONENT, proxySystem);
88+
89+
// SpanType: web
7590
span.setTag(SPAN_TYPE, "web");
76-
span.setTag(HTTP_METHOD, header(PROXY_HTTP_METHOD));
77-
span.setTag(HTTP_URL, header(PROXY_DOMAIN_NAME) + header(PROXY_PATH));
91+
92+
// Http.method - value of x-dd-proxy-httpmethod
93+
span.setTag(HTTP_METHOD, httpMethod);
94+
95+
// Http.url - value of x-dd-proxy-domain-name + x-dd-proxy-path
96+
span.setTag(HTTP_URL, domainName != null ? domainName + path : path);
97+
98+
// Http.route - value of x-dd-proxy-path
99+
span.setTag(HTTP_ROUTE, path);
100+
101+
// "stage" - value of x-dd-proxy-stage
78102
span.setTag("stage", header(STAGE));
103+
104+
// _dd.inferred_span = 1 (indicates that this is an inferred span)
79105
span.setTag("_dd.inferred_span", 1);
80106

107+
// Resource Name: value of x-dd-proxy-httpmethod + " " + value of x-dd-proxy-path
108+
// Use MANUAL_INSTRUMENTATION priority to prevent TagInterceptor from overriding
109+
String resourceName = httpMethod != null && path != null ? httpMethod + " " + path : null;
110+
if (resourceName != null) {
111+
span.setResourceName(resourceName, MANUAL_INSTRUMENTATION);
112+
}
113+
81114
// Free collected headers
82115
this.headers.clear();
83116
// Store inferred span

internal-api/src/test/java/datadog/trace/api/gateway/InferredProxySpanTests.java

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,4 +92,108 @@ void testStoreAndFromContext() {
9292

9393
assertNull(fromContext(root()), "fromContext on empty context should be null");
9494
}
95+
96+
@Test
97+
@DisplayName("Invalid start time should return extracted context")
98+
void testInvalidStartTime() {
99+
Map<String, String> headers = new HashMap<>();
100+
headers.put(PROXY_START_TIME_MS, "invalid-number");
101+
headers.put(PROXY_SYSTEM, "aws-apigateway");
102+
103+
InferredProxySpan inferredProxySpan = fromHeaders(headers);
104+
assertTrue(inferredProxySpan.isValid());
105+
assertNull(inferredProxySpan.start(null), "Invalid start time should return null");
106+
}
107+
108+
@Test
109+
@DisplayName("Service name should fallback to config when domain name is null")
110+
void testServiceNameFallbackNull() {
111+
Map<String, String> headers = new HashMap<>();
112+
headers.put(PROXY_START_TIME_MS, "12345");
113+
headers.put(PROXY_SYSTEM, "aws-apigateway");
114+
headers.put(InferredProxySpan.PROXY_HTTP_METHOD, "GET");
115+
headers.put(InferredProxySpan.PROXY_PATH, "/test");
116+
117+
InferredProxySpan inferredProxySpan = fromHeaders(headers);
118+
assertNotNull(inferredProxySpan.start(null));
119+
// Service name should use Config.get().getServiceName() when domain name is null
120+
}
121+
122+
@Test
123+
@DisplayName("Service name should fallback to config when domain name is empty")
124+
void testServiceNameFallbackEmpty() {
125+
Map<String, String> headers = new HashMap<>();
126+
headers.put(PROXY_START_TIME_MS, "12345");
127+
headers.put(PROXY_SYSTEM, "aws-apigateway");
128+
headers.put(InferredProxySpan.PROXY_DOMAIN_NAME, "");
129+
headers.put(InferredProxySpan.PROXY_HTTP_METHOD, "GET");
130+
headers.put(InferredProxySpan.PROXY_PATH, "/test");
131+
132+
InferredProxySpan inferredProxySpan = fromHeaders(headers);
133+
assertNotNull(inferredProxySpan.start(null));
134+
// Service name should use Config.get().getServiceName() when domain name is empty
135+
}
136+
137+
@Test
138+
@DisplayName("HTTP URL should use path only when domain name is null")
139+
void testHttpUrlWithoutDomain() {
140+
Map<String, String> headers = new HashMap<>();
141+
headers.put(PROXY_START_TIME_MS, "12345");
142+
headers.put(PROXY_SYSTEM, "aws-apigateway");
143+
headers.put(InferredProxySpan.PROXY_HTTP_METHOD, "GET");
144+
headers.put(InferredProxySpan.PROXY_PATH, "/test");
145+
146+
InferredProxySpan inferredProxySpan = fromHeaders(headers);
147+
assertNotNull(inferredProxySpan.start(null));
148+
// HTTP URL should be just the path when domain name is null
149+
}
150+
151+
@Test
152+
@DisplayName("Resource name should be null when httpMethod is null")
153+
void testResourceNameNullMethod() {
154+
Map<String, String> headers = new HashMap<>();
155+
headers.put(PROXY_START_TIME_MS, "12345");
156+
headers.put(PROXY_SYSTEM, "aws-apigateway");
157+
headers.put(InferredProxySpan.PROXY_PATH, "/test");
158+
159+
InferredProxySpan inferredProxySpan = fromHeaders(headers);
160+
assertNotNull(inferredProxySpan.start(null));
161+
// Resource name should be null when httpMethod is null
162+
}
163+
164+
@Test
165+
@DisplayName("Resource name should be null when path is null")
166+
void testResourceNameNullPath() {
167+
Map<String, String> headers = new HashMap<>();
168+
headers.put(PROXY_START_TIME_MS, "12345");
169+
headers.put(PROXY_SYSTEM, "aws-apigateway");
170+
headers.put(InferredProxySpan.PROXY_HTTP_METHOD, "GET");
171+
172+
InferredProxySpan inferredProxySpan = fromHeaders(headers);
173+
assertNotNull(inferredProxySpan.start(null));
174+
// Resource name should be null when path is null
175+
}
176+
177+
@Test
178+
@DisplayName("Finish should handle null span gracefully")
179+
void testFinishWithNullSpan() {
180+
InferredProxySpan inferredProxySpan = fromHeaders(null);
181+
// Should not throw exception when span is null
182+
inferredProxySpan.finish();
183+
assertFalse(inferredProxySpan.isValid());
184+
}
185+
186+
@Test
187+
@DisplayName("Finish should clear span after finishing")
188+
void testFinishClearsSpan() {
189+
Map<String, String> headers = new HashMap<>();
190+
headers.put(PROXY_START_TIME_MS, "12345");
191+
headers.put(PROXY_SYSTEM, "aws-apigateway");
192+
193+
InferredProxySpan inferredProxySpan = fromHeaders(headers);
194+
assertNotNull(inferredProxySpan.start(null));
195+
inferredProxySpan.finish();
196+
// Span should be cleared after finish, so calling finish again should be safe
197+
inferredProxySpan.finish();
198+
}
95199
}

0 commit comments

Comments
 (0)