refactor: share an instrumentation emitter and harden body capture#144
Open
OmarAlJarrah wants to merge 1 commit into
Open
refactor: share an instrumentation emitter and harden body capture#144OmarAlJarrah wants to merge 1 commit into
OmarAlJarrah wants to merge 1 commit into
Conversation
The sync and async instrumentation steps duplicated ~200 lines of emit/redact/preview/metrics logic. Extract a shared InstrumentationEmitters that owns the event shape, header redaction, body wrapping, and metric instruments; both steps now delegate to it and differ only in how they thread the downstream call (synchronous process vs. a CompletableFuture continuation). While consolidating, fix two body-capture problems the shared path now handles once for both steps: - Stop the sync step from stalling time-to-first-byte. The async step already skipped body capture for unknown-length (contentLength() < 0) response bodies because the bounded drain runs on the completion thread and would block on a slow producer; the sync step had no such guard and drained eagerly on the caller's thread, so an SSE / long-poll / chunked-trickle endpoint logged at BODY_AND_HEADERS did not return the response until the preview cap filled or the producer hit EOF. The shared wrapResponseForLogging applies the contentLength() < 0 skip to both steps, so unknown-length bodies stream to the caller unwrapped with no preview. - Make the true body size observable. The *.body.size fields are derived from the capped preview snapshot, so they saturate at bodyPreviewMaxBytes and a dashboard cannot tell an 8 KiB body from an 8 GB one. The events now also carry *.body.actual_size (the full length, emitted when known) and *.body.preview_truncated (true when the preview is only a prefix). Existing field names and metric/event names are unchanged. Docs and the HttpInstrumentationOptions KDoc are updated to describe the new fields and the symmetric streaming-body skip. Closes #26 Closes #107 Closes #108
Member
Author
|
This cleanly factors the duplicated emit/redact/preview/metrics logic out of the two instrumentation steps into Issues
Worth double-checking
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The synchronous (
DefaultInstrumentationStep) and asynchronous(
DefaultAsyncInstrumentationStep) instrumentation steps duplicated ~200 lines ofemit / redact / preview / metrics logic, with a standing TODO to deduplicate. The
duplication also let two body-capture problems drift between the two steps:
Time-to-first-byte stall in the sync step. The async step deliberately skips
body capture for an unknown-length (
contentLength() < 0) response body, becausethe bounded drain runs on the future-completion thread and would block on a slow /
idle producer. The sync step had no such guard: under
BODY_AND_HEADERSit drainedeagerly on the caller's thread, so a streaming endpoint (SSE, long-poll, chunked
trickle) did not return the response until the preview cap filled or the producer
reached EOF — turning time-to-first-byte into "however long the first
bodyPreviewMaxBytestake to arrive."Preview-capped body size only. The
request.body.size/response.body.sizefields are derived from the capped preview snapshot, so they report
min(actualSize, bodyPreviewMaxBytes)and saturate at the cap. A dashboard keyed on*.body.sizecannot tell an 8 KiB body from an 8 GB one.Change
InstrumentationEmitters, an internal class that owns the shared event shape,header redaction, body wrapping, metric instruments, and URL redaction. Both steps
delegate to it and now differ only in how they thread the downstream call (synchronous
processvs. aCompletableFuturecontinuation).wrapResponseForLoggingapplies thecontentLength() < 0skip for both steps, soan unknown-length body streams to the caller unwrapped with no preview. The
http.responseevent still carries headers, status, andresponse.content.length = -1.*.body.actual_size(the true length,emitted when known) and
*.body.preview_truncated(truewhen the preview is only aprefix of a larger body). Response-body truncation is derived from the existing capture
via a new internal
LoggableResponseBody.isFullyCapturedseam, without re-reading thebody.
http.client.request.count,http.client.request.duration),event names (
http.request,http.response), and field names are unchanged.HttpInstrumentationOptionsKDoc anddocs/http-body-logging-and-concurrency.mdto document the new fields and the now-symmetricstreaming-body skip.
Rationale
Consolidating the two steps removes the source of drift and gives a single home for the
charset-aware preview and size logic. Skipping the drain for unknown-length bodies keeps
time-to-first-byte gated by network transfer rather than producer pacing, matching the
async step's existing behaviour. The additive size fields make the true body size
observable without breaking dashboards that read the existing
*.body.sizefield.API impact
No public-API change. The new
isFullyCapturedaccessor isinternal/@JvmSynthetic;apiCheckpasses with no regenerated snapshots.Gated build commands run (all passed)
./gradlew :sdk-core:test :sdk-core:ktlintCheck :sdk-core:detekt :sdk-core:apiCheck --no-daemon— BUILD SUCCESSFUL./gradlew :sdk-core:test :sdk-core:ktlintCheck :sdk-core:detekt --no-daemon(after adding the async size test) — BUILD SUCCESSFULCloses #26
Closes #107
Closes #108