-
Notifications
You must be signed in to change notification settings - Fork 325
Fix AWS API Gateway endpoints correlation HTTP span tags - Inferred Proxy Spans #10561
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
fffbc5a
245c186
9e4cfe6
85a368c
d968d3a
c8a926f
c8bdd36
e1b1a6f
bb7503b
a1da74d
d1657be
06ba1cf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
|
|
@@ -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; | ||
|
|
@@ -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); | ||
|
|
@@ -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); | ||
|
|
||
| // 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); | ||
| } | ||
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The new AppSec copy path is gated on 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); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This now treats any non-null
x-dd-proxy-domain-nameas usable, so an empty header value produceshttp.urllikehttps:///pathinstead 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 justpath; this regression can generate malformed URL tags and hurt endpoint correlation/parsing when gateways forward blank domain headers.Useful? React with 👍 / 👎.