Skip to content

Commit 85a368c

Browse files
committed
WIP
1 parent 9e4cfe6 commit 85a368c

File tree

3 files changed

+575
-7
lines changed

3 files changed

+575
-7
lines changed

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

Lines changed: 60 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,65 @@ static Stream<Arguments> invalidOrMissingHeadersProviderForPropagator() { // Ren
8687
of("PROXY_REQUEST_TIME_MS_KEY missing", missingTime));
8788
}
8889

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

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

Lines changed: 99 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;
@@ -24,15 +26,21 @@ public class InferredProxySpan implements ImplicitContextKeyed {
2426
static final String PROXY_SYSTEM = "x-dd-proxy";
2527
static final String PROXY_START_TIME_MS = "x-dd-proxy-request-time-ms";
2628
static final String PROXY_PATH = "x-dd-proxy-path";
29+
static final String PROXY_RESOURCE_PATH = "x-dd-proxy-resource-path";
2730
static final String PROXY_HTTP_METHOD = "x-dd-proxy-httpmethod";
2831
static final String PROXY_DOMAIN_NAME = "x-dd-proxy-domain-name";
2932
static final String STAGE = "x-dd-proxy-stage";
33+
// Optional tags
34+
static final String PROXY_ACCOUNT_ID = "x-dd-proxy-account-id";
35+
static final String PROXY_API_ID = "x-dd-proxy-api-id";
36+
static final String PROXY_REGION = "x-dd-proxy-region";
3037
static final Map<String, String> SUPPORTED_PROXIES;
3138
static final String INSTRUMENTATION_NAME = "inferred_proxy";
3239

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

3846
private final Map<String, String> headers;
@@ -74,6 +82,7 @@ public AgentSpanContext start(AgentSpanContext extracted) {
7482
String proxy = SUPPORTED_PROXIES.get(proxySystem);
7583
String httpMethod = header(PROXY_HTTP_METHOD);
7684
String path = header(PROXY_PATH);
85+
String resourcePath = header(PROXY_RESOURCE_PATH);
7786
String domainName = header(PROXY_DOMAIN_NAME);
7887

7988
AgentSpan span = AgentTracer.get().startSpan(INSTRUMENTATION_NAME, proxy, extracted, startTime);
@@ -83,30 +92,59 @@ public AgentSpanContext start(AgentSpanContext extracted) {
8392
domainName != null && !domainName.isEmpty() ? domainName : Config.get().getServiceName();
8493
span.setServiceName(serviceName);
8594

86-
// Component: aws-apigateway
95+
// Component: aws-apigateway or aws-httpapi
8796
span.setTag(COMPONENT, proxySystem);
8897

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

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

95-
// Http.url - value of x-dd-proxy-domain-name + x-dd-proxy-path
96-
span.setTag(HTTP_URL, domainName != null ? domainName + path : path);
107+
// Http.url - https:// + x-dd-proxy-domain-name + x-dd-proxy-path
108+
span.setTag(HTTP_URL, domainName != null ? "https://" + domainName + path : path);
97109

98-
// Http.route - value of x-dd-proxy-path
99-
span.setTag(HTTP_ROUTE, path);
110+
// Http.route - value of x-dd-proxy-resource-path (or x-dd-proxy-path as fallback)
111+
span.setTag(HTTP_ROUTE, resourcePath != null ? resourcePath : path);
100112

101113
// "stage" - value of x-dd-proxy-stage
102114
span.setTag("stage", header(STAGE));
103115

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

107-
// Resource Name: value of x-dd-proxy-httpmethod + " " + value of x-dd-proxy-path
143+
// Resource Name: <Method> <Route> when route available, else <Method> <Path>
144+
// Prefer x-dd-proxy-resource-path (route) over x-dd-proxy-path (path)
108145
// Use MANUAL_INSTRUMENTATION priority to prevent TagInterceptor from overriding
109-
String resourceName = httpMethod != null && path != null ? httpMethod + " " + path : null;
146+
String routeOrPath = resourcePath != null ? resourcePath : path;
147+
String resourceName = httpMethod != null && routeOrPath != null ? httpMethod + " " + routeOrPath : null;
110148
if (resourceName != null) {
111149
span.setResourceName(resourceName, MANUAL_INSTRUMENTATION);
112150
}
@@ -123,13 +161,67 @@ private String header(String name) {
123161
return this.headers.get(name);
124162
}
125163

164+
/**
165+
* Compute ARN for the API Gateway resource.
166+
* Format for v1 REST: arn:aws:apigateway:{region}::/restapis/{api-id}
167+
* Format for v2 HTTP: arn:aws:apigateway:{region}::/apis/{api-id}
168+
*/
169+
private String computeArn(String proxySystem, String region, String apiId) {
170+
if (proxySystem == null || region == null || apiId == null) {
171+
return null;
172+
}
173+
174+
// Assume AWS partition (could be extended to support other partitions like aws-cn, aws-us-gov)
175+
String partition = "aws";
176+
177+
// Determine resource type based on proxy system
178+
String resourceType;
179+
if ("aws-apigateway".equals(proxySystem)) {
180+
resourceType = "restapis"; // v1 REST API
181+
} else if ("aws-httpapi".equals(proxySystem)) {
182+
resourceType = "apis"; // v2 HTTP API
183+
} else {
184+
return null; // Unknown proxy type
185+
}
186+
187+
return String.format("arn:%s:apigateway:%s::/%s/%s", partition, region, resourceType, apiId);
188+
}
189+
126190
public void finish() {
127191
if (this.span != null) {
192+
// Copy AppSec tags from root span if needed (distributed tracing scenario)
193+
copyAppSecTagsFromRoot();
194+
128195
this.span.finish();
129196
this.span = null;
130197
}
131198
}
132199

200+
/**
201+
* Copy AppSec tags from the root span to this inferred proxy span.
202+
* This is needed when distributed tracing is active, because AppSec sets tags
203+
* on the absolute root span (via setTagTop), but we need them on the inferred
204+
* proxy span which may be a child of the upstream root span.
205+
*/
206+
private void copyAppSecTagsFromRoot() {
207+
AgentSpan rootSpan = this.span.getLocalRootSpan();
208+
209+
// If root span is different from this span (distributed tracing case)
210+
if (rootSpan != null && rootSpan != this.span) {
211+
// Copy _dd.appsec.enabled metric (always 1 if present)
212+
Object appsecEnabled = rootSpan.getTag("_dd.appsec.enabled");
213+
if (appsecEnabled != null) {
214+
this.span.setMetric("_dd.appsec.enabled", 1);
215+
}
216+
217+
// Copy _dd.appsec.json tag (AppSec events)
218+
Object appsecJson = rootSpan.getTag("_dd.appsec.json");
219+
if (appsecJson != null) {
220+
this.span.setTag("_dd.appsec.json", appsecJson.toString());
221+
}
222+
}
223+
}
224+
133225
@Override
134226
public Context storeInto(Context context) {
135227
return context.with(CONTEXT_KEY, this);

0 commit comments

Comments
 (0)