22
33import static datadog .context .ContextKey .named ;
44import static datadog .trace .api .DDTags .SPAN_TYPE ;
5+ import static datadog .trace .bootstrap .instrumentation .api .ErrorPriorities .HTTP_SERVER_DECORATOR ;
56import static datadog .trace .bootstrap .instrumentation .api .ResourceNamePriorities .MANUAL_INSTRUMENTATION ;
67import static datadog .trace .bootstrap .instrumentation .api .Tags .COMPONENT ;
78import static datadog .trace .bootstrap .instrumentation .api .Tags .HTTP_METHOD ;
89import static datadog .trace .bootstrap .instrumentation .api .Tags .HTTP_ROUTE ;
910import static datadog .trace .bootstrap .instrumentation .api .Tags .HTTP_URL ;
11+ import static datadog .trace .bootstrap .instrumentation .api .Tags .HTTP_USER_AGENT ;
1012import static datadog .trace .bootstrap .instrumentation .api .Tags .SPAN_KIND ;
1113import static datadog .trace .bootstrap .instrumentation .api .Tags .SPAN_KIND_SERVER ;
1214
@@ -46,6 +48,10 @@ public class InferredProxySpan implements ImplicitContextKeyed {
4648
4749 private final Map <String , String > headers ;
4850 private AgentSpan span ;
51+ // Service-entry span registered at startSpan() time; used to guard against premature finishing
52+ // by child spans (e.g., Spring MVC handler spans) before the response status is known.
53+ private AgentSpan registeredServiceEntrySpan ;
54+ private boolean phasedFinished ;
4955
5056 public static InferredProxySpan fromHeaders (Map <String , String > values ) {
5157 return new InferredProxySpan (values );
@@ -191,31 +197,75 @@ private String computeArn(String proxySystem, String region, String apiId) {
191197 return String .format ("arn:%s:apigateway:%s::/%s/%s" , partition , region , resourceType , apiId );
192198 }
193199
200+ /**
201+ * Registers the service-entry span for this inferred proxy span. This allows {@link
202+ * #finish(AgentSpan)} to distinguish between premature finish calls from child handler spans
203+ * (e.g., Spring MVC) and the final finish call from the service-entry span after the response is
204+ * written.
205+ */
206+ public void registerServiceEntrySpan (AgentSpan serviceEntrySpan ) {
207+ this .registeredServiceEntrySpan = serviceEntrySpan ;
208+ }
209+
194210 public void finish () {
195211 finish (null );
196212 }
197213
198214 /**
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.
215+ * Finishes this inferred proxy span, copying relevant tags from the given span.
202216 *
203- * @param serviceEntrySpan the service-entry child span, or null if not available
217+ * <p>When a service-entry span is registered (via {@link #registerServiceEntrySpan}), this method
218+ * distinguishes between two callers:
219+ *
220+ * <ul>
221+ * <li><b>Non-service-entry caller</b> (e.g., Spring MVC handler span): copies available tags
222+ * (AppSec) and calls {@link AgentSpan#phasedFinish()} to record duration without
223+ * publishing. The span stays alive so HTTP tags can be added later.
224+ * <li><b>Service-entry caller</b>: copies all tags including HTTP status/error/useragent, then
225+ * publishes the span (via {@link AgentSpan#publish()} if phasedFinished, or {@link
226+ * AgentSpan#finish()} otherwise).
227+ * </ul>
228+ *
229+ * @param callerSpan the span calling finish, used to copy tags and determine caller type
204230 */
205- public void finish (AgentSpan serviceEntrySpan ) {
206- if (this .span != null ) {
207- copyAppSecTagsFromServiceEntry (serviceEntrySpan );
208- this .span .finish ();
231+ public void finish (AgentSpan callerSpan ) {
232+ if (this .span == null ) {
233+ return ;
234+ }
235+
236+ boolean isServiceEntryOrFallback =
237+ registeredServiceEntrySpan == null
238+ || callerSpan == null
239+ || callerSpan == registeredServiceEntrySpan ;
240+
241+ if (isServiceEntryOrFallback ) {
242+ // Final call: copy all tags (AppSec + HTTP status/error/useragent) and close the span
243+ copyTagsFromServiceEntry (callerSpan );
244+ if (phasedFinished ) {
245+ this .span .publish ();
246+ } else {
247+ this .span .finish ();
248+ }
209249 this .span = null ;
250+ this .registeredServiceEntrySpan = null ;
251+ this .phasedFinished = false ;
252+ } else if (!phasedFinished ) {
253+ // First non-service-entry call (e.g., Spring MVC handler span fires beforeFinish() before
254+ // the response is written): copy available tags (AppSec) and phase-finish to record
255+ // duration, but keep the span alive so the service-entry call can add HTTP tags later.
256+ copyTagsFromServiceEntry (callerSpan );
257+ this .span .phasedFinish ();
258+ this .phasedFinished = true ;
210259 }
260+ // If already phasedFinished and caller is not service-entry: ignore duplicate calls
211261 }
212262
213263 /**
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 .
264+ * Copies relevant tags from the service-entry span to this inferred proxy span. This includes
265+ * AppSec tags required by RFC-1081, plus HTTP tags that are only known after the request
266+ * completes ({@code http.status_code}, {@code error}, {@code http.useragent}) .
217267 */
218- private void copyAppSecTagsFromServiceEntry (AgentSpan serviceEntrySpan ) {
268+ private void copyTagsFromServiceEntry (AgentSpan serviceEntrySpan ) {
219269 if (serviceEntrySpan == null || serviceEntrySpan == this .span ) {
220270 return ;
221271 }
@@ -229,6 +279,18 @@ private void copyAppSecTagsFromServiceEntry(AgentSpan serviceEntrySpan) {
229279 if (appsecJson != null ) {
230280 this .span .setTag ("_dd.appsec.json" , appsecJson .toString ());
231281 }
282+
283+ short statusCode = serviceEntrySpan .getHttpStatusCode ();
284+ if (statusCode > 0 ) {
285+ this .span .setHttpStatusCode (statusCode );
286+ boolean isError = Config .get ().getHttpServerErrorStatuses ().get (statusCode );
287+ this .span .setError (isError , HTTP_SERVER_DECORATOR );
288+ }
289+
290+ Object userAgent = serviceEntrySpan .getTag (HTTP_USER_AGENT );
291+ if (userAgent != null ) {
292+ this .span .setTag (HTTP_USER_AGENT , userAgent .toString ());
293+ }
232294 }
233295
234296 @ Override
0 commit comments