Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -517,8 +517,9 @@ class SpringBootBasedTest extends HttpServerTest<ConfigurableApplicationContext>
parent()
tags {
"$Tags.COMPONENT" "aws-apigateway"
"$Tags.SPAN_KIND" Tags.SPAN_KIND_SERVER
"$Tags.HTTP_METHOD" "GET"
"$Tags.HTTP_URL" "api.example.com/success"
"$Tags.HTTP_URL" "https://api.example.com/success"
"$Tags.HTTP_ROUTE" "/success"
"stage" "test"
"_dd.inferred_span" 1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class InferredProxyPropagatorTests {
private static final String PROXY_SYSTEM_KEY = "x-dd-proxy";
private static final String PROXY_REQUEST_TIME_MS_KEY = "x-dd-proxy-request-time-ms";
private static final String PROXY_PATH_KEY = "x-dd-proxy-path";
private static final String PROXY_RESOURCE_PATH_KEY = "x-dd-proxy-resource-path";
private static final String PROXY_HTTP_METHOD_KEY = "x-dd-proxy-httpmethod";
private static final String PROXY_DOMAIN_NAME_KEY = "x-dd-proxy-domain-name";
private static final MapVisitor MAP_VISITOR = new MapVisitor();
Expand Down Expand Up @@ -86,6 +87,64 @@ static Stream<Arguments> invalidOrMissingHeadersProviderForPropagator() { // Ren
of("PROXY_REQUEST_TIME_MS_KEY missing", missingTime));
}

@Test
@DisplayName("Should extract x-dd-proxy-resource-path header when present")
void testResourcePathHeaderExtraction() {
Map<String, String> headers = new HashMap<>();
headers.put(PROXY_SYSTEM_KEY, "aws-apigateway");
headers.put(PROXY_REQUEST_TIME_MS_KEY, "12345");
headers.put(PROXY_PATH_KEY, "/api/users/123");
headers.put(PROXY_RESOURCE_PATH_KEY, "/api/users/{id}");
headers.put(PROXY_HTTP_METHOD_KEY, "GET");
headers.put(PROXY_DOMAIN_NAME_KEY, "api.example.com");

Context context = this.propagator.extract(root(), headers, MAP_VISITOR);
InferredProxySpan inferredProxySpan = fromContext(context);
assertNotNull(inferredProxySpan);
assertTrue(inferredProxySpan.isValid());

// The resourcePath header should be extracted and available
// for use in http.route and resource.name
}

@Test
@DisplayName("Should work without x-dd-proxy-resource-path header for backwards compatibility")
void testExtractionWithoutResourcePath() {
Map<String, String> headers = new HashMap<>();
headers.put(PROXY_SYSTEM_KEY, "aws-apigateway");
headers.put(PROXY_REQUEST_TIME_MS_KEY, "12345");
headers.put(PROXY_PATH_KEY, "/api/users/123");
// No PROXY_RESOURCE_PATH_KEY
headers.put(PROXY_HTTP_METHOD_KEY, "GET");
headers.put(PROXY_DOMAIN_NAME_KEY, "api.example.com");

Context context = this.propagator.extract(root(), headers, MAP_VISITOR);
InferredProxySpan inferredProxySpan = fromContext(context);
assertNotNull(inferredProxySpan);
assertTrue(inferredProxySpan.isValid());

// Should still be valid without resourcePath (backwards compatibility)
}

@Test
@DisplayName("Should extract x-dd-proxy-resource-path for aws-httpapi")
void testResourcePathHeaderExtractionForAwsHttpApi() {
Map<String, String> headers = new HashMap<>();
headers.put(PROXY_SYSTEM_KEY, "aws-httpapi");
headers.put(PROXY_REQUEST_TIME_MS_KEY, "12345");
headers.put(PROXY_PATH_KEY, "/v2/items/abc-123");
headers.put(PROXY_RESOURCE_PATH_KEY, "/v2/items/{itemId}");
headers.put(PROXY_HTTP_METHOD_KEY, "POST");
headers.put(PROXY_DOMAIN_NAME_KEY, "httpapi.example.com");

Context context = this.propagator.extract(root(), headers, MAP_VISITOR);
InferredProxySpan inferredProxySpan = fromContext(context);
assertNotNull(inferredProxySpan);
assertTrue(inferredProxySpan.isValid());

// aws-httpapi should also support resourcePath extraction
}

@ParametersAreNonnullByDefault
private static class MapVisitor implements CarrierVisitor<Map<String, String>> {
@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import static datadog.trace.bootstrap.instrumentation.api.Tags.HTTP_METHOD;
import static datadog.trace.bootstrap.instrumentation.api.Tags.HTTP_ROUTE;
import static datadog.trace.bootstrap.instrumentation.api.Tags.HTTP_URL;
import static datadog.trace.bootstrap.instrumentation.api.Tags.SPAN_KIND;
import static datadog.trace.bootstrap.instrumentation.api.Tags.SPAN_KIND_SERVER;

import datadog.context.Context;
import datadog.context.ContextKey;
Expand All @@ -25,15 +27,21 @@ public class InferredProxySpan implements ImplicitContextKeyed {
static final String PROXY_SYSTEM = "x-dd-proxy";
static final String PROXY_START_TIME_MS = "x-dd-proxy-request-time-ms";
static final String PROXY_PATH = "x-dd-proxy-path";
static final String PROXY_RESOURCE_PATH = "x-dd-proxy-resource-path";
static final String PROXY_HTTP_METHOD = "x-dd-proxy-httpmethod";
static final String PROXY_DOMAIN_NAME = "x-dd-proxy-domain-name";
static final String STAGE = "x-dd-proxy-stage";
// Optional tags
static final String PROXY_ACCOUNT_ID = "x-dd-proxy-account-id";
static final String PROXY_API_ID = "x-dd-proxy-api-id";
static final String PROXY_REGION = "x-dd-proxy-region";
static final Map<String, String> SUPPORTED_PROXIES;
static final String INSTRUMENTATION_NAME = "inferred_proxy";

static {
SUPPORTED_PROXIES = new HashMap<>();
SUPPORTED_PROXIES.put("aws-apigateway", "aws.apigateway");
SUPPORTED_PROXIES.put("aws-httpapi", "aws.httpapi");
}

private final Map<String, String> headers;
Expand Down Expand Up @@ -75,6 +83,7 @@ public AgentSpanContext start(AgentSpanContext extracted) {
String proxy = SUPPORTED_PROXIES.get(proxySystem);
String httpMethod = header(PROXY_HTTP_METHOD);
String path = header(PROXY_PATH);
String resourcePath = header(PROXY_RESOURCE_PATH);
String domainName = header(PROXY_DOMAIN_NAME);

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

// Component: aws-apigateway
// Component: aws-apigateway or aws-httpapi
span.setTag(COMPONENT, proxySystem);

// Span kind: server
span.setTag(SPAN_KIND, SPAN_KIND_SERVER);

// SpanType: web
span.setTag(SPAN_TYPE, "web");

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

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Handle empty proxy domain when composing http.url

This now treats any non-null x-dd-proxy-domain-name as usable, so an empty header value produces http.url like https:///path instead of a valid path-only URL. The code already treats empty domain as missing for service-name fallback, and before this change an empty domain naturally yielded just path; this regression can generate malformed URL tags and hurt endpoint correlation/parsing when gateways forward blank domain headers.

Useful? React with 👍 / 👎.


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

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

// Optional tags - only set if present
String accountId = header(PROXY_ACCOUNT_ID);
if (accountId != null && !accountId.isEmpty()) {
span.setTag("account_id", accountId);
}

String apiId = header(PROXY_API_ID);
if (apiId != null && !apiId.isEmpty()) {
span.setTag("apiid", apiId);
}

String region = header(PROXY_REGION);
if (region != null && !region.isEmpty()) {
span.setTag("region", region);
}

// Compute and set dd_resource_key (ARN) if we have region and apiId
if (region != null && !region.isEmpty() && apiId != null && !apiId.isEmpty()) {
String arn = computeArn(proxySystem, region, apiId);
if (arn != null) {
span.setTag("dd_resource_key", arn);
}
}

// _dd.inferred_span = 1 (indicates that this is an inferred span)
span.setTag("_dd.inferred_span", 1);

// Resource Name: value of x-dd-proxy-httpmethod + " " + value of x-dd-proxy-path
// Resource Name: <Method> <Route> when route available, else <Method> <Path>
// Prefer x-dd-proxy-resource-path (route) over x-dd-proxy-path (path)
// Use MANUAL_INSTRUMENTATION priority to prevent TagInterceptor from overriding
String resourceName = httpMethod != null && path != null ? httpMethod + " " + path : null;
String routeOrPath = resourcePath != null ? resourcePath : path;
String resourceName =
httpMethod != null && routeOrPath != null ? httpMethod + " " + routeOrPath : null;
if (resourceName != null) {
span.setResourceName(resourceName, MANUAL_INSTRUMENTATION);
}
Expand All @@ -124,13 +163,67 @@ private String header(String name) {
return this.headers.get(name);
}

/**
* Compute ARN for the API Gateway resource. Format for v1 REST:
* arn:aws:apigateway:{region}::/restapis/{api-id} Format for v2 HTTP:
* arn:aws:apigateway:{region}::/apis/{api-id}
*/
private String computeArn(String proxySystem, String region, String apiId) {
if (proxySystem == null || region == null || apiId == null) {
return null;
}

// Assume AWS partition (could be extended to support other partitions like aws-cn, aws-us-gov)
String partition = "aws";

// Determine resource type based on proxy system
String resourceType;
if ("aws-apigateway".equals(proxySystem)) {
resourceType = "restapis"; // v1 REST API
} else if ("aws-httpapi".equals(proxySystem)) {
resourceType = "apis"; // v2 HTTP API
} else {
return null; // Unknown proxy type
}

return String.format("arn:%s:apigateway:%s::/%s/%s", partition, region, resourceType, apiId);
}

public void finish() {
if (this.span != null) {
// Copy AppSec tags from root span if needed (distributed tracing scenario)
copyAppSecTagsFromRoot();

this.span.finish();
this.span = null;
}
}

/**
* Copy AppSec tags from the root span to this inferred proxy span. This is needed when
* distributed tracing is active, because AppSec sets tags on the absolute root span (via
* setTagTop), but we need them on the inferred proxy span which may be a child of the upstream
* root span.
*/
private void copyAppSecTagsFromRoot() {
AgentSpan rootSpan = this.span.getLocalRootSpan();

// If root span is different from this span (distributed tracing case)
if (rootSpan != null && rootSpan != this.span) {
// Copy _dd.appsec.enabled metric (always 1 if present)
Comment on lines +212 to +213

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Copy AppSec tags without excluding local-root inferred spans

The new AppSec copy path is gated on rootSpan != this.span, but inferred proxy spans are created before the framework server span in HttpServerDecorator.startSpan, so for incoming distributed requests this inferred span is typically the local root and the condition is false. In the exact distributed-tracing scenario this change targets, _dd.appsec.enabled/_dd.appsec.json therefore never get copied, making the new propagation behavior ineffective.

Useful? React with 👍 / 👎.

Object appsecEnabled = rootSpan.getTag("_dd.appsec.enabled");
if (appsecEnabled != null) {
this.span.setMetric("_dd.appsec.enabled", 1);
}

// Copy _dd.appsec.json tag (AppSec events)
Object appsecJson = rootSpan.getTag("_dd.appsec.json");
if (appsecJson != null) {
this.span.setTag("_dd.appsec.json", appsecJson.toString());
}
}
}

@Override
public Context storeInto(@Nonnull Context context) {
return context.with(CONTEXT_KEY, this);
Expand Down
Loading
Loading