Skip to content

Commit 270f869

Browse files
committed
Mandatory tags implemented:
Span naming: aws.httpapi for v2 HTTP API, aws.apigateway for v1 REST (previously only v1 was supported) span.kind: set to server http.url: prefixed with https:// scheme (was missing, causing backend parsing issues) http.route: populated from new x-dd-proxy-resource-path header (resource template, e.g. /users/{id}) resource.name: uses <Method> <Route> when http.route is available, falls back to <Method> <Path> Optional tags (set only when the corresponding header is present): account_id from x-dd-proxy-account-id apiid from x-dd-proxy-api-id region from x-dd-proxy-region dd_resource_key: computed ARN (arn:aws:apigateway:{region}::/restapis/{api-id} or .../apis/{api-id})
1 parent 9ab4514 commit 270f869

File tree

8 files changed

+603
-27
lines changed

8 files changed

+603
-27
lines changed

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

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -586,16 +586,24 @@ public Context beforeFinish(Context context) {
586586
}
587587

588588
protected void finishInferredProxySpan(Context context) {
589-
InferredProxySpan span;
590-
if ((span = InferredProxySpan.fromContext(context)) != null) {
591-
span.finish();
589+
InferredProxySpan inferredProxySpan;
590+
if ((inferredProxySpan = InferredProxySpan.fromContext(context)) != null) {
591+
inferredProxySpan.finish(AgentSpan.fromContext(context));
592592
}
593593
}
594594

595595
private void onRequestEndForInstrumentationGateway(@Nonnull final AgentSpan span) {
596-
if (span.getLocalRootSpan() != span) {
596+
AgentSpan localRoot = span.getLocalRootSpan();
597+
598+
// Check if the local root is an inferred proxy span
599+
boolean hasInferredProxyParent =
600+
localRoot != span && localRoot.getTag("_dd.inferred_span") != null;
601+
602+
// Only proceed if this is the root span OR if we have an inferred proxy parent
603+
if (localRoot != span && !hasInferredProxyParent) {
597604
return;
598605
}
606+
599607
CallbackProvider cbp = tracer().getUniversalCallbackProvider();
600608
RequestContext requestContext = span.getRequestContext();
601609
if (cbp != null && requestContext != null) {

dd-java-agent/appsec/src/main/java/com/datadog/appsec/gateway/GatewayBridge.java

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
import datadog.trace.api.telemetry.RuleType;
4545
import datadog.trace.api.telemetry.WafMetricCollector;
4646
import datadog.trace.bootstrap.blocking.BlockingActionHelper;
47+
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
4748
import datadog.trace.bootstrap.instrumentation.api.Tags;
4849
import datadog.trace.bootstrap.instrumentation.api.URIDataAdapter;
4950
import datadog.trace.util.stacktrace.StackTraceEvent;
@@ -849,6 +850,7 @@ private NoopFlow onRequestEnded(RequestContext ctx_, IGSpanInfo spanInfo) {
849850
}
850851
ctx.setRequestEndCalled();
851852

853+
AgentSpan span = (AgentSpan) spanInfo;
852854
TraceSegment traceSeg = ctx_.getTraceSegment();
853855
Map<String, Object> tags = spanInfo.getTags();
854856

@@ -863,8 +865,11 @@ private NoopFlow onRequestEnded(RequestContext ctx_, IGSpanInfo spanInfo) {
863865

864866
// AppSec report metric and events for web span only
865867
if (traceSeg != null) {
866-
traceSeg.setTagTop("_dd.appsec.enabled", 1);
867-
traceSeg.setTagTop("_dd.runtime_family", "jvm");
868+
// Set AppSec tags on the service-entry span (where detection occurs).
869+
// When an inferred proxy span is present, InferredProxySpan.finish() will copy
870+
// these tags to the inferred proxy span as required by RFC-1081.
871+
span.setMetric("_dd.appsec.enabled", 1);
872+
span.setTag("_dd.runtime_family", "jvm");
868873

869874
Collection<AppSecEvent> collectedEvents = ctx.transferCollectedEvents();
870875

@@ -884,17 +889,22 @@ private NoopFlow onRequestEnded(RequestContext ctx_, IGSpanInfo spanInfo) {
884889
traceSeg.setTagTop(Tags.ASM_KEEP, true);
885890
traceSeg.setTagTop(Tags.PROPAGATED_TRACE_SOURCE, ProductTraceSource.ASM);
886891
}
887-
traceSeg.setTagTop("appsec.event", true);
888-
traceSeg.setTagTop("network.client.ip", ctx.getPeerAddress());
892+
893+
span.setTag("appsec.event", true);
894+
895+
String peerAddress = ctx.getPeerAddress();
896+
span.setTag("network.client.ip", peerAddress);
889897

890898
// Reflect client_ip as actor.ip for backward compatibility
891899
Object clientIp = tags.get(Tags.HTTP_CLIENT_IP);
892900
if (clientIp != null) {
893-
traceSeg.setTagTop("actor.ip", clientIp);
901+
span.setTag("actor.ip", clientIp.toString());
894902
}
895903

896-
// Report AppSec events via "_dd.appsec.json" tag
904+
// Report AppSec events on the service-entry span; also stored in meta_struct on the
905+
// root span via setDataTop for agent processing
897906
AppSecEventWrapper wrapper = new AppSecEventWrapper(collectedEvents);
907+
span.setTag("_dd.appsec.json", wrapper);
898908
traceSeg.setDataTop("appsec", wrapper);
899909

900910
// Report collected request and response headers based on allow list
@@ -1191,7 +1201,6 @@ public AppSecRequestContext getResult() {
11911201

11921202
private Flow<Void> maybePublishRequestData(AppSecRequestContext ctx) {
11931203
String savedRawURI = ctx.getSavedRawURI();
1194-
11951204
if (savedRawURI == null || !ctx.isFinishedRequestHeaders() || ctx.getPeerAddress() == null) {
11961205
return NoopFlow.INSTANCE;
11971206
}

dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/AppSecSystemSpecification.groovy

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ class AppSecSystemSpecification extends DDSpecification {
9999
1 * appSecReqCtx.transferCollectedEvents() >> [Stub(AppSecEvent)]
100100
1 * appSecReqCtx.getRequestHeaders() >> ['foo-bar': ['1.1.1.1']]
101101
1 * appSecReqCtx.getResponseHeaders() >> [:]
102-
1 * traceSegment.setTagTop('actor.ip', '1.1.1.1')
102+
1 * span.setTag('actor.ip', '1.1.1.1')
103103
}
104104

105105
void 'throws if the config file is not parseable'() {

dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/gateway/GatewayBridgeSpecification.groovy

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -188,13 +188,12 @@ class GatewayBridgeSpecification extends DDSpecification {
188188
1 * mockAppSecCtx.transferCollectedEvents() >> [event]
189189
1 * mockAppSecCtx.peerAddress >> '2001::1'
190190
1 * mockAppSecCtx.close()
191-
1 * traceSegment.setTagTop("_dd.appsec.enabled", 1)
192-
1 * traceSegment.setTagTop("_dd.runtime_family", "jvm")
193-
1 * traceSegment.setTagTop('appsec.event', true)
191+
1 * spanInfo.setMetric("_dd.appsec.enabled", 1)
192+
1 * spanInfo.setTag("_dd.runtime_family", "jvm")
193+
1 * spanInfo.setTag('appsec.event', true)
194+
1 * spanInfo.setTag('network.client.ip', '2001::1')
195+
1 * spanInfo.setTag('actor.ip', '1.1.1.1')
194196
1 * traceSegment.setDataTop('appsec', new AppSecEventWrapper([event]))
195-
1 * traceSegment.setTagTop('http.request.headers.accept', 'header_value')
196-
1 * traceSegment.setTagTop('http.response.headers.content-type', 'text/html; charset=UTF-8')
197-
1 * traceSegment.setTagTop('network.client.ip', '2001::1')
198197
1 * mockAppSecCtx.isWafBlocked()
199198
1 * mockAppSecCtx.hasWafErrors()
200199
1 * mockAppSecCtx.getWafTimeouts()
@@ -224,7 +223,7 @@ class GatewayBridgeSpecification extends DDSpecification {
224223
then:
225224
1 * mockAppSecCtx.transferCollectedEvents() >> [Stub(AppSecEvent)]
226225
1 * spanInfo.getTags() >> TagMap.fromMap(['http.client_ip': '8.8.8.8'])
227-
1 * traceSegment.setTagTop('actor.ip', '8.8.8.8')
226+
1 * spanInfo.setTag('actor.ip', '8.8.8.8')
228227
}
229228

230229
void 'bridge can collect headers'() {

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -517,8 +517,9 @@ class SpringBootBasedTest extends HttpServerTest<ConfigurableApplicationContext>
517517
parent()
518518
tags {
519519
"$Tags.COMPONENT" "aws-apigateway"
520+
"$Tags.SPAN_KIND" Tags.SPAN_KIND_SERVER
520521
"$Tags.HTTP_METHOD" "GET"
521-
"$Tags.HTTP_URL" "api.example.com/success"
522+
"$Tags.HTTP_URL" "https://api.example.com/success"
522523
"$Tags.HTTP_ROUTE" "/success"
523524
"stage" "test"
524525
"_dd.inferred_span" 1

dd-trace-core/src/test/java/datadog/trace/core/propagation/InferredProxyPropagatorTests.java

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ class InferredProxyPropagatorTests {
2727
private static final String PROXY_SYSTEM_KEY = "x-dd-proxy";
2828
private static final String PROXY_REQUEST_TIME_MS_KEY = "x-dd-proxy-request-time-ms";
2929
private static final String PROXY_PATH_KEY = "x-dd-proxy-path";
30+
private static final String PROXY_RESOURCE_PATH_KEY = "x-dd-proxy-resource-path";
3031
private static final String PROXY_HTTP_METHOD_KEY = "x-dd-proxy-httpmethod";
3132
private static final String PROXY_DOMAIN_NAME_KEY = "x-dd-proxy-domain-name";
3233
private static final MapVisitor MAP_VISITOR = new MapVisitor();
@@ -86,6 +87,64 @@ static Stream<Arguments> invalidOrMissingHeadersProviderForPropagator() { // Ren
8687
of("PROXY_REQUEST_TIME_MS_KEY missing", missingTime));
8788
}
8889

90+
@Test
91+
@DisplayName("Should extract x-dd-proxy-resource-path header when present")
92+
void testResourcePathHeaderExtraction() {
93+
Map<String, String> headers = new HashMap<>();
94+
headers.put(PROXY_SYSTEM_KEY, "aws-apigateway");
95+
headers.put(PROXY_REQUEST_TIME_MS_KEY, "12345");
96+
headers.put(PROXY_PATH_KEY, "/api/users/123");
97+
headers.put(PROXY_RESOURCE_PATH_KEY, "/api/users/{id}");
98+
headers.put(PROXY_HTTP_METHOD_KEY, "GET");
99+
headers.put(PROXY_DOMAIN_NAME_KEY, "api.example.com");
100+
101+
Context context = this.propagator.extract(root(), headers, MAP_VISITOR);
102+
InferredProxySpan inferredProxySpan = fromContext(context);
103+
assertNotNull(inferredProxySpan);
104+
assertTrue(inferredProxySpan.isValid());
105+
106+
// The resourcePath header should be extracted and available
107+
// for use in http.route and resource.name
108+
}
109+
110+
@Test
111+
@DisplayName("Should work without x-dd-proxy-resource-path header for backwards compatibility")
112+
void testExtractionWithoutResourcePath() {
113+
Map<String, String> headers = new HashMap<>();
114+
headers.put(PROXY_SYSTEM_KEY, "aws-apigateway");
115+
headers.put(PROXY_REQUEST_TIME_MS_KEY, "12345");
116+
headers.put(PROXY_PATH_KEY, "/api/users/123");
117+
// No PROXY_RESOURCE_PATH_KEY
118+
headers.put(PROXY_HTTP_METHOD_KEY, "GET");
119+
headers.put(PROXY_DOMAIN_NAME_KEY, "api.example.com");
120+
121+
Context context = this.propagator.extract(root(), headers, MAP_VISITOR);
122+
InferredProxySpan inferredProxySpan = fromContext(context);
123+
assertNotNull(inferredProxySpan);
124+
assertTrue(inferredProxySpan.isValid());
125+
126+
// Should still be valid without resourcePath (backwards compatibility)
127+
}
128+
129+
@Test
130+
@DisplayName("Should extract x-dd-proxy-resource-path for aws-httpapi")
131+
void testResourcePathHeaderExtractionForAwsHttpApi() {
132+
Map<String, String> headers = new HashMap<>();
133+
headers.put(PROXY_SYSTEM_KEY, "aws-httpapi");
134+
headers.put(PROXY_REQUEST_TIME_MS_KEY, "12345");
135+
headers.put(PROXY_PATH_KEY, "/v2/items/abc-123");
136+
headers.put(PROXY_RESOURCE_PATH_KEY, "/v2/items/{itemId}");
137+
headers.put(PROXY_HTTP_METHOD_KEY, "POST");
138+
headers.put(PROXY_DOMAIN_NAME_KEY, "httpapi.example.com");
139+
140+
Context context = this.propagator.extract(root(), headers, MAP_VISITOR);
141+
InferredProxySpan inferredProxySpan = fromContext(context);
142+
assertNotNull(inferredProxySpan);
143+
assertTrue(inferredProxySpan.isValid());
144+
145+
// aws-httpapi should also support resourcePath extraction
146+
}
147+
89148
@ParametersAreNonnullByDefault
90149
private static class MapVisitor implements CarrierVisitor<Map<String, String>> {
91150
@Override

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

Lines changed: 107 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import static datadog.trace.bootstrap.instrumentation.api.Tags.HTTP_METHOD;
88
import static datadog.trace.bootstrap.instrumentation.api.Tags.HTTP_ROUTE;
99
import static datadog.trace.bootstrap.instrumentation.api.Tags.HTTP_URL;
10+
import static datadog.trace.bootstrap.instrumentation.api.Tags.SPAN_KIND;
11+
import static datadog.trace.bootstrap.instrumentation.api.Tags.SPAN_KIND_SERVER;
1012

1113
import datadog.context.Context;
1214
import datadog.context.ContextKey;
@@ -25,15 +27,21 @@ public class InferredProxySpan implements ImplicitContextKeyed {
2527
static final String PROXY_SYSTEM = "x-dd-proxy";
2628
static final String PROXY_START_TIME_MS = "x-dd-proxy-request-time-ms";
2729
static final String PROXY_PATH = "x-dd-proxy-path";
30+
static final String PROXY_RESOURCE_PATH = "x-dd-proxy-resource-path";
2831
static final String PROXY_HTTP_METHOD = "x-dd-proxy-httpmethod";
2932
static final String PROXY_DOMAIN_NAME = "x-dd-proxy-domain-name";
3033
static final String STAGE = "x-dd-proxy-stage";
34+
// Optional tags
35+
static final String PROXY_ACCOUNT_ID = "x-dd-proxy-account-id";
36+
static final String PROXY_API_ID = "x-dd-proxy-api-id";
37+
static final String PROXY_REGION = "x-dd-proxy-region";
3138
static final Map<String, String> SUPPORTED_PROXIES;
3239
static final String INSTRUMENTATION_NAME = "inferred_proxy";
3340

3441
static {
3542
SUPPORTED_PROXIES = new HashMap<>();
3643
SUPPORTED_PROXIES.put("aws-apigateway", "aws.apigateway");
44+
SUPPORTED_PROXIES.put("aws-httpapi", "aws.httpapi");
3745
}
3846

3947
private final Map<String, String> headers;
@@ -75,6 +83,7 @@ public AgentSpanContext start(AgentSpanContext extracted) {
7583
String proxy = SUPPORTED_PROXIES.get(proxySystem);
7684
String httpMethod = header(PROXY_HTTP_METHOD);
7785
String path = header(PROXY_PATH);
86+
String resourcePath = header(PROXY_RESOURCE_PATH);
7887
String domainName = header(PROXY_DOMAIN_NAME);
7988

8089
AgentSpan span = AgentTracer.get().startSpan(INSTRUMENTATION_NAME, proxy, extracted, startTime);
@@ -84,30 +93,62 @@ public AgentSpanContext start(AgentSpanContext extracted) {
8493
domainName != null && !domainName.isEmpty() ? domainName : Config.get().getServiceName();
8594
span.setServiceName(serviceName, INSTRUMENTATION_NAME);
8695

87-
// Component: aws-apigateway
96+
// Component: aws-apigateway or aws-httpapi
8897
span.setTag(COMPONENT, proxySystem);
8998

99+
// Span kind: server
100+
span.setTag(SPAN_KIND, SPAN_KIND_SERVER);
101+
90102
// SpanType: web
91103
span.setTag(SPAN_TYPE, "web");
92104

93105
// Http.method - value of x-dd-proxy-httpmethod
94106
span.setTag(HTTP_METHOD, httpMethod);
95107

96-
// Http.url - value of x-dd-proxy-domain-name + x-dd-proxy-path
97-
span.setTag(HTTP_URL, domainName != null ? domainName + path : path);
108+
// Http.url - https:// + x-dd-proxy-domain-name + x-dd-proxy-path
109+
span.setTag(
110+
HTTP_URL,
111+
domainName != null && !domainName.isEmpty() ? "https://" + domainName + path : path);
98112

99-
// Http.route - value of x-dd-proxy-path
100-
span.setTag(HTTP_ROUTE, path);
113+
// Http.route - value of x-dd-proxy-resource-path (or x-dd-proxy-path as fallback)
114+
span.setTag(HTTP_ROUTE, resourcePath != null && !resourcePath.isEmpty() ? resourcePath : path);
101115

102116
// "stage" - value of x-dd-proxy-stage
103117
span.setTag("stage", header(STAGE));
104118

119+
// Optional tags - only set if present
120+
String accountId = header(PROXY_ACCOUNT_ID);
121+
if (accountId != null && !accountId.isEmpty()) {
122+
span.setTag("account_id", accountId);
123+
}
124+
125+
String apiId = header(PROXY_API_ID);
126+
if (apiId != null && !apiId.isEmpty()) {
127+
span.setTag("apiid", apiId);
128+
}
129+
130+
String region = header(PROXY_REGION);
131+
if (region != null && !region.isEmpty()) {
132+
span.setTag("region", region);
133+
}
134+
135+
// Compute and set dd_resource_key (ARN) if we have region and apiId
136+
if (region != null && !region.isEmpty() && apiId != null && !apiId.isEmpty()) {
137+
String arn = computeArn(proxySystem, region, apiId);
138+
if (arn != null) {
139+
span.setTag("dd_resource_key", arn);
140+
}
141+
}
142+
105143
// _dd.inferred_span = 1 (indicates that this is an inferred span)
106144
span.setTag("_dd.inferred_span", 1);
107145

108-
// Resource Name: value of x-dd-proxy-httpmethod + " " + value of x-dd-proxy-path
146+
// Resource Name: <Method> <Route> when route available, else <Method> <Path>
147+
// Prefer x-dd-proxy-resource-path (route) over x-dd-proxy-path (path)
109148
// Use MANUAL_INSTRUMENTATION priority to prevent TagInterceptor from overriding
110-
String resourceName = httpMethod != null && path != null ? httpMethod + " " + path : null;
149+
String routeOrPath = resourcePath != null && !resourcePath.isEmpty() ? resourcePath : path;
150+
String resourceName =
151+
httpMethod != null && routeOrPath != null ? httpMethod + " " + routeOrPath : null;
111152
if (resourceName != null) {
112153
span.setResourceName(resourceName, MANUAL_INSTRUMENTATION);
113154
}
@@ -124,13 +165,72 @@ private String header(String name) {
124165
return this.headers.get(name);
125166
}
126167

168+
/**
169+
* Compute ARN for the API Gateway resource. Format for v1 REST:
170+
* arn:aws:apigateway:{region}::/restapis/{api-id} Format for v2 HTTP:
171+
* arn:aws:apigateway:{region}::/apis/{api-id}
172+
*/
173+
private String computeArn(String proxySystem, String region, String apiId) {
174+
if (proxySystem == null || region == null || apiId == null) {
175+
return null;
176+
}
177+
178+
// Assume AWS partition (could be extended to support other partitions like aws-cn, aws-us-gov)
179+
String partition = "aws";
180+
181+
// Determine resource type based on proxy system
182+
String resourceType;
183+
if ("aws-apigateway".equals(proxySystem)) {
184+
resourceType = "restapis"; // v1 REST API
185+
} else if ("aws-httpapi".equals(proxySystem)) {
186+
resourceType = "apis"; // v2 HTTP API
187+
} else {
188+
return null; // Unknown proxy type
189+
}
190+
191+
return String.format("arn:%s:apigateway:%s::/%s/%s", partition, region, resourceType, apiId);
192+
}
193+
127194
public void finish() {
195+
finish(null);
196+
}
197+
198+
/**
199+
* Finishes this inferred proxy span and copies AppSec tags from the service-entry span to this
200+
* span as required by RFC-1081. AppSec detection occurs in the service-entry span context, so its
201+
* tags must be propagated to the inferred proxy span for endpoint correlation.
202+
*
203+
* @param serviceEntrySpan the service-entry child span, or null if not available
204+
*/
205+
public void finish(AgentSpan serviceEntrySpan) {
128206
if (this.span != null) {
207+
copyAppSecTagsFromServiceEntry(serviceEntrySpan);
129208
this.span.finish();
130209
this.span = null;
131210
}
132211
}
133212

213+
/**
214+
* Copies AppSec tags from the service-entry span to this inferred proxy span as required by
215+
* RFC-1081: the inferred span must carry {@code _dd.appsec.enabled} and {@code _dd.appsec.json}
216+
* so that security activity can be correlated with the API Gateway endpoint.
217+
*/
218+
private void copyAppSecTagsFromServiceEntry(AgentSpan serviceEntrySpan) {
219+
if (serviceEntrySpan == null || serviceEntrySpan == this.span) {
220+
return;
221+
}
222+
223+
Object appsecEnabled = serviceEntrySpan.getTag("_dd.appsec.enabled");
224+
if (appsecEnabled != null) {
225+
this.span.setMetric("_dd.appsec.enabled", 1);
226+
}
227+
228+
Object appsecJson = serviceEntrySpan.getTag("_dd.appsec.json");
229+
if (appsecJson != null) {
230+
this.span.setTag("_dd.appsec.json", appsecJson.toString());
231+
}
232+
}
233+
134234
@Override
135235
public Context storeInto(@Nonnull Context context) {
136236
return context.with(CONTEXT_KEY, this);

0 commit comments

Comments
 (0)