Title: Fix Netty HTTP span lifecycle for chunked/streaming responses by gtukmachev · Pull Request #1 · GoodNotes/dd-trace-java · GitHub
Open Graph Title: Fix Netty HTTP span lifecycle for chunked/streaming responses by gtukmachev · Pull Request #1 · GoodNotes/dd-trace-java
X Title: Fix Netty HTTP span lifecycle for chunked/streaming responses by gtukmachev · Pull Request #1 · GoodNotes/dd-trace-java
Description: Problem
Applications using chunked HTTP responses (e.g. Ktor's respondOutputStream) report near-zero latency in APM — the span closes when response headers are sent, not when the stream finishes.
// Ktor — respondOutputStream sends headers immediately, streams body async
call.respondOutputStream {
repeat(5) { writeSomeChunk(); delay(1000) } // 5s total — DD reports ~0ms
}
This affects any Netty-backed server that sends HttpResponse + multiple HttpContent + LastHttpContent separately (i.e. chunked transfer encoding).
Root Cause
Bug 1 — span always closed on HttpResponse, no LastHttpContent handling
The original handler had a single dispatch branch. Every HttpResponse (headers-only or full) immediately finished the span. LastHttpContent — the actual end of a chunked stream — was never inspected; it fell through to ctx.write(msg, prm) silently.
// Before: only one branch, span always closed at header-send time
if (span == null || !(msg instanceof HttpResponse)) {
ctx.write(msg, prm); // LastHttpContent passes through here — span untouched
return;
}
// ... span.finish() always called here, even for chunked headers
The fix adds explicit routing for all four Netty message types. FullHttpResponse must be checked first because it extends both LastHttpContent and HttpResponse; without that ordering it would be caught by the wrong branch.
// After: three explicit branches; everything else (chunks, unrelated messages) falls through
if (msg instanceof FullHttpResponse) { handleFullHttpResponse(...); return; } // finish immediately
if (msg instanceof HttpResponse) { handleHttpResponse(...); return; } // headers only — don't finish
if (msg instanceof LastHttpContent) { handleLastHttpContent(...); return; } // finish here
ctx.write(msg, prm); // intermediate HttpContent chunks + unrelated messages — pass through
Bug 2 — keep-alive race condition
Under HTTP keep-alive, Netty's event loop can process channelRead for the next request (overwriting CONTEXT_ATTRIBUTE_KEY with the new span) before the pending write task for the previous response's LastHttpContent runs. Result: handleLastHttpContent finishes the new request's span with ~1-chunk duration.
Fix: a dedicated STREAMING_CONTEXT_KEY channel attribute, set when chunked headers are sent and read (then cleared) by LastHttpContent — immune to overwrite by the next request.
// AttributeKeys.java
public static final AttributeKey
Open Graph Description: Problem Applications using chunked HTTP responses (e.g. Ktor's respondOutputStream) report near-zero latency in APM — the span closes when response headers are sent, not when the stream finishe...
X Description: Problem Applications using chunked HTTP responses (e.g. Ktor's respondOutputStream) report near-zero latency in APM — the span closes when response headers are sent, not when the stream fin...
Opengraph URL: https://github.com/GoodNotes/dd-trace-java/pull/1
X: @github
Domain: github.com
| route-pattern | /:user_id/:repository/pull/:id/files(.:format) |
| route-controller | pull_requests |
| route-action | files |
| fetch-nonce | v2:415ead19-401b-f349-9215-cf16a6a98207 |
| current-catalog-service-hash | ae870bc5e265a340912cde392f23dad3671a0a881730ffdadd82f2f57d81641b |
| request-id | 8402:3A8185:6C2F71:8FADAC:6A5BDC2E |
| html-safe-nonce | aff1166a03e1ea74b0445b22537da69eb916008a7c319c1650763fe2f93e2d97 |
| visitor-payload | eyJyZWZlcnJlciI6IiIsInJlcXVlc3RfaWQiOiI4NDAyOjNBODE4NTo2QzJGNzE6OEZBREFDOjZBNUJEQzJFIiwidmlzaXRvcl9pZCI6IjIzMzIxNTE5ODc3ODg1Njk2NDYiLCJyZWdpb25fZWRnZSI6ImlhZCIsInJlZ2lvbl9yZW5kZXIiOiJpYWQifQ== |
| visitor-hmac | 0d5c17328a4e4a3bd1bf186c54532c2234bdf54f9cf93315717f5976f4bb6810 |
| hovercard-subject-tag | pull_request:3321056987 |
| github-keyboard-shortcuts | repository,pull-request-list,pull-request-conversation,pull-request-files-changed,copilot |
| google-site-verification | Apib7-x98H0j5cPqHWwSMm6dNU4GmODRoqxLiDzdx9I |
| octolytics-url | https://collector.github.com/github/collect |
| analytics-location | / |
| fb:app_id | 1401488693436528 |
| apple-itunes-app | app-id=1477376905, app-argument=https://github.com/GoodNotes/dd-trace-java/pull/1/files |
| twitter:image | https://avatars.githubusercontent.com/u/8056762?s=400&v=4 |
| twitter:card | summary_large_image |
| og:image | https://avatars.githubusercontent.com/u/8056762?s=400&v=4 |
| og:image:alt | Problem Applications using chunked HTTP responses (e.g. Ktor's respondOutputStream) report near-zero latency in APM — the span closes when response headers are sent, not when the stream finishe... |
| og:site_name | GitHub |
| og:type | object |
| hostname | github.com |
| expected-hostname | github.com |
| None | 5290d7e14309ad1e76106a9c4237bd1041517e83ea182c8ab756752cb0c6940b |
| turbo-cache-control | no-preview |
| diff-view | unified |
| go-import | github.com/GoodNotes/dd-trace-java git https://github.com/GoodNotes/dd-trace-java.git |
| octolytics-dimension-user_id | 20379375 |
| octolytics-dimension-user_login | GoodNotes |
| octolytics-dimension-repository_id | 1165766701 |
| octolytics-dimension-repository_nwo | GoodNotes/dd-trace-java |
| octolytics-dimension-repository_public | true |
| octolytics-dimension-repository_is_fork | true |
| octolytics-dimension-repository_parent_id | 89221572 |
| octolytics-dimension-repository_parent_nwo | DataDog/dd-trace-java |
| octolytics-dimension-repository_network_root_id | 89221572 |
| octolytics-dimension-repository_network_root_nwo | DataDog/dd-trace-java |
| turbo-body-classes | logged-out env-production page-responsive full-width |
| disable-turbo | true |
| browser-stats-url | https://api.github.com/_private/browser/stats |
| browser-errors-url | https://api.github.com/_private/browser/errors |
| release | 9c975978430e9ad293956f2bbdaf153b1bd84a99 |
| ui-target | full |
| theme-color | #1e2327 |
| color-scheme | light dark |
Links:
Viewport: width=device-width