From 922775afaeff5cc2c1fc4b99b59e6c17f0582f5d Mon Sep 17 00:00:00 2001 From: "datadog-official[bot]" <214633350+datadog-official[bot]@users.noreply.github.com> Date: Tue, 20 Jan 2026 16:40:50 +0000 Subject: [PATCH] Handle HTTP2 netty server status Co-authored-by: dougqh --- .../HttpServerResponseTracingHandler.java | 32 +++++++++++++++- .../Netty41Http2StatusPropagationTest.groovy | 38 +++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 dd-java-agent/instrumentation/netty/netty-4.1/src/test/groovy/Netty41Http2StatusPropagationTest.groovy diff --git a/dd-java-agent/instrumentation/netty/netty-4.1/src/main/java/datadog/trace/instrumentation/netty41/server/HttpServerResponseTracingHandler.java b/dd-java-agent/instrumentation/netty/netty-4.1/src/main/java/datadog/trace/instrumentation/netty41/server/HttpServerResponseTracingHandler.java index 1cf31f91eae..32afc422e19 100644 --- a/dd-java-agent/instrumentation/netty/netty-4.1/src/main/java/datadog/trace/instrumentation/netty41/server/HttpServerResponseTracingHandler.java +++ b/dd-java-agent/instrumentation/netty/netty-4.1/src/main/java/datadog/trace/instrumentation/netty41/server/HttpServerResponseTracingHandler.java @@ -16,6 +16,8 @@ import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http2.Http2Headers; +import io.netty.handler.codec.http2.Http2HeadersFrame; @ChannelHandler.Sharable public class HttpServerResponseTracingHandler extends ChannelOutboundHandlerAdapter { @@ -26,12 +28,40 @@ public void write(final ChannelHandlerContext ctx, final Object msg, final Chann final Context storedContext = ctx.channel().attr(CONTEXT_ATTRIBUTE_KEY).get(); final AgentSpan span = spanFromContext(storedContext); - if (span == null || !(msg instanceof HttpResponse)) { + if (span == null + || !(msg instanceof HttpResponse || msg instanceof Http2HeadersFrame)) { ctx.write(msg, prm); return; } try (final ContextScope scope = storedContext.attach()) { + if (msg instanceof Http2HeadersFrame) { + final Http2HeadersFrame headersFrame = (Http2HeadersFrame) msg; + final Http2Headers headers = headersFrame.headers(); + try { + ctx.write(msg, prm); + } catch (final Throwable throwable) { + DECORATE.onError(span, throwable); + span.setHttpStatusCode(500); + span.finish(); + ctx.channel().attr(CONTEXT_ATTRIBUTE_KEY).remove(); + throw throwable; + } + + final CharSequence status = headers.status(); + if (status != null) { + try { + DECORATE.onResponseStatus(span, HttpResponseStatus.parseLine(status).code()); + } catch (IllegalArgumentException ignored) { + // ignore unparsable status and finish without setting http.status_code + } + } + DECORATE.beforeFinish(scope.context()); + span.finish(); + ctx.channel().attr(CONTEXT_ATTRIBUTE_KEY).remove(); + return; + } + final HttpResponse response = (HttpResponse) msg; try { diff --git a/dd-java-agent/instrumentation/netty/netty-4.1/src/test/groovy/Netty41Http2StatusPropagationTest.groovy b/dd-java-agent/instrumentation/netty/netty-4.1/src/test/groovy/Netty41Http2StatusPropagationTest.groovy new file mode 100644 index 00000000000..1b6e5bc490f --- /dev/null +++ b/dd-java-agent/instrumentation/netty/netty-4.1/src/test/groovy/Netty41Http2StatusPropagationTest.groovy @@ -0,0 +1,38 @@ +import datadog.context.Context +import datadog.trace.agent.test.InstrumentationSpecification +import datadog.trace.instrumentation.netty41.AttributeKeys +import datadog.trace.instrumentation.netty41.server.HttpServerResponseTracingHandler +import datadog.trace.instrumentation.netty41.server.NettyHttpServerDecorator +import io.netty.channel.embedded.EmbeddedChannel +import io.netty.handler.codec.http.DefaultHttpRequest +import io.netty.handler.codec.http.HttpMethod +import io.netty.handler.codec.http.HttpVersion +import io.netty.handler.codec.http2.DefaultHttp2Headers +import io.netty.handler.codec.http2.DefaultHttp2HeadersFrame + +class Netty41Http2StatusPropagationTest extends InstrumentationSpecification { + + def "http2 headers frame sets status on netty server span"() { + setup: + def channel = new EmbeddedChannel(HttpServerResponseTracingHandler.INSTANCE) + def request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/test") + def context = NettyHttpServerDecorator.DECORATE.startSpan(request, Context.root()) + channel.attr(AttributeKeys.CONTEXT_ATTRIBUTE_KEY).set(context) + + when: + def headers = new DefaultHttp2Headers().status("200") + channel.writeAndFlush(new DefaultHttp2HeadersFrame(headers, true)) + + then: + assertTraces(1) { + trace(1) { + span { + tags { "http.status_code" 200 } + } + } + } + + cleanup: + channel.finishAndReleaseAll() + } +} \ No newline at end of file