77import static datadog .trace .bootstrap .instrumentation .api .Tags .HTTP_METHOD ;
88import static datadog .trace .bootstrap .instrumentation .api .Tags .HTTP_ROUTE ;
99import 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
1113import datadog .context .Context ;
1214import 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