22
33import static datadog .trace .bootstrap .instrumentation .api .Java8BytecodeBridge .spanFromContext ;
44import static datadog .trace .instrumentation .netty41 .AttributeKeys .CONTEXT_ATTRIBUTE_KEY ;
5+ import static datadog .trace .instrumentation .netty41 .AttributeKeys .STREAMING_CONTEXT_KEY ;
56import static datadog .trace .instrumentation .netty41 .AttributeKeys .WEBSOCKET_SENDER_HANDLER_CONTEXT ;
67import static datadog .trace .instrumentation .netty41 .server .NettyHttpServerDecorator .DECORATE ;
78
1314import io .netty .channel .ChannelHandlerContext ;
1415import io .netty .channel .ChannelOutboundHandlerAdapter ;
1516import io .netty .channel .ChannelPromise ;
17+ import io .netty .handler .codec .http .FullHttpResponse ;
1618import io .netty .handler .codec .http .HttpHeaderNames ;
1719import io .netty .handler .codec .http .HttpResponse ;
1820import io .netty .handler .codec .http .HttpResponseStatus ;
21+ import io .netty .handler .codec .http .LastHttpContent ;
1922
2023@ ChannelHandler .Sharable
2124public class HttpServerResponseTracingHandler extends ChannelOutboundHandlerAdapter {
@@ -26,36 +29,176 @@ public void write(final ChannelHandlerContext ctx, final Object msg, final Chann
2629 final Context storedContext = ctx .channel ().attr (CONTEXT_ATTRIBUTE_KEY ).get ();
2730 final AgentSpan span = spanFromContext (storedContext );
2831
29- if (span == null || !(msg instanceof HttpResponse )) {
30- ctx .write (msg , prm );
32+ // FullHttpResponse must be checked BEFORE LastHttpContent and HttpResponse,
33+ // because FullHttpResponse extends both LastHttpContent and HttpResponse.
34+ if (msg instanceof FullHttpResponse ) {
35+ handleFullHttpResponse (ctx , storedContext , span , (FullHttpResponse ) msg , prm );
3136 return ;
3237 }
3338
34- try (final ContextScope scope = storedContext .attach ()) {
35- final HttpResponse response = (HttpResponse ) msg ;
39+ // Handle HttpResponse (headers only — start of chunked/streaming response).
40+ // Must be checked BEFORE LastHttpContent/HttpContent.
41+ if (msg instanceof HttpResponse ) {
42+ handleHttpResponse (ctx , storedContext , span , (HttpResponse ) msg , prm );
43+ return ;
44+ }
45+
46+ // Handle LastHttpContent (end of chunked/streaming response).
47+ // Must be checked BEFORE HttpContent (LastHttpContent extends HttpContent).
48+ // IMPORTANT: Use STREAMING_CONTEXT_KEY to avoid keep-alive race condition where
49+ // channelRead for the next request may overwrite CONTEXT_ATTRIBUTE_KEY before
50+ // this LastHttpContent write task runs on the EventLoop.
51+ if (msg instanceof LastHttpContent ) {
52+ Context streamingContext = ctx .channel ().attr (STREAMING_CONTEXT_KEY ).getAndRemove ();
53+ Context contextForLastContent = streamingContext != null ? streamingContext : storedContext ;
54+ AgentSpan spanForLastContent =
55+ streamingContext != null ? spanFromContext (streamingContext ) : span ;
56+ handleLastHttpContent (
57+ ctx , contextForLastContent , spanForLastContent , (LastHttpContent ) msg , prm );
58+ return ;
59+ }
60+
61+ // Intermediate HttpContent chunks — pass through without touching the span.
62+ ctx .write (msg , prm );
63+ }
3664
65+ /** Complete response in a single message (non-streaming). Finish span immediately. */
66+ private void handleFullHttpResponse (
67+ final ChannelHandlerContext ctx ,
68+ final Context storedContext ,
69+ final AgentSpan span ,
70+ final FullHttpResponse response ,
71+ final ChannelPromise prm ) {
72+
73+ if (span == null ) {
74+ ctx .write (response , prm );
75+ return ;
76+ }
77+
78+ try (final ContextScope scope = storedContext .attach ()) {
3779 try {
38- ctx .write (msg , prm );
80+ ctx .write (response , prm );
3981 } catch (final Throwable throwable ) {
4082 DECORATE .onError (span , throwable );
4183 span .setHttpStatusCode (500 );
42- span .finish (); // Finish the span manually since finishSpanOnClose was false
84+ span .finish ();
4385 ctx .channel ().attr (CONTEXT_ATTRIBUTE_KEY ).remove ();
4486 throw throwable ;
4587 }
88+
4689 final boolean isWebsocketUpgrade =
4790 response .status () == HttpResponseStatus .SWITCHING_PROTOCOLS
4891 && "websocket" .equals (response .headers ().get (HttpHeaderNames .UPGRADE ));
92+
4993 if (isWebsocketUpgrade ) {
5094 ctx .channel ()
5195 .attr (WEBSOCKET_SENDER_HANDLER_CONTEXT )
5296 .set (new HandlerContext .Sender (span , ctx .channel ().id ().asShortText ()));
5397 }
98+
5499 if (response .status () != HttpResponseStatus .CONTINUE
55100 && (response .status () != HttpResponseStatus .SWITCHING_PROTOCOLS || isWebsocketUpgrade )) {
56101 DECORATE .onResponse (span , response );
57102 DECORATE .beforeFinish (scope .context ());
58- span .finish (); // Finish the span manually since finishSpanOnClose was false
103+ span .finish ();
104+ ctx .channel ().attr (CONTEXT_ATTRIBUTE_KEY ).remove ();
105+ }
106+ }
107+ }
108+
109+ /**
110+ * Chunked response headers — record status but do NOT finish the span yet. The span will be
111+ * finished when the corresponding LastHttpContent is written. Context is saved to
112+ * STREAMING_CONTEXT_KEY so that a keep-alive channelRead for the next request cannot overwrite it
113+ * before LastHttpContent arrives.
114+ */
115+ private void handleHttpResponse (
116+ final ChannelHandlerContext ctx ,
117+ final Context storedContext ,
118+ final AgentSpan span ,
119+ final HttpResponse response ,
120+ final ChannelPromise prm ) {
121+
122+ if (span == null ) {
123+ ctx .write (response , prm );
124+ return ;
125+ }
126+
127+ try (final ContextScope scope = storedContext .attach ()) {
128+ try {
129+ ctx .write (response , prm );
130+ } catch (final Throwable throwable ) {
131+ DECORATE .onError (span , throwable );
132+ span .setHttpStatusCode (500 );
133+ span .finish ();
134+ ctx .channel ().attr (CONTEXT_ATTRIBUTE_KEY ).remove ();
135+ throw throwable ;
136+ }
137+
138+ final boolean isWebsocketUpgrade =
139+ response .status () == HttpResponseStatus .SWITCHING_PROTOCOLS
140+ && "websocket" .equals (response .headers ().get (HttpHeaderNames .UPGRADE ));
141+
142+ if (isWebsocketUpgrade ) {
143+ ctx .channel ()
144+ .attr (WEBSOCKET_SENDER_HANDLER_CONTEXT )
145+ .set (new HandlerContext .Sender (span , ctx .channel ().id ().asShortText ()));
146+ }
147+
148+ if (response .status () != HttpResponseStatus .CONTINUE
149+ && (response .status () != HttpResponseStatus .SWITCHING_PROTOCOLS || isWebsocketUpgrade )) {
150+ DECORATE .onResponse (span , response );
151+
152+ int statusCode = response .status ().code ();
153+ boolean isBodyless = statusCode == 204 || statusCode == 205 || statusCode == 304 ;
154+
155+ if (isWebsocketUpgrade || isBodyless ) {
156+ // WebSocket upgrades and bodyless responses (204, 205, 304) don't produce
157+ // LastHttpContent — finish span immediately.
158+ DECORATE .beforeFinish (scope .context ());
159+ span .finish ();
160+ ctx .channel ().attr (CONTEXT_ATTRIBUTE_KEY ).remove ();
161+ } else {
162+ ctx .channel ().attr (STREAMING_CONTEXT_KEY ).set (storedContext );
163+ ctx .channel ().attr (CONTEXT_ATTRIBUTE_KEY ).remove ();
164+ // Span finish is deferred to handleLastHttpContent.
165+ }
166+ }
167+ }
168+ }
169+
170+ /** End of chunked/streaming response — finish the span now that the full duration is known. */
171+ private void handleLastHttpContent (
172+ final ChannelHandlerContext ctx ,
173+ final Context storedContext ,
174+ final AgentSpan span ,
175+ final LastHttpContent msg ,
176+ final ChannelPromise prm ) {
177+
178+ if (span == null ) {
179+ ctx .write (msg , prm );
180+ return ;
181+ }
182+
183+ try (final ContextScope scope = storedContext .attach ()) {
184+ try {
185+ ctx .write (msg , prm );
186+ } catch (final Throwable throwable ) {
187+ DECORATE .onError (span , throwable );
188+ span .setHttpStatusCode (500 );
189+ span .finish ();
190+ if (ctx .channel ().attr (CONTEXT_ATTRIBUTE_KEY ).get () == storedContext ) {
191+ ctx .channel ().attr (CONTEXT_ATTRIBUTE_KEY ).remove ();
192+ }
193+ throw throwable ;
194+ }
195+
196+ DECORATE .beforeFinish (scope .context ());
197+ span .finish ();
198+ // Only remove CONTEXT_ATTRIBUTE_KEY if it still holds our context.
199+ // Under keep-alive a new request's channelRead may have already replaced it.
200+ // All channel ops run on the same EventLoop thread so this check is race-free.
201+ if (ctx .channel ().attr (CONTEXT_ATTRIBUTE_KEY ).get () == storedContext ) {
59202 ctx .channel ().attr (CONTEXT_ATTRIBUTE_KEY ).remove ();
60203 }
61204 }
0 commit comments