Skip to content

Test evidence

Generated by the Docs workflow from the Surefire results of the test run and the rationale comments in the test sources. Do not edit by hand.

The run executed 363 tests (0 failures, 0 errors, 0 skipped) in 34.2s across both modules, against a real HTTP peer (the JDK's own HTTP server) but without Docker or any external service - see CONTRIBUTING.

Module Component under test Tests Time
legatium-common CountingCorrelationIdGeneratorTest 17 0.1s
legatium-common TraceparentFuzzTest 15 0.0s
legatium-common ClientLoggingMetricsTest 11 0.1s
legatium-common ClientLoggingPropertiesTest 11 0.4s
legatium-common HeaderMaskingFuzzTest 11 0.0s
legatium-common ClientLogFieldTest 8 0.1s
legatium-common HeaderLogPropertiesTest 8 0.0s
legatium-common SharedContractTest 5 0.0s
legatium-common TimeoutsTest 5 0.1s
legatium-common HeaderValueMaskerTest 4 0.1s
legatium-common MdcScopeTest 4 0.0s
legatium-common BodyLogModeTest 3 0.0s
legatium-common ClientLoggingReferenceConfigTest 3 0.9s
legatium-common FailOpenDiagnosticsTest 3 0.0s
legatium-common TraceparentTest 3 0.0s
legatium-common CorrelationHeaderTest 2 0.0s
legatium-restclient-logging ClientRequestLoggingInterceptorTest 31 0.1s
legatium-restclient-logging ClientRequestLoggingInterceptorBodyAndHeaderTest 24 0.2s
legatium-restclient-logging ClientRequestLoggingMetricsTest 14 0.1s
legatium-restclient-logging BoundedBodyCaptureFuzzTest 10 0.0s
legatium-restclient-logging BoundedBodyCaptureTest 10 0.0s
legatium-restclient-logging ClientLoggingAutoConfigurationTest 9 0.2s
legatium-restclient-logging ClientRequestLoggingInterceptorIntegrationTest 8 0.3s
legatium-restclient-logging HttpComponentsRequestFactoryIntegrationTest 6 0.5s
legatium-restclient-logging JdkClientRequestFactoryIntegrationTest 6 0.6s
legatium-restclient-logging JettyRequestFactoryIntegrationTest 6 1.0s
legatium-restclient-logging ReactorNettyRequestFactoryIntegrationTest 6 1.1s
legatium-restclient-logging SimpleRequestFactoryIntegrationTest 6 0.4s
legatium-restclient-logging ClientRequestLoggingTracingIntegrationTest 2 1.3s
legatium-restclient-logging TwinContractTest 2 0.0s
legatium-restclient-logging UriTemplateAttributeTest 2 0.0s
legatium-webclient-logging ClientRequestLoggingFilterTest 30 0.1s
legatium-webclient-logging ClientRequestLoggingFilterBodyAndHeaderTest 22 0.2s
legatium-webclient-logging ClientRequestLoggingMetricsTest 10 0.0s
legatium-webclient-logging BoundedBodyCaptureTest 9 0.0s
legatium-webclient-logging ClientLoggingAutoConfigurationTest 9 0.2s
legatium-webclient-logging ClientRequestLoggingFilterIntegrationTest 6 0.5s
legatium-webclient-logging HttpComponentsConnectorIntegrationTest 4 2.2s
legatium-webclient-logging JdkHttpClientConnectorIntegrationTest 4 0.7s
legatium-webclient-logging JettyConnectorIntegrationTest 4 0.8s
legatium-webclient-logging ReactorNettyConnectorIntegrationTest 4 0.4s
legatium-webclient-logging ClientRequestLoggingTracingIntegrationTest 2 1.5s
legatium-webclient-logging TwinContractTest 2 0.0s
legatium-webclient-logging UriTemplateAttributeTest 2 0.0s

legatium-common

113 tests.

BodyLogModeTest

3 tests.

should capture in every mode but never

Rationale

What is tested? the captures half of the truth table - whether a bounded capture is installed at all.

How is success determined? false for NEVER, true for ON_FAILURE and ALWAYS - on-failure pays the capture up front.

Why does it matter? the request body flows before the outcome is known; a mode that logs but does not capture would have nothing to write when the exchange fails.

should log always and never regardless of the outcome

Rationale

What is tested? the two unconditional branches of logs - the outcome argument is ignored.

How is success determined? ALWAYS is true and NEVER is false for both a failed and a successful exchange.

Why does it matter? NEVER is the default; a body appearing on a failed exchange under it would be a leak, and ALWAYS dropping a success would silently halve the volume an operator asked for.

should log on failure only when the exchange failed

Rationale

What is tested? the one decision the emitters delegate - on-failure discards a success.

How is success determined? true for a failed exchange (outcome not success, or a 4xx), false otherwise.

Why does it matter? this single predicate is the volume switch of ADR-0006.

ClientLogFieldTest

8 tests.

Declared shape

should accept exactly the JVM type each field declares

Rationale

What is tested? the shape each field declares for the index template - what the lockstep test maps and what the emitters pass.

How is success determined? a value of the declared type is accepted, a value of another type (an Int where the mapping says long) or null is not.

Why does it matter? the declared shape is what the component template maps; the emitters are the only writers, so the declaration is a pin, not a runtime gate. Given/When/Then

The component template

should be a component template, claiming no indices of its own

Rationale

What is tested? the top-level shape of the shipped JSON - a component template, not an index template.

How is success determined? the root carries only template and _meta; in particular no index_patterns.

Why does it matter? an index_patterns entry would compete with the host's own template on priority and claim data streams the host never meant to hand to this module. Given /

should keep every payload field out of the index

Rationale

What is tested? the mapping half of the sensitivity rule - headers and bodies must not be searchable, no matter how the rest of the template changes.

How is success determined? index false AND doc_values false asserted explicitly, not via the field set - a field silently re-typed to a searchable keyword would still pass the set check.

Why does it matter? selection and masking in code are the real protection; the mapping is the second line, so a value that slips through cannot at least be searched for deliberately.

should keep the high-cardinality URL fields out of doc values but leave them searchable

Rationale

What is tested? the repetition-factor split of the path pair - the decision an unsuspecting edit is most likely to undo ("why is url_path not aggregatable? let me fix it").

How is success determined? path and query have doc_values off but stay indexed; the template half keeps its doc values as the aggregation counterpart.

Why does it matter? the resolved path appears in about one line each - doc values on it grow an ordinal dictionary to the document count and buy only singleton buckets, while adapter_url_template is the field that answers "which endpoint is slow". Given / When /

should map exactly the fields this module emits

Rationale

What is tested? that the template and the enum describe the same field set.

How is success determined? set equality - it fails in BOTH directions, a field added to the enum without a mapping AND a mapping left behind for a removed field.

Why does it matter? an unmapped field is not an error at index time - Elasticsearch maps it dynamically, and for a body or a header that means the value becomes SEARCHABLE, the one outcome the mapping guide's sensitivity rule forbids.

should map the numeric and boolean shapes the code guarantees

Rationale

What is tested? the type half of the lockstep - the three non-keyword fields against the JVM type the enum declares (Long duration, Int status, Boolean slow flag).

How is success determined? duration maps as long, the status code as short, the slow flag as boolean.

Why does it matter? a keyword duration cannot be ranged or percentiled, and a status mapped as a number that is summed reads as garbage - the index type is what makes the dashboards work. Given / When /

Wire names

should be the literal strings the component template maps

Rationale

What is tested? every wire name, spelled out as a literal - independently of the enum, so a rename cannot pass by asserting a value against itself.

How is success determined? all thirteen names match exactly.

Why does it matter? once the template is composed into a pipeline, changing a name is a breaking change for every dashboard and alert keying on it - the compiler cannot see that. Given/When/

should prefix every wire name with adapter and keep them unique

Rationale

What is tested? the naming contract of the whole family in one place.

How is success determined? every wire name starts with 'adapter_', is lower snake_case, and no two fields collide.

Why does it matter? the names are index-side contract - a stray prefix or a duplicate silently splits one logical field into two that no dashboard knows about. Given/

ClientLoggingMetricsTest

11 tests.

should be a no-op against an empty composite registry

Rationale

What is tested? the owner against the registry the auto-configurations hand it when the host has none - an empty CompositeMeterRegistry, whose meters are Micrometer no-ops.

How is success determined? construction and every recording path succeed, nothing is warned, and the composite holds the registered ids but no child accumulates a value.

Why does it matter? a host without actuator must run the module unchanged - with no private registry quietly accumulating per-template meters nobody ever reads. Given

should count a throwing host counter as stage wiring instead of throwing

Rationale

What is tested? updateQuietly around the correlation and events counters - host Counters that registered fine but throw on increment.

How is success determined? neither requestId nor eventEmitted throws, and the fail-open counter shows stage=wiring at exactly 2 on the hostile registry.

Why does it matter? a bookkeeping failure in a host meter must degrade to a lost count, never surface in the entry point and turn the call into an unlogged pass-through.

should count the response read state per template, host and state

Rationale

What is tested? responseBodyRead - the lazily created counter and its three-tag id.

How is success determined? two unread and one complete recording under one template/host give counters of 2 and 1; no partial counter exists.

Why does it matter? the unread share per call site is the one place a discarded payload is visible. Given

should fold a recorded template without a placeholder into the untemplated tag value

Rationale

What is tested? the cardinality guard of the body meters' uri tag - the client records whatever string was passed to uri(String, ...), so uri("/things/" + id) would put one tag value per id on the meter.

How is success determined? a template with a placeholder is kept; one without, or none, folds to UNKNOWN.

Why does it matter? an unbounded tag set is a slow memory leak in the host registry. Given/When/Then

should hand out one owner per registry and stack

Rationale

What is tested? forRegistry's cache - same registry and stack give the same instance, a different stack or a different registry gives a different one.

How is success determined? identity for the same key, distinct instances otherwise, and the two stacks' gauges coexist in one registry under their own client tags.

Why does it matter? a duplicate owner's gauge registration would be silently ignored by Micrometer and its open exchanges become invisible; two stacks in one host must NOT share a gauge. Given

should keep the gauge private with a warning when the host registry already holds an identical gauge

Rationale

What is tested? the same-type collision check of the gauge registration - a host (or an older library copy on another classloader) already registered adapter.logging.exchanges.open{client=webclient}; Micrometer would return that gauge unchanged and silently drop this owner's state function.

How is success determined? the host's gauge keeps its own value (7) while an exchange is open, the registry holds exactly one meter under the id, and one WARN names the meter as kept private.

Why does it matter? without the check the liveness gauge showed a foreign value and this owner's open exchanges were invisible - the one silent-loss signal itself lost silently.

should keep working with a private meter when the host registry rejects a registration

Rationale

What is tested? registerOrFallback - the events meter's success id is already taken by a Gauge, so Micrometer rejects the counter registration with a different-type error.

How is success determined? construction succeeds, counting the conflicting outcome does not throw, the host keeps its gauge and holds no counter under that id, the other outcomes and the fail-open counters registered normally, and one WARN names the meter as kept private.

Why does it matter? a name clash with the host or another library must neither abort the context start nor suppress the exchange event - the one meter goes private, everything else exports.

should move the gauge with opened and completed exchanges

Rationale

What is tested? exchangeOpened/exchangeCompleted against the registered gauge.

How is success determined? two opens read 2, one completion reads 1, the second reads 0.

Why does it matter? the gauge is the one signal for exchanges that never end; it must track the owner's counter exactly, not a snapshot taken at registration. Given

should pre-register every fixed-tag meter at zero for the stack$legatium_common(ClientStack)[1]

Rationale

What is tested? construction against a fresh registry - the fail-open, events and correlation counters exist for every tag value, and the gauge exists under the stack's client tag, all at zero, before anything was counted.

How is success determined? three fail-open stages, the stack's outcomes (three or four), three sources, one gauge tagged client=<stack>, every value 0.

Why does it matter? a rate() alert must see the zero before the first occurrence, not a meter that springs into existence at the moment it should already fire. Given

should pre-register every fixed-tag meter at zero for the stack$legatium_common(ClientStack)[2]

Rationale

What is tested? construction against a fresh registry - the fail-open, events and correlation counters exist for every tag value, and the gauge exists under the stack's client tag, all at zero, before anything was counted.

How is success determined? three fail-open stages, the stack's outcomes (three or four), three sources, one gauge tagged client=<stack>, every value 0.

Why does it matter? a rate() alert must see the zero before the first occurrence, not a meter that springs into existence at the moment it should already fire. Given

should record body sizes under template and host and skip zero-byte bodies

Rationale

What is tested? the lazily created size summaries - the tag set, the base unit, the host fallback, and the zero-byte rule.

How is success determined? two request samples (5 and 7 bytes) land in ONE summary tagged by template and host; a body under an unknown host tags UNKNOWN; a zero-byte body creates no summary.

Why does it matter? the summary describes bodies that exist and the sum stays exact either way; a summary per call site is what the cardinality rules promise the host registry. Given

ClientLoggingPropertiesTest

11 tests.

Binding-time validation

should reject a blank logger name and a blank correlation header name

Rationale

What is tested? the two blank-name checks - a logger with no name and a header with no name are both misconfigurations Boot would bind silently.

How is success determined? construction fails naming the property.

Why does it matter? a blank logger name routes the exchange stream nowhere an operator expects; a blank header name cannot go on the wire. Given/When/Then

should reject a non-positive body capture limit

Rationale

What is tested? max-body-bytes must be positive - count-only mode is selected by the body modes, never by a zero limit.

How is success determined? zero and a negative limit fail naming the property.

Why does it matter? a zero limit would log every body as truncated to nothing without saying why. Given/When/Then

should reject blank entries in the activation lists

Rationale

What is tested? the three blank-entry checks of the activation lists.

How is success determined? each list rejects a blank entry with a message naming the list.

Why does it matter? a blank pattern or prefix matches nothing or everything by accident. Given/When/Then

Correlation id header

should accept every token character of a field name

Rationale

What is tested? the positive side of the RFC 9110 field-name check - every tchar in one name.

How is success determined? construction succeeds and the name binds unchanged.

Why does it matter? the regex is hand-written; a missing special character would reject a legal header name and fail the context start for a host with an unusual but valid convention. Given/

should reject a correlation header name outside the HTTP field-name grammar

Rationale

What is tested? binding-time validation of the header NAME - the name is written onto every traceless outgoing request, and an HTTP engine that validates field names rejects a non-token at runtime on every call.

How is success determined? whitespace, separators and a non-ASCII character fail construction with a message naming the property.

Why does it matter? a runtime rejection would fail the CALL itself, not merely the log line - a logging library turning into an outage. Given/When/

Excluded hosts

should reject blank host entries

Rationale

What is tested? the blank-entry check of excludeHosts next to a valid entry.

How is success determined? construction fails with a message naming the property.

Why does it matter? the host match is case-insensitive equality, so a blank entry can never match - the operator believes a peer is excluded while its calls keep logging. Given/When/

Masking key

should redact the masking key in toString

Rationale

What is tested? the key is a secret - a properties dump (a startup log, a debug endpoint) must not print it.

How is success determined? toString carries the redaction marker, never the key; the empty default renders empty.

Why does it matter? data-class toString would otherwise leak the secret into every context that prints the bean. Given/When/Then

should reject a blank masking key but accept an empty one

Rationale

What is tested? the binding-time rule - empty means unkeyed, blank is a misconfiguration.

How is success determined? whitespace fails construction naming the property; the empty default binds.

Why does it matter? a whitespace key would silently key the fingerprint with a worthless secret. Given/When/Then

Slow request threshold

should accept exactly one millisecond as the smallest threshold

Rationale

What is tested? the boundary of the toMillis() &gt;= 1 check - the smallest legal threshold.

How is success determined? construction succeeds and the property holds exactly one millisecond.

Why does it matter? an off-by-one in the floor would reject the documented minimum and fail the context start of a host that tuned the threshold down. Given/When

should reject a positive threshold below one millisecond

Rationale

What is tested? the resolution floor - the logged duration has millisecond resolution, so a sub-millisecond threshold would flag calls whose logged duration reads 0 ms.

How is success determined? construction fails with a message naming the floor.

Why does it matter? a silently accepted 500us threshold escalates all traffic to WARN. Given/When

should reject zero and negative thresholds

Rationale

What is tested? the lower end of the threshold check below the resolution floor - zero and a negative duration.

How is success determined? both fail construction with an IllegalArgumentException.

Why does it matter? a zero threshold flags every call as slow and escalates the whole stream to WARN; a negative one is a binding typo that would do the same. Given/When/

ClientLoggingReferenceConfigTest

3 tests.

should bind the body modes by their kebab-case names and refuse the former booleans

Rationale

What is tested? the documented spellings never / on-failure / always bind (Boot's lenient enum conversion), and a leftover true from the boolean era fails the binding loudly.

How is success determined? the two modes bound; true raises a BindException.

Why does it matter? a silently ignored true would switch body logging OFF for an operator who believed it on - the migration must be visible at startup. Given

should bind the reference configuration to exactly the built-in defaults

Rationale

What is tested? that every VALUE in the reference YAML is the built-in default.

How is success determined? binding the file yields an object equal to ClientLoggingProperties() - the data-class equality covers every property at once.

Why does it matter? the reference promises "copy it, and nothing changes"; a drifted default would silently break that promise for everyone who copies the block. Given/

should document only keys that actually exist and every key that does

Rationale

What is tested? that the reference contains no stale or misspelled keys - the Binder silently IGNORES unknown keys, so the equality test above cannot catch a typo on its own - and that no existing key goes undocumented.

How is success determined? the adapter-logging.* key set of the YAML equals the property names derived from the class's primary constructor (nested sections recursed).

Why does it matter? a documented key that does not bind is worse than an undocumented one - readers copy it and believe it works.

CorrelationHeaderTest

2 tests.

should accept a visible-ASCII id within the length bound

Rationale

What is tested? the positive side of the rule - the id shapes real systems send.

How is success determined? UUIDs, base-36 ids and ids with the usual punctuation are accepted verbatim.

Why does it matter? an over-strict rule would replace legitimate ids and break the join with the caller's logs. Given/When/Then

should treat control characters, whitespace, non-ASCII, emptiness and oversize as absent

Rationale

What is tested? the rejection side - every class of value that could forge a log line, bloat the MDC or is not an id at all.

How is success determined? null for each, so the twin generates and sends its own id instead.

Why does it matter? the value lands verbatim in the message, the MDC and the header field of every line of the call; a CR/LF inside it forges lines in every plain-text sink. Given/When/Then

CountingCorrelationIdGeneratorTest

17 tests.

Counter behaviour

should increment the counter by one on every call

Rationale

What is tested? getAndIncrement - consecutive calls render consecutive counter values.

How is success determined? three calls yield the suffixes 0, 1 and 2 behind an unchanged prefix.

Why does it matter? a step other than one (or an increment-then-get) would waste counter capacity or skip the zero id, and the prefix must not move between calls. Given

should keep the counter width constant across a base-36 carry

Rationale

What is tested? that the counter keeps its fixed width across a base-36 carry.

How is success determined? call 36 renders as ...0000000z and call 37 as ...00000010 - both eight characters wide.

Why does it matter? this is the exact point where a missing padStart would first show up. Up to value 35 the counter happens to be one character wide either way, so a test that only checks the first few ids would pass against a broken implementation. Given

should keep the counter width constant across a second base-36 carry

Rationale

What is tested? the counter padding at the two-digit carry - 36^2 - 1 renders zz, 36^2 renders 100.

How is success determined? call 1296 ends in 000000zz, call 1297 in 00000100, both eight wide.

Why does it matter? the first-carry test alone would pass against padding that only handles a one-digit counter; the second carry proves the width is fixed, not coincidental.

should keep the fixed width up to the last counter value the width can hold

Rationale

What is tested? the 21-character contract at its real boundary - the last value the counter width can render - reached through the internal counterStart seam instead of 2.8e12 warm-up calls.

How is success determined? the id at counter 36^8 - 1 still has exactly 21 characters and ends in eight z; the very next id grows to 22 characters, pinning the documented, deliberately unguarded overflow behavior (padStart silently stops applying).

Why does it matter? beyond the width both load-bearing format properties - the fixed split point and the lexicographic ordering - break without any signal. This test is the executable guard the production KDoc points to: narrowing a width constant fails here instead of in production.

should produce a reproducible sequence for a given seed

Rationale

What is tested? determinism under an explicit seed - two instances with the same seed and counter start.

How is success determined? the first five ids of both instances are identical.

Why does it matter? the seed is the test seam the other twins' tests rely on to pin ids without a mocking library; hidden per-instance randomness would make those tests flaky.

should start the counter at zero

Rationale

What is tested? the initial value of the AtomicLong - the first id of an instance.

How is success determined? with seed 0 the very first id is 21 zeros.

Why does it matter? the counter width holds exactly 36^8 ids; a counter that started elsewhere would reach the width boundary earlier than the documented lifetime. Given

should use different prefixes for different seeds

Rationale

What is tested? the prefix is derived from the seed, not from a shared or constant source.

How is success determined? the first 13 characters differ between seed 1 and seed 2.

Why does it matter? the prefix carries the cross-instance uniqueness of ADR-0004; two JVMs with identical prefixes would hand out colliding ids from their first call on. Given

Id format

should pad a small seed to the full prefix width

Rationale

What is tested? the padStart of the prefix - seed 1 renders as a single digit before padding.

How is success determined? the id starts with twelve zeros followed by 1.

Why does it matter? without the padding the prefix length would vary with the seed and the prefix/counter split point would move from id to id. Given

should produce a well-formed id when constructed without an explicit seed

Rationale

What is tested? the production constructor path - the prefix seeded from SecureRandom.

How is success determined? the id still matches the 21-character base-36 contract.

Why does it matter? this is the only path production takes; the seeded tests would keep passing while a broken default (a sign character, a wrong width) shipped unnoticed.

should render a negative seed as an unsigned value without a sign character

Rationale

What is tested? that a negative seed is reinterpreted as an unsigned value rather than rendered with a minus sign.

How is success determined? the prefix still occupies exactly 13 characters drawn from the base-36 alphabet and carries no sign character. The exact rendering is not asserted, because re-deriving it in the test would only duplicate the production code.

Why does it matter? half of all values a random source produces are negative. Using toString instead of toUnsignedString would put a - into every second id, silently changing both the id length and the character set that downstream consumers see. Given

should render an id of exactly twenty-one lowercase alphanumeric characters

Rationale

What is tested? the format contract of ADR-0004 - 13 prefix plus 8 counter characters, base-36 digits only.

How is success determined? length 21 and the whole id matches [0-9a-z].

Why does it matter? the fixed length is what makes the id splittable and sortable downstream; a stray uppercase or sign character would break consumers that key on the alphabet. Given

should render seed 35 as the last single-digit value of base 36

Rationale

What is tested? the top of the base-36 digit alphabet in the prefix - 35 is the last value rendered by one character.

How is success determined? the id starts with twelve zeros followed by z.

Why does it matter? pins that the radix is 36 and the digits are lowercase; a radix of 32 or 62 or an uppercase alphabet would change the character set the index sees.

should render seed 36 as a carry into the second digit

Rationale

What is tested? the first carry of the prefix rendering - 36 is 10 in base 36.

How is success determined? the id starts with eleven zeros, then 10.

Why does it matter? together with the seed-35 test this pins the radix from both sides - the boundary a wrong radix or a toString() without radix would cross first. Given

Lexicographic ordering

should hand out ids that sort in allocation order

Rationale

What is tested? that ids handed out by one instance sort lexicographically in the order they were allocated.

How is success determined? sorting the generated sequence leaves it unchanged. The range deliberately spans the carry at 36, which is where a width regression would break the ordering first.

Why does it matter? this property is what makes the id usable as a tiebreaker when log entries share a timestamp. It is not enforced by any type - it rests entirely on the fixed field widths, and a change to those constants would drop it silently. Given

should hand out ids that sort in allocation order across a carry

Rationale

What is tested? the ordering property exactly at the second counter carry - twelve ids around 1296 (36^2).

How is success determined? the ids sort lexicographically in allocation order.

Why does it matter? a width regression breaks ordering first at a carry, where zz must sort before 100 only because both are padded to eight characters. Given

Thread safety

should hand out distinct ids under concurrent access

Rationale

What is tested? that concurrent calls never hand out the same id twice.

How is success determined? the number of distinct ids equals the number of calls. Since uniqueness within an instance is a guarantee rather than a probability, any duplicate is a hard failure, not a flake.

Why does it matter? the counter is the one piece of mutable shared state in the class. Replacing the AtomicLong with a plain Long - or, more plausibly, with a ThreadLocal in an attempt to avoid contention - would produce duplicates here. Given

should keep the id format intact under concurrent access

Rationale

What is tested? the rendering under contention - eight threads sharing one AtomicLong.

How is success determined? the pool finishes within the timeout and every id matches the 21-character base-36 contract.

Why does it matter? the distinctness test would not catch a torn or malformed id; a non-atomic read-modify-write could produce a value that renders outside the width. Given

FailOpenDiagnosticsTest

3 tests.

should confine a handler that throws instead of letting it escape the guard

Rationale

What is tested? the diagnostics channel itself is guarded - a handler backed by a throwing counter or appender must not turn a confined failure into an escaping one.

How is success determined? neither the failing handler nor the original exception escapes; the interrupt flag is still restored when the interrupted handler throws.

Why does it matter? the handler runs against host-provided components; their failure is the case reportQuietly exists for. Given/When

should restore the interrupt flag and route an InterruptedException to its handler

Rationale

What is tested? the InterruptedException branch of failOpen - the JVM cleared the flag when it threw, and on a request-serving or event-loop thread the interrupt must still reach its addressee after the guard confined the exception.

How is success determined? the thread is interrupted afterwards, the interrupted handler ran once, the generic handler did not, nothing escaped.

Why does it matter? a swallowed interrupt on a pooled thread is a hang or a late cancellation in the host - the one outcome a fail-open logging guard must not produce. Given

should run the operation untouched when it does not throw

Rationale

What is tested? the happy path of failOpen - no catch branch runs when the operation returns.

How is success determined? the operation ran, neither handler was called (they would fail the test), and the interrupt flag stays clear.

Why does it matter? the guard wraps every emitter and callback; touching the interrupt flag or reporting on success would count a fail-open event for every healthy call. Given/When

HeaderLogPropertiesTest

8 tests.

should keep supporting the wildcard in includes and masked

Rationale

What is tested? the two positions where * is legal - the validation must reject it only in excludes and unmasked.

How is success determined? construction succeeds and a header the wildcard includes is masked.

Why does it matter? masked: ["*"] is the default value; a validation tightened by mistake would fail every context that spells the default out. Given/

should log an explicitly included header once although it is listed twice in differing case

Rationale

What is tested? deduplication of the explicit include path, which the wildcard path already had.

How is success determined? ["Accept", "accept"] selects the header once, under the first spelling.

Why does it matter? a duplicated name=value pair on the line is the one input the validation does not normalise. Given

should mask every logged header by default and let unmasked names through in plaintext

Rationale

What is tested? ADR-0005 - the default section masks everything it logs; the plaintext set is an explicit allowlist that wins over the mask.

How is success determined? with a wildcard include and NOTHING said about masking, both headers are fingerprinted; naming one in unmasked renders exactly that one in plaintext.

Why does it matter? includes: ["*"] is the documented debugging move; with the old defaults it put every header in the log in plaintext because masking was a second, empty list.

should mask through the masker handed to select so a host bean decides the shape

Rationale

What is tested? the masker is an injected collaborator of the selection, not a hard-wired fingerprint - the shape of a masked value is the host's policy.

How is success determined? a section with a masked name renders the masker's output for that header and the plain value for every other; the plaintext of the masked header never appears.

Why does it matter? a compliance regime may forbid unkeyed hashes; the bean is the one place to satisfy it for both twins at once.

should reject blank entries in every list at construction time

Rationale

What is tested? the four blank-entry checks - the validation surface the masking fuzz target relies on (it catches the IllegalArgumentException) but never proves.

How is success determined? each list rejects a blank entry with a message naming the list.

Why does it matter? a blank name binds silently otherwise and matches nothing, an operator's typo turning into a missing header without feedback. Given/When/Then

should reject the wildcard in excludes at construction time

Rationale

What is tested? the binding-time validation for a plausible misconfiguration - '*' means something in includes and masked, but was a silent no-op in excludes.

How is success determined? construction fails with a message naming the alternative.

Why does it matter? a wildcard exclude reads like "log nothing", would have logged EVERYTHING the includes selected, and gave no feedback - the classic silent misconfiguration. Given/

should reject the wildcard in unmasked at construction time

Rationale

What is tested? the plaintext set is an explicit list of names by design.

How is success determined? construction fails with a message naming the alternative (empty masked).

Why does it matter? unmasked: ["*"] would be the one-token way back to plaintext-everything; the way back must be the visible removal of the mask, not an addition that reads harmless. Given/When

should switch masking off only through an explicitly emptied masked list

Rationale

What is tested? the documented way back to plaintext - masked = [] clears the maskAll flag and the masked set at once.

How is success determined? an explicitly included Authorization value renders in plaintext.

Why does it matter? the mask-by-default rule needs exactly one visible off switch; if emptying the list did not work an operator would reach for a less visible route.

HeaderMaskingFuzzTest

11 tests.

selection_and_masking_uphold_their_contract(FuzzedDataProvider)[10]

Rationale

What is tested? HeaderLogProperties construction and select() plus the default masker against arbitrary name lists and header maps - the documented rejection cases exactly, no throw from select(), include-minus-exclude once per name, masked values only as the fingerprint.

How is success determined? no exception and no oracle violation for any input Jazzer generates - a masked value in plaintext or a rejection outside the documented cases fails the run.

Why does it matter? header names and values are peer- and operator-controlled input on every exchange; a plaintext leak through an unforeseen name shape is a secret in the logs.

selection_and_masking_uphold_their_contract(FuzzedDataProvider)[11]

Rationale

What is tested? HeaderLogProperties construction and select() plus the default masker against arbitrary name lists and header maps - the documented rejection cases exactly, no throw from select(), include-minus-exclude once per name, masked values only as the fingerprint.

How is success determined? no exception and no oracle violation for any input Jazzer generates - a masked value in plaintext or a rejection outside the documented cases fails the run.

Why does it matter? header names and values are peer- and operator-controlled input on every exchange; a plaintext leak through an unforeseen name shape is a secret in the logs.

selection_and_masking_uphold_their_contract(FuzzedDataProvider)[1]

Rationale

What is tested? HeaderLogProperties construction and select() plus the default masker against arbitrary name lists and header maps - the documented rejection cases exactly, no throw from select(), include-minus-exclude once per name, masked values only as the fingerprint.

How is success determined? no exception and no oracle violation for any input Jazzer generates - a masked value in plaintext or a rejection outside the documented cases fails the run.

Why does it matter? header names and values are peer- and operator-controlled input on every exchange; a plaintext leak through an unforeseen name shape is a secret in the logs.

selection_and_masking_uphold_their_contract(FuzzedDataProvider)[2]

Rationale

What is tested? HeaderLogProperties construction and select() plus the default masker against arbitrary name lists and header maps - the documented rejection cases exactly, no throw from select(), include-minus-exclude once per name, masked values only as the fingerprint.

How is success determined? no exception and no oracle violation for any input Jazzer generates - a masked value in plaintext or a rejection outside the documented cases fails the run.

Why does it matter? header names and values are peer- and operator-controlled input on every exchange; a plaintext leak through an unforeseen name shape is a secret in the logs.

selection_and_masking_uphold_their_contract(FuzzedDataProvider)[3]

Rationale

What is tested? HeaderLogProperties construction and select() plus the default masker against arbitrary name lists and header maps - the documented rejection cases exactly, no throw from select(), include-minus-exclude once per name, masked values only as the fingerprint.

How is success determined? no exception and no oracle violation for any input Jazzer generates - a masked value in plaintext or a rejection outside the documented cases fails the run.

Why does it matter? header names and values are peer- and operator-controlled input on every exchange; a plaintext leak through an unforeseen name shape is a secret in the logs.

selection_and_masking_uphold_their_contract(FuzzedDataProvider)[4]

Rationale

What is tested? HeaderLogProperties construction and select() plus the default masker against arbitrary name lists and header maps - the documented rejection cases exactly, no throw from select(), include-minus-exclude once per name, masked values only as the fingerprint.

How is success determined? no exception and no oracle violation for any input Jazzer generates - a masked value in plaintext or a rejection outside the documented cases fails the run.

Why does it matter? header names and values are peer- and operator-controlled input on every exchange; a plaintext leak through an unforeseen name shape is a secret in the logs.

selection_and_masking_uphold_their_contract(FuzzedDataProvider)[5]

Rationale

What is tested? HeaderLogProperties construction and select() plus the default masker against arbitrary name lists and header maps - the documented rejection cases exactly, no throw from select(), include-minus-exclude once per name, masked values only as the fingerprint.

How is success determined? no exception and no oracle violation for any input Jazzer generates - a masked value in plaintext or a rejection outside the documented cases fails the run.

Why does it matter? header names and values are peer- and operator-controlled input on every exchange; a plaintext leak through an unforeseen name shape is a secret in the logs.

selection_and_masking_uphold_their_contract(FuzzedDataProvider)[6]

Rationale

What is tested? HeaderLogProperties construction and select() plus the default masker against arbitrary name lists and header maps - the documented rejection cases exactly, no throw from select(), include-minus-exclude once per name, masked values only as the fingerprint.

How is success determined? no exception and no oracle violation for any input Jazzer generates - a masked value in plaintext or a rejection outside the documented cases fails the run.

Why does it matter? header names and values are peer- and operator-controlled input on every exchange; a plaintext leak through an unforeseen name shape is a secret in the logs.

selection_and_masking_uphold_their_contract(FuzzedDataProvider)[7]

Rationale

What is tested? HeaderLogProperties construction and select() plus the default masker against arbitrary name lists and header maps - the documented rejection cases exactly, no throw from select(), include-minus-exclude once per name, masked values only as the fingerprint.

How is success determined? no exception and no oracle violation for any input Jazzer generates - a masked value in plaintext or a rejection outside the documented cases fails the run.

Why does it matter? header names and values are peer- and operator-controlled input on every exchange; a plaintext leak through an unforeseen name shape is a secret in the logs.

selection_and_masking_uphold_their_contract(FuzzedDataProvider)[8]

Rationale

What is tested? HeaderLogProperties construction and select() plus the default masker against arbitrary name lists and header maps - the documented rejection cases exactly, no throw from select(), include-minus-exclude once per name, masked values only as the fingerprint.

How is success determined? no exception and no oracle violation for any input Jazzer generates - a masked value in plaintext or a rejection outside the documented cases fails the run.

Why does it matter? header names and values are peer- and operator-controlled input on every exchange; a plaintext leak through an unforeseen name shape is a secret in the logs.

selection_and_masking_uphold_their_contract(FuzzedDataProvider)[9]

Rationale

What is tested? HeaderLogProperties construction and select() plus the default masker against arbitrary name lists and header maps - the documented rejection cases exactly, no throw from select(), include-minus-exclude once per name, masked values only as the fingerprint.

How is success determined? no exception and no oracle violation for any input Jazzer generates - a masked value in plaintext or a rejection outside the documented cases fails the run.

Why does it matter? header names and values are peer- and operator-controlled input on every exchange; a plaintext leak through an unforeseen name shape is a secret in the logs.

HeaderValueMaskerTest

4 tests.

should key the fingerprint with an HMAC that keeps the shape and changes the digits

Rationale

What is tested? the keyed variant - same length:hex16 shape, different 64 bits, pinned as known answers (HMAC-SHA256 over UTF-8, first 8 bytes) so the format cannot drift silently.

How is success determined? two keys give two different fingerprints, both differ from the unkeyed one, and each matches its literal.

Why does it matter? a keyed fingerprint is what makes masked guess-proof; the literals are the contract a peer sharing the key can rely on. Given/When/Then

should reject a blank key

Rationale

What is tested? the blank-key guard of keyed, reached directly and through forKey.

How is success determined? both throw an IllegalArgumentException.

Why does it matter? a whitespace key is not empty, so forKey would otherwise build an HMAC under a worthless secret and present the result as guess-proof. Given/When/

should render identical values identically under the same key

Rationale

What is tested? stability of the keyed fingerprint - within one masker and across two maskers built from the same key.

How is success determined? the same value renders the same string in both cases.

Why does it matter? a per-instance nonce or salt would keep the shape but break the correlation of a masked token across events, twins and the inbound sibling. Given/When/

should select the unkeyed default for an empty key and the keyed variant otherwise

Rationale

What is tested? the property's factory - the empty default keeps the twin-contract fingerprint.

How is success determined? empty key -> the DEFAULT instance; a key -> the keyed rendering.

Why does it matter? the auto-configurations build their bean from this; an empty key must not silently key the fingerprint with an empty secret. Given/When/Then

MdcScopeTest

4 tests.

should attach a failing rollback to the install exception instead of replacing it

Rationale

What is tested? the nested failure in the init block - the put fails AND the rollback's remove fails on a key installed before it.

How is success determined? the install exception propagates with the rollback failure attached as suppressed; the key the rollback could remove is gone.

Why does it matter? a rollback exception replacing the original would hide the actual cause of the broken adapter behind its own follow-up failure.

should restore a previous value of a module-owned key instead of removing it

Rationale

What is tested? the additive-overlay promise for a NESTED scope - an outer scope (or an ambient owner of the same keys) has values in place; the inner scope overlays and must put them back.

How is success determined? inside the scope the inner values are visible; after close the outer values are back, key for key - including an owned trace key that the inner scope removed.

Why does it matter? an outbound call made while another outbound call's scope is active (a retry inside a client, a nested adapter) must not erase the outer identity.

should restore every remaining key when one restoration fails and attach later failures as suppressed

Rationale

What is tested? best-effort restoration on close - the adapter fails on TWO keys' removes.

How is success determined? close throws the first failure with the second attached as suppressed, and the other keys were still restored.

Why does it matter? a restoration loop that stops at the first failure leaves module-owned MDC on the thread for every later key - exactly the contamination the scope exists to prevent.

should roll back the keys already installed when a later put fails and keep the install exception

Rationale

What is tested? the partial-install rollback - the adapter fails on the THIRD key.

How is success determined? the install exception propagates as-is, and the two keys installed before it are gone from the MDC (pooled-thread hygiene).

Why does it matter? half an identity on a pooled thread contaminates the next request's logs.

SharedContractTest

5 tests.

should pin the MDC keys

Rationale

What is tested? the MdcKeys and TraceMdcKeys constants both twins write into the MDC.

How is success determined? the three adapter_* keys and Boot's traceId/spanId names are the literals.

Why does it matter? structured encoders emit MDC entries as fields by name - the adapter_ prefix keeps them beside limesium's endpoint_ keys, and only Boot's own trace key names make the join with the tracing bridge hold. Given/When/Then

should pin the fail-open stages and the request-id sources

Rationale

What is tested? the stage tag values of adapter.logging.failopen and the source tag values of adapter.logging.correlation.id.

How is success determined? emission, arrival, wiring and trace, header, generated - exactly.

Why does it matter? the suggested alert set in the guide queries stage="emission" literally; the generated share of the source tag is the propagation-regression signal (ADR-0002). Given/When/Then

should pin the meter names and the fallback tag values

Rationale

What is tested? the string constants of ClientLoggingMetrics that name the seven meters (six families) and the two fallback tag values.

How is success determined? each constant equals its literal.

Why does it matter? dashboards and alerts key on these names across both twins and beside limesium's endpoint.* family; a rename must break the build, not silently split a metric. Given/When/Then

should pin the outcome vocabulary

Rationale

What is tested? the tagValue literals of ClientOutcome - the three both stacks share and the reactive cancelled - and the size of the enum.

How is success determined? the four literals match the values dashboards filter on; no fifth value.

Why does it matter? adapter_outcome and the outcome tag of adapter.logging.events are the closed vocabulary every alert keys on; a renamed value would silently zero an alert. Given/When/Then

should pin the response body read states

Rationale

What is tested? the state tag values of adapter.response.body.read and the size of the enum.

How is success determined? unread, partial, complete - and no fourth value.

Why does it matter? the tag values are the wire contract of the counter; an added or renamed state would change the meter's cardinality under every consumer. Given/When/Then

TimeoutsTest

5 tests.

should not classify ordinary failures or a missing throwable as a timeout

Rationale

What is tested? the negative side of the classification - a plain I/O error, a state error wrapping one, and null.

How is success determined? false for all three.

Why does it matter? a walk that matched a superclass too eagerly would report every connection reset as a timeout and make the timeout outcome meaningless on the dashboards. Given/When/

should recognise Netty's read and connect timeouts by their class names

Rationale

What is tested? the by-name matches for io.netty.handler.timeout.TimeoutException (Reactor Netty's ReadTimeoutException is one) and io.netty.channel.ConnectTimeoutException (a java.net.ConnectException that no JDK timeout type covers) - against the REAL classes, Netty being a test-scoped dependency here while neither twin depends on it.

How is success determined? both, wrapped the way the WebClient twin meets them, classify as a timeout.

Why does it matter? the WebClient twin's most common timeouts would otherwise log as plain failures. Given

should recognise a timeout carried as a suppressed exception of a composite error

Rationale

What is tested? the walk over SUPPRESSED exceptions - Reactor's composite errors (zip/merge/when) carry their components as suppressed, not as the cause.

How is success determined? a composite whose only timeout is suppressed classifies as a timeout; a suppressed cycle terminates.

Why does it matter? a timeout hidden in a composite would log as a plain failure on the WebClient twin.

should recognise the JDK timeout types through a wrapping cause chain

Rationale

What is tested? the cause walk - clients never throw the raw timeout, they wrap it (ResourceAccessException over SocketTimeoutException, a client exception over a TimeoutException from a future).

How is success determined? a timeout anywhere in the chain classifies the failure as a timeout.

Why does it matter? a timeout logged as a generic failure hides the one client-side disposition an operator reads differently from every other one.

should terminate on a cyclic cause chain

Rationale

What is tested? the visited-set guard of the cause walk - Throwable permits a cycle.

How is success determined? the call returns (false) instead of looping.

Why does it matter? an unbounded walk on a pooled thread is a hang, not a log line.

TraceparentFuzzTest

15 tests.

parser_upholds_its_contract(FuzzedDataProvider)[10]

Rationale

What is tested? Traceparent.parse against arbitrary input - the negative oracle (never throws, accepts only well-formed lowercase-hex ids of fixed length, neither all zeros) and the positive oracle (a structurally valid version-00 header built from fuzzed hex always parses).

How is success determined? no exception and no verdict that contradicts either oracle for any input Jazzer generates, the checked-in corpus included.

Why does it matter? the header is peer-controlled; a throw would break a call on the fail-open path, a false accept would join the event to a foreign trace, a false reject would drop it.

parser_upholds_its_contract(FuzzedDataProvider)[11]

Rationale

What is tested? Traceparent.parse against arbitrary input - the negative oracle (never throws, accepts only well-formed lowercase-hex ids of fixed length, neither all zeros) and the positive oracle (a structurally valid version-00 header built from fuzzed hex always parses).

How is success determined? no exception and no verdict that contradicts either oracle for any input Jazzer generates, the checked-in corpus included.

Why does it matter? the header is peer-controlled; a throw would break a call on the fail-open path, a false accept would join the event to a foreign trace, a false reject would drop it.

parser_upholds_its_contract(FuzzedDataProvider)[12]

Rationale

What is tested? Traceparent.parse against arbitrary input - the negative oracle (never throws, accepts only well-formed lowercase-hex ids of fixed length, neither all zeros) and the positive oracle (a structurally valid version-00 header built from fuzzed hex always parses).

How is success determined? no exception and no verdict that contradicts either oracle for any input Jazzer generates, the checked-in corpus included.

Why does it matter? the header is peer-controlled; a throw would break a call on the fail-open path, a false accept would join the event to a foreign trace, a false reject would drop it.

parser_upholds_its_contract(FuzzedDataProvider)[13]

Rationale

What is tested? Traceparent.parse against arbitrary input - the negative oracle (never throws, accepts only well-formed lowercase-hex ids of fixed length, neither all zeros) and the positive oracle (a structurally valid version-00 header built from fuzzed hex always parses).

How is success determined? no exception and no verdict that contradicts either oracle for any input Jazzer generates, the checked-in corpus included.

Why does it matter? the header is peer-controlled; a throw would break a call on the fail-open path, a false accept would join the event to a foreign trace, a false reject would drop it.

parser_upholds_its_contract(FuzzedDataProvider)[14]

Rationale

What is tested? Traceparent.parse against arbitrary input - the negative oracle (never throws, accepts only well-formed lowercase-hex ids of fixed length, neither all zeros) and the positive oracle (a structurally valid version-00 header built from fuzzed hex always parses).

How is success determined? no exception and no verdict that contradicts either oracle for any input Jazzer generates, the checked-in corpus included.

Why does it matter? the header is peer-controlled; a throw would break a call on the fail-open path, a false accept would join the event to a foreign trace, a false reject would drop it.

parser_upholds_its_contract(FuzzedDataProvider)[15]

Rationale

What is tested? Traceparent.parse against arbitrary input - the negative oracle (never throws, accepts only well-formed lowercase-hex ids of fixed length, neither all zeros) and the positive oracle (a structurally valid version-00 header built from fuzzed hex always parses).

How is success determined? no exception and no verdict that contradicts either oracle for any input Jazzer generates, the checked-in corpus included.

Why does it matter? the header is peer-controlled; a throw would break a call on the fail-open path, a false accept would join the event to a foreign trace, a false reject would drop it.

parser_upholds_its_contract(FuzzedDataProvider)[1]

Rationale

What is tested? Traceparent.parse against arbitrary input - the negative oracle (never throws, accepts only well-formed lowercase-hex ids of fixed length, neither all zeros) and the positive oracle (a structurally valid version-00 header built from fuzzed hex always parses).

How is success determined? no exception and no verdict that contradicts either oracle for any input Jazzer generates, the checked-in corpus included.

Why does it matter? the header is peer-controlled; a throw would break a call on the fail-open path, a false accept would join the event to a foreign trace, a false reject would drop it.

parser_upholds_its_contract(FuzzedDataProvider)[2]

Rationale

What is tested? Traceparent.parse against arbitrary input - the negative oracle (never throws, accepts only well-formed lowercase-hex ids of fixed length, neither all zeros) and the positive oracle (a structurally valid version-00 header built from fuzzed hex always parses).

How is success determined? no exception and no verdict that contradicts either oracle for any input Jazzer generates, the checked-in corpus included.

Why does it matter? the header is peer-controlled; a throw would break a call on the fail-open path, a false accept would join the event to a foreign trace, a false reject would drop it.

parser_upholds_its_contract(FuzzedDataProvider)[3]

Rationale

What is tested? Traceparent.parse against arbitrary input - the negative oracle (never throws, accepts only well-formed lowercase-hex ids of fixed length, neither all zeros) and the positive oracle (a structurally valid version-00 header built from fuzzed hex always parses).

How is success determined? no exception and no verdict that contradicts either oracle for any input Jazzer generates, the checked-in corpus included.

Why does it matter? the header is peer-controlled; a throw would break a call on the fail-open path, a false accept would join the event to a foreign trace, a false reject would drop it.

parser_upholds_its_contract(FuzzedDataProvider)[4]

Rationale

What is tested? Traceparent.parse against arbitrary input - the negative oracle (never throws, accepts only well-formed lowercase-hex ids of fixed length, neither all zeros) and the positive oracle (a structurally valid version-00 header built from fuzzed hex always parses).

How is success determined? no exception and no verdict that contradicts either oracle for any input Jazzer generates, the checked-in corpus included.

Why does it matter? the header is peer-controlled; a throw would break a call on the fail-open path, a false accept would join the event to a foreign trace, a false reject would drop it.

parser_upholds_its_contract(FuzzedDataProvider)[5]

Rationale

What is tested? Traceparent.parse against arbitrary input - the negative oracle (never throws, accepts only well-formed lowercase-hex ids of fixed length, neither all zeros) and the positive oracle (a structurally valid version-00 header built from fuzzed hex always parses).

How is success determined? no exception and no verdict that contradicts either oracle for any input Jazzer generates, the checked-in corpus included.

Why does it matter? the header is peer-controlled; a throw would break a call on the fail-open path, a false accept would join the event to a foreign trace, a false reject would drop it.

parser_upholds_its_contract(FuzzedDataProvider)[6]

Rationale

What is tested? Traceparent.parse against arbitrary input - the negative oracle (never throws, accepts only well-formed lowercase-hex ids of fixed length, neither all zeros) and the positive oracle (a structurally valid version-00 header built from fuzzed hex always parses).

How is success determined? no exception and no verdict that contradicts either oracle for any input Jazzer generates, the checked-in corpus included.

Why does it matter? the header is peer-controlled; a throw would break a call on the fail-open path, a false accept would join the event to a foreign trace, a false reject would drop it.

parser_upholds_its_contract(FuzzedDataProvider)[7]

Rationale

What is tested? Traceparent.parse against arbitrary input - the negative oracle (never throws, accepts only well-formed lowercase-hex ids of fixed length, neither all zeros) and the positive oracle (a structurally valid version-00 header built from fuzzed hex always parses).

How is success determined? no exception and no verdict that contradicts either oracle for any input Jazzer generates, the checked-in corpus included.

Why does it matter? the header is peer-controlled; a throw would break a call on the fail-open path, a false accept would join the event to a foreign trace, a false reject would drop it.

parser_upholds_its_contract(FuzzedDataProvider)[8]

Rationale

What is tested? Traceparent.parse against arbitrary input - the negative oracle (never throws, accepts only well-formed lowercase-hex ids of fixed length, neither all zeros) and the positive oracle (a structurally valid version-00 header built from fuzzed hex always parses).

How is success determined? no exception and no verdict that contradicts either oracle for any input Jazzer generates, the checked-in corpus included.

Why does it matter? the header is peer-controlled; a throw would break a call on the fail-open path, a false accept would join the event to a foreign trace, a false reject would drop it.

parser_upholds_its_contract(FuzzedDataProvider)[9]

Rationale

What is tested? Traceparent.parse against arbitrary input - the negative oracle (never throws, accepts only well-formed lowercase-hex ids of fixed length, neither all zeros) and the positive oracle (a structurally valid version-00 header built from fuzzed hex always parses).

How is success determined? no exception and no verdict that contradicts either oracle for any input Jazzer generates, the checked-in corpus included.

Why does it matter? the header is peer-controlled; a throw would break a call on the fail-open path, a false accept would join the event to a foreign trace, a false reject would drop it.

TraceparentTest

3 tests.

should accept every conformant header of the shared fixture with the expected identifiers

Rationale

What is tested? the accepting side of the parser - each valid fixture line yields the trace id and the parent id it names, including higher versions with extra fields.

How is success determined? the fixture is non-empty and every line parses to its expected pair.

Why does it matter? a rejected valid header drops the call out of the trace and into the correlation-header path, so its log line no longer joins the tracing infrastructure.

should reject every non-conformant header of the shared fixture

Rationale

What is tested? identifier shape, all-zero ids, version (two lowercase hex, not ff, exactly four fields for 00), flags (two lowercase hex) and structure - the cases the fixture enumerates.

How is success determined? null for each.

Why does it matter? an accepted invalid header lands under the traceId/spanId MDC keys and produces joins the tracing infrastructure does not contain.

should treat an absent header as no trace context

Rationale

What is tested? the null branch at the top of parse.

How is success determined? null in, null out - no exception.

Why does it matter? most outbound calls of a host without tracing carry no header; the traceless path starts here and must not pay an exception per call. Given/When/Then

legatium-restclient-logging

142 tests.

BoundedBodyCaptureFuzzTest

10 tests.

capture_upholds_its_contract(FuzzedDataProvider)[10]

Rationale

What is tested? BoundedBodyCapture under a random sequence of single-byte, array and ranged writes with a random limit and charset - exact byte total, null loggedValue() only for a zero-byte body, truncation announced exactly when more bytes flowed than the limit holds.

How is success determined? no exception and no invariant violation for any input Jazzer generates, whatever the byte content and charset.

Why does it matter? the capture sees every body byte of every exchange; a throw on an odd byte sequence would surface inside the client's read, a wrong count would corrupt the size meter.

capture_upholds_its_contract(FuzzedDataProvider)[1]

Rationale

What is tested? BoundedBodyCapture under a random sequence of single-byte, array and ranged writes with a random limit and charset - exact byte total, null loggedValue() only for a zero-byte body, truncation announced exactly when more bytes flowed than the limit holds.

How is success determined? no exception and no invariant violation for any input Jazzer generates, whatever the byte content and charset.

Why does it matter? the capture sees every body byte of every exchange; a throw on an odd byte sequence would surface inside the client's read, a wrong count would corrupt the size meter.

capture_upholds_its_contract(FuzzedDataProvider)[2]

Rationale

What is tested? BoundedBodyCapture under a random sequence of single-byte, array and ranged writes with a random limit and charset - exact byte total, null loggedValue() only for a zero-byte body, truncation announced exactly when more bytes flowed than the limit holds.

How is success determined? no exception and no invariant violation for any input Jazzer generates, whatever the byte content and charset.

Why does it matter? the capture sees every body byte of every exchange; a throw on an odd byte sequence would surface inside the client's read, a wrong count would corrupt the size meter.

capture_upholds_its_contract(FuzzedDataProvider)[3]

Rationale

What is tested? BoundedBodyCapture under a random sequence of single-byte, array and ranged writes with a random limit and charset - exact byte total, null loggedValue() only for a zero-byte body, truncation announced exactly when more bytes flowed than the limit holds.

How is success determined? no exception and no invariant violation for any input Jazzer generates, whatever the byte content and charset.

Why does it matter? the capture sees every body byte of every exchange; a throw on an odd byte sequence would surface inside the client's read, a wrong count would corrupt the size meter.

capture_upholds_its_contract(FuzzedDataProvider)[4]

Rationale

What is tested? BoundedBodyCapture under a random sequence of single-byte, array and ranged writes with a random limit and charset - exact byte total, null loggedValue() only for a zero-byte body, truncation announced exactly when more bytes flowed than the limit holds.

How is success determined? no exception and no invariant violation for any input Jazzer generates, whatever the byte content and charset.

Why does it matter? the capture sees every body byte of every exchange; a throw on an odd byte sequence would surface inside the client's read, a wrong count would corrupt the size meter.

capture_upholds_its_contract(FuzzedDataProvider)[5]

Rationale

What is tested? BoundedBodyCapture under a random sequence of single-byte, array and ranged writes with a random limit and charset - exact byte total, null loggedValue() only for a zero-byte body, truncation announced exactly when more bytes flowed than the limit holds.

How is success determined? no exception and no invariant violation for any input Jazzer generates, whatever the byte content and charset.

Why does it matter? the capture sees every body byte of every exchange; a throw on an odd byte sequence would surface inside the client's read, a wrong count would corrupt the size meter.

capture_upholds_its_contract(FuzzedDataProvider)[6]

Rationale

What is tested? BoundedBodyCapture under a random sequence of single-byte, array and ranged writes with a random limit and charset - exact byte total, null loggedValue() only for a zero-byte body, truncation announced exactly when more bytes flowed than the limit holds.

How is success determined? no exception and no invariant violation for any input Jazzer generates, whatever the byte content and charset.

Why does it matter? the capture sees every body byte of every exchange; a throw on an odd byte sequence would surface inside the client's read, a wrong count would corrupt the size meter.

capture_upholds_its_contract(FuzzedDataProvider)[7]

Rationale

What is tested? BoundedBodyCapture under a random sequence of single-byte, array and ranged writes with a random limit and charset - exact byte total, null loggedValue() only for a zero-byte body, truncation announced exactly when more bytes flowed than the limit holds.

How is success determined? no exception and no invariant violation for any input Jazzer generates, whatever the byte content and charset.

Why does it matter? the capture sees every body byte of every exchange; a throw on an odd byte sequence would surface inside the client's read, a wrong count would corrupt the size meter.

capture_upholds_its_contract(FuzzedDataProvider)[8]

Rationale

What is tested? BoundedBodyCapture under a random sequence of single-byte, array and ranged writes with a random limit and charset - exact byte total, null loggedValue() only for a zero-byte body, truncation announced exactly when more bytes flowed than the limit holds.

How is success determined? no exception and no invariant violation for any input Jazzer generates, whatever the byte content and charset.

Why does it matter? the capture sees every body byte of every exchange; a throw on an odd byte sequence would surface inside the client's read, a wrong count would corrupt the size meter.

capture_upholds_its_contract(FuzzedDataProvider)[9]

Rationale

What is tested? BoundedBodyCapture under a random sequence of single-byte, array and ranged writes with a random limit and charset - exact byte total, null loggedValue() only for a zero-byte body, truncation announced exactly when more bytes flowed than the limit holds.

How is success determined? no exception and no invariant violation for any input Jazzer generates, whatever the byte content and charset.

Why does it matter? the capture sees every body byte of every exchange; a throw on an odd byte sequence would surface inside the client's read, a wrong count would corrupt the size meter.

BoundedBodyCaptureTest

10 tests.

Counting and bounding

should buffer nothing in count-only mode and still count everything

Rationale

What is tested? limit 0, the mode newCaptures installs for measure-*-body-size without body logging - room is never positive, so the write is skipped, but the count advances.

How is success determined? totalBytes is 5 and loggedValue is the bare truncation note without a prefix.

Why does it matter? a measure-only capture is installed on every exchange and must cost no buffer memory; the exact total is what the size summary records.

should count every byte but buffer only up to the limit

Rationale

What is tested? both capture overloads against one 4-byte cap - the chunk write clips to the remaining room, the single-byte write checks the buffer size, totalBytes advances regardless.

How is success determined? totalBytes is 10 while loggedValue renders the four buffered bytes plus the "... [truncated, 10 bytes total]" note.

Why does it matter? the size summaries need the exact total and the log line must never hold more than max-body-bytes, whatever chunking the engine's stream uses.

should report a zero-byte body as absent

Rationale

What is tested? the totalBytes == 0 short-circuit of loggedValue on a capture nothing flowed through.

How is success determined? null, not an empty string and not a "[truncated, 0 bytes total]" note.

Why does it matter? the emitter's addKeyValueIfPresent drops a null, so a bodiless exchange gets no body key at all instead of an empty one that looks like an empty body. Given/When/Then

Read state

should complete a declared zero-length body when the stream is opened

Rationale

What is tested? markStarted with a declared length of zero - a length-aware reader opens the stream and reads nothing, which is the whole body.

How is success determined? COMPLETE right after markStarted; with an unknown length the same call yields PARTIAL.

Why does it matter? an empty declared body must not count as consumption that stopped early. Given/When/

should complete when the byte count reaches the declared length without an EOF

Rationale

What is tested? expectBytes - a declared length completes the read state the moment the count reaches it, through either capture overload, with no markCompleted call.

How is success determined? PARTIAL after 5 of 6 bytes, COMPLETE after the sixth, and COMPLETE stays when more bytes than declared arrive.

Why does it matter? a length-aware reader (Spring's ByteArrayHttpMessageConverter, readNBytes) never asks for the EOF; without this rule its complete reads counted as partial.

should start unread and move to partial on start and to complete on completion, never backwards

Rationale

What is tested? the readState transitions - UNREAD at construction, PARTIAL on markStarted, COMPLETE on markCompleted, and markStarted's UNREAD guard after completion.

How is success determined? the four observations in that order; the last markStarted leaves COMPLETE in place.

Why does it matter? the state becomes the state tag of adapter.response.body.read - a fully consumed body reported as partial would show payload discarded that was not. Given

Truncation at a character boundary

should drop an incomplete trailing UTF-8 sequence instead of decoding a replacement character

Rationale

What is tested? byte-bounded truncation of multi-byte text - the cap counts bytes, so it can split a character.

How is success determined? with a 2-byte cap over "hé" (3 bytes: 68 c3 a9) the logged prefix is "h", not "h�"; the byte count stays exact.

Why does it matter? a replacement character in the logged prefix is corruption the reader cannot distinguish from corrupt input. Given

should drop an incomplete trailing sequence of another variable-width charset

Rationale

What is tested? decodeTruncated with a charset other than UTF-8 - Shift_JIS "aあ" (61 82 a0) cut at 2 bytes leaves the lead byte 82 dangling; the underflow handling must be charset-generic.

How is success determined? the prefix is "a" followed by the note for 3 bytes total; the dangling lead byte yields no replacement character.

Why does it matter? the response body is decoded with the charset the peer declares, so the boundary logic must hold for every variable-width encoding, not only UTF-8.

should keep a complete multi-byte character that ends exactly at the cap

Rationale

What is tested? the boundary case of the truncation decoder - "éx" capped at 2 bytes ends exactly after the 2-byte é (c3 a9), so nothing is incomplete.

How is success determined? the prefix is the whole "é" followed by the note for 3 bytes total.

Why does it matter? the underflow handling must drop only an incomplete tail; dropping a complete character would lose a byte the cap admitted. Given

should still replace malformed bytes inside the prefix

Rationale

What is tested? the REPLACE action of the truncation decoder - 0xa9 is a stray continuation byte between "a" and "b", well inside the 3-byte cap.

How is success determined? the prefix renders "a�b" with the note for 4 bytes total; the malformed byte is replaced, not dropped, and does not abort the decoding.

Why does it matter? endOfInput=false must suppress only the trailing underflow; genuinely broken input must render as String(bytes, charset) would, so the log shows the corruption where it is. Given

ClientLoggingAutoConfigurationTest

9 tests.

should attach the interceptor to every RestClient builder and RestTemplate Boot hands out

Rationale

What is tested? the customizer path - the interceptor bean is only useful if Boot's builders carry it.

How is success determined? the RestClient.Builder bean's interceptor list and a built RestTemplate's interceptors both contain the module's interceptor, as their LAST entry.

Why does it matter? the shipped activation is this customizer, not the bean. Given/When

should back off entirely when disabled by the property

Rationale

What is tested? the class-level @ConditionalOnProperty on adapter-logging.enabled - with it false the whole configuration, including @EnableConfigurationProperties and the nested customizer classes, is skipped.

How is success determined? neither the interceptor, the defaults, the bound properties nor the customizer bean exists.

Why does it matter? the kill switch must remove every trace of the module, not only the log line - a leftover customizer or default bean would still shadow a host's own beans. Given/When

should bind the adapter-logging namespace

Rationale

What is tested? property binding of ClientLoggingProperties under the adapter-logging prefix - a scalar, a list, a nested header section and a boolean, through Boot's relaxed binding.

How is success determined? the bound bean reports logger-name, exclude-hosts, request-headers.masked and measure-response-body-size exactly as configured.

Why does it matter? the prefix and the nested section names are the documented configuration contract; a rename in the properties class would silently ignore an operator's YAML. Given/When

should keep the interceptor bean without the customizers when Boot's restclient module is absent

Rationale

What is tested? the optional-dependency boundary - a host wiring clients by hand still gets the bean to add.

How is success determined? with the customizer contracts hidden from the classloader, the context starts, the interceptor exists, no customizer bean does.

Why does it matter? an unconditional customizer would fail the context of every such host. Given/When

should key the default masker from the masking-key property

Rationale

What is tested? the property path to a guess-proof fingerprint - no host bean needed.

How is success determined? with masking-key set, the masker bean renders the keyed fingerprint, not the unkeyed default.

Why does it matter? keying is the documented answer to "masked is not a security boundary for guessable values"; it must be reachable from application.yml alone. Given/When

should let a host interceptor bean win and consume a host registry

Rationale

What is tested? the @ConditionalOnMissingBean back-off for the interceptor and the masker, with the host-defined interceptor still handed to the customizer and the host's MeterRegistry receiving the module's meters.

How is success determined? the single interceptor bean is the host's, Boot's builder carries it as the last interceptor, the host registry holds the three fail-open counters, and the single masker renders the host's "***".

Why does it matter? a host must be able to replace the interceptor or the masking policy without losing the customizer wiring, and the meters must land in the exported registry rather than a private one. Given/When

should let host time source and id generator beans back the defaults off

Rationale

What is tested? the @ConditionalOnMissingBean back-off for the two remaining collaborators, NanoTimeSource and CorrelationIdGenerator, with the host pinning both and nothing else - the interceptor and the masker stay the auto-configured defaults.

How is success determined? exactly one bean of each collaborator type, each the host's instance (same reference, host behaviour on a call), while the interceptor and the masker still exist exactly once.

Why does it matter? a deterministic clock and id generator are the documented override for a test profile and for a peer that insists on an id format; the back-off is what makes the host bean reach the interceptor's constructor injection instead of colliding with a second bean of the same type. Given/When

should register the interceptor, the defaults and both customizers

Rationale

What is tested? the default bean set of the auto-configuration in a plain (non-web) context with Boot's restclient auto-configurations present - interceptor, the three @ConditionalOnMissingBean defaults and both nested customizer configurations.

How is success determined? exactly one bean each of the interceptor, NanoTimeSource, CorrelationIdGenerator and HeaderValueMasker, plus the two named customizer beans.

Why does it matter? a missing default would fail the interceptor's constructor injection, a missing customizer would leave Boot's clients unlogged; the context runner has no web type, pinning that no web application is required. Given/When

should ship the auto-configuration through the imports resource

Rationale

What is tested? the META-INF/spring/...AutoConfiguration.imports resource of the module - the registration mechanism Boot 3+ uses instead of spring.factories.

How is success determined? the merged import lines on the classpath contain the fully qualified class name of ClientLoggingAutoConfiguration.

Why does it matter? the context-runner tests register the class explicitly; only this resource makes the module active by merely being on a host's classpath. Given/

ClientRequestLoggingInterceptorBodyAndHeaderTest

24 tests.

Header selection and masking

should key the built-in fingerprint from the properties when constructed without a masker

Rationale

What is tested? the masker default of the public four-argument constructor - the manual wiring path the guides recommend - derives from properties.maskingKey through HeaderValueMasker.forKey, exactly as the auto-configuration's default bean does.

How is success determined? with masking-key set and no masker passed, the masked Authorization value is the keyed HMAC fingerprint and NOT the unkeyed default fingerprint of the same value.

Why does it matter? a host that configured a secret and wired the interceptor by hand silently logged unkeyed, guessable fingerprints - the configured guess-resistance depended on how the interceptor was constructed instead of on the property.

should log selected request headers multi-value, mask the configured ones stably and include the sent correlation header

Rationale

What is tested? selection at wiring time from the OUTGOING headers - after the correlation header was added - multi-value joining, and the stable masking fingerprint.

How is success determined? Accept is joined with ", ", Authorization is the length:hash fingerprint (never the plaintext), the generated correlation header appears because it went out.

Why does it matter? what the line shows must be what the peer received. Given

should log the selected response headers as the peer sent them

Rationale

What is tested? response-side selection at emission from the SNAPSHOTTED headers - a wildcard include over the names the peer sent, an exclude that wins over it, and an unmasked name rendered in plaintext.

How is success determined? adapter_response_headers carries Content-Type with its plaintext value and no Set-Cookie at all, neither of its two values.

Why does it matter? the wildcard is the debugging move an operator reaches for; the exclude must remove a multi-value cookie completely, not only its first value, and the allowlist must not be confused with the exclude.

should mask every selected header by default so a wildcard include never leaks plaintext

Rationale

What is tested? ADR-0005 at the interceptor - the documented debugging move includes: ["*"] with nothing said about masking.

How is success determined? every logged request header is a fingerprint; the secret appears nowhere.

Why does it matter? with masking as a second, empty list the same configuration logged everything in plaintext - the unsafe combination was the convenient one. Given

should omit the header fields when nothing is selected

Rationale

What is tested? the shipped default sections (empty includes on both sides) - select short-circuits to an empty list and renderHeaders turns that into null.

How is success determined? neither adapter_request_headers nor adapter_response_headers is on the event although the request carried an Accept header.

Why does it matter? the default must log no header at all, and an empty "[]" field would look like a selection that found nothing rather than none configured.

should render masked values through a host-provided masker

Rationale

What is tested? the masker is an injected collaborator - the interceptor built with a host bean masks request AND response headers with it.

How is success determined? both selected, masked headers carry the host masker's output, never the plaintext and never the built-in fingerprint.

Why does it matter? a compliance regime forbidding unkeyed hashes must be satisfiable without forking the module. Given

Outcome-gated bodies

should log a decoding failure of the application as a successful exchange without bodies in on-failure mode

Rationale

What is tested? the boundary of the outcome gate - a 200 whose body the client's Jackson converter cannot map to the requested type. The converter fails ABOVE the interceptor, after every byte flowed through the tee; the exchange itself saw a clean stream.

How is success determined? the caller gets RestClientException; the single event is INFO with outcome success, status 200 and NO body fields - on-failure withholds them.

Why does it matter? this is the one case where the line's outcome and the caller's outcome differ, decided and documented (guide §6.3, ADR-0006): the module observes the wire, not the application's decoding, and there is no seam through which a converter failure could reach the interceptor. Pinned so a change here is a decision, not an accident. Given/When

should log both bodies of a 4xx answer although its outcome stays success

Rationale

What is tested? the gate is wider than the outcome vocabulary by one status class - a 4xx keeps its success outcome (the peer answered; the request was wrong) but is exactly the case a body explains.

How is success determined? outcome success, and BOTH bodies on the line.

Why does it matter? a validation error\'s response body is the most wanted body of all; hiding it behind the outcome vocabulary would make on-failure useless for client errors. Given/

should log both bodies of a 5xx answer in on-failure mode

Rationale

What is tested? loggedBodies for a failure outcome without an exception - a 502 classifies as failure, so both directions' on-failure gates admit their captured bytes.

How is success determined? outcome failure, adapter_request_body "sent" and adapter_response_body "upstream down" on one line.

Why does it matter? a 5xx is exactly the line an operator opens to see what was sent and what the peer answered; on-failure must not withhold either side there. Given/

should log the buffered request body of a call that threw in on-failure mode

Rationale

What is tested? the no-response completion path with on-failure - the request body was copied at wiring time before the wire call, so it is available for the event emitted from the catch block. The FIELD is documented as the body the client handed to the wire call, not as bytes that reached the peer (the size METER is the one that claims that, and stays silent here - see the metrics test).

How is success determined? the IOException propagates unchanged, the event carries outcome failure and the request body, and no response body key exists.

Why does it matter? for a call that never got an answer the request body is the only payload evidence there is; it must survive the exception path.

should log the raw body the converter read when the application's decoding fails in always mode

Rationale

What is tested? the same decoding failure with log-response-body=always - the tee logged what the converter read before it gave up.

How is success determined? the caller gets RestClientException; the single event carries the raw JSON as adapter_response_body, outcome success.

Why does it matter? always is the documented way to see what a peer really sent when the application cannot make sense of it - the body must be the bytes, not the failure. Given/When

should still measure the size of a body it withholds

Rationale

What is tested? the split between capturing and logging - on-failure plus measure-request-body-size on a successful call: recordBodySizes runs before the level and outcome gates, loggedBodies then drops the field.

How is success determined? the request body size summary records 4 bytes while the event carries no adapter_request_body.

Why does it matter? the size metric must not depend on whether the body reached the line; an operator can measure payloads without paying for their log volume.

should withhold both bodies from a successful exchange in on-failure mode

Rationale

What is tested? the volume switch - on-failure captures (the outcome is unknown while the bytes flow) and discards at emission when the outcome is success.

How is success determined? the application receives the response body; the line carries neither body.

Why does it matter? this is the mode that keeps body logging affordable outside a debug session. Given/When

Request body

should decode the request body with the declared charset

Rationale

What is tested? declaredCharsetOrUtf8 on the request headers at wiring time - the charset parameter of the Content-Type selects how the captured bytes are decoded.

How is success determined? an ISO-8859-1 encoded "café" declared as such is logged as "café", not as a mis-decoded UTF-8 rendering.

Why does it matter? a body decoded with the wrong charset shows mojibake for every non-ASCII character, which reads like corrupt data on the wire.

should log the request body the client hands the interceptor

Rationale

What is tested? the request-side capture in always mode - the byte array the client passes to intercept is copied at wiring time and decoded with the JSON Content-Type's default charset.

How is success determined? adapter_request_body carries the JSON exactly as sent.

Why does it matter? the blocking clients hand the interceptor the complete, final body - the line must show what actually went out, not what the caller intended to send. Given

should omit the request body key for a bodiless request

Rationale

What is tested? a zero-length body array through the always-mode request capture - totalBytes stays 0 and loggedValue returns null.

How is success determined? no adapter_request_body key on the event.

Why does it matter? a GET without a body must not show an empty body field, which a dashboard would count as a body that was sent. Given/When

should truncate the logged request body at the capture limit and say so

Rationale

What is tested? max-body-bytes applied to the request capture - the wiring copies the whole array into a 4-byte capture, so only the prefix is buffered while the total is counted.

How is success determined? adapter_request_body is the 4-byte prefix followed by the "[truncated, 10 bytes total]" note.

Why does it matter? a large upload must never land in the log line in full, and the reader must see that the value is cut and how large the body really was.

Response body tee

should decode the response body with the charset the peer declared

Rationale

What is tested? declaredCharsetOrUtf8 on the snapshotted response headers at emission - the peer's Content-Type charset decodes the captured bytes.

How is success determined? a Latin-1 encoded "café" declared as ISO-8859-1 is logged as "café".

Why does it matter? peers do declare legacy charsets; decoding them as UTF-8 would render every accented character as a replacement and hide the real payload.

should log exactly the prefix the application read of a partially consumed body

Rationale

What is tested? the tee mirrors consumption, not transmission - the application reads 3 of 6 bytes and closes; the capture holds only what flowed through read().

How is success determined? adapter_response_body is "abc" without a truncation note, since the capture saw no more bytes than it buffered.

Why does it matter? an early-exiting parser leaves bytes on the wire the log must not invent; the truthful count is what the size summary records as well.

should log the response body the application actually read

Rationale

What is tested? the response tee through consumeAndClose - the application reads the stream to EOF, every byte is copied into the capture, and the emission at close renders it.

How is success determined? the application receives "hello" unchanged and adapter_response_body carries the same "hello".

Why does it matter? the tee is a passive copy - it must neither alter what the converters see nor log anything the converters did not read. Given

should not attach a capture when neither logging nor measuring is on

Rationale

What is tested? the zero-cost default path - with body logging and measuring off, no capture buffers or counts the body (the read-failure reporting wrapper exists either way).

How is success determined? the returned response reports no capture; the body passes through unchanged; no body field on the event.

Why does it matter? an absent key alone cannot observe this (a capture with logging off also yields no key) - the seam is what proves no capture was installed.

should omit the response body key when the application never opened the body

Rationale

What is tested? the tee's truthfulness - a body the application never read flows nowhere.

How is success determined? no adapter_response_body key, rather than an empty or fabricated value.

Why does it matter? 'logged' must mean 'actually flowed'. Given/

should record the read state of the response body as unread, partial or complete

Rationale

What is tested? the observation points of the read state - opening the stream marks PARTIAL, observing EOF marks COMPLETE, never opening it leaves UNREAD.

How is success determined? three exchanges, three states, read off the captures.

Why does it matter? the state is the one signal that tells a discarded response body from an absent one. Given

should truncate the logged response body at the capture limit and keep the exact total

Rationale

What is tested? max-body-bytes on the response tee - the capture buffers the first 4 bytes the application reads and counts the remaining 6.

How is success determined? adapter_response_body is the 4-byte prefix plus the "[truncated, 10 bytes total]" note.

Why does it matter? a large download must cost at most max-body-bytes of log line and memory, and the note must state the real size the application consumed. Given

ClientRequestLoggingInterceptorIntegrationTest

8 tests.

should log a 5xx answer as WARN failure with the body the client read for its exception

Rationale

What is tested? the emission point at response close against the real client - RestClient's default status handler reads the 500 body to build its HttpServerErrorException, and that read passes through the tee before the close triggers the event.

How is success determined? the client throws HttpServerErrorException; the single event is WARN with outcome failure, status 500 and adapter_response_body "boom".

Why does it matter? emitting when the interceptor returns would log an empty body for exactly the answers an operator most wants to read; only a real client proves the close comes after the handler's read. Given

should log a RestTemplate call through the same interceptor without a template

Rationale

What is tested? the RestTemplateCustomizer path end to end - Boot's RestTemplateBuilder carries the interceptor, the expanded URI is logged, and RestTemplate sets no uriTemplate attribute, so the field stays absent.

How is success determined? the echo body arrives, the event carries adapter_url_path /things/9 and no adapter_url_template, and the peer received a generated correlation header.

Why does it matter? RestTemplate is still the client of most existing code; it must be logged by the same interceptor with the same identity contract, and the template field must not show a fabricated value.

should log a bodiless 204 without body fields

Rationale

What is tested? a bodiless consumption on a real stream - toBodilessEntity closes the 204 without opening the body, so both captures stay at zero bytes although body logging is always on.

How is success determined? status 204 on the entity and the event; neither adapter_request_body nor adapter_response_body is present.

Why does it matter? a 204 is the routine answer of every delete and update; an empty body field on each of them would be noise that looks like a payload. Given

should log a read timeout as WARN timeout the way the JDK engine raises it

Rationale

What is tested? the timeout classification against the REAL engine's exception type.

How is success determined? with a 200 ms read timeout against a peer that answers after 1.5 s, the client throws and the single event is WARN with outcome timeout and no status.

Why does it matter? the classification walks the cause chain by type and name; only a real engine proves the names are the ones that actually occur. Given

should log a refused connection as ERROR failure without a status

Rationale

What is tested? the no-response completion path with the real JDK engine - the connect fails inside execution.execute, the catch block completes the exchange and rethrows, and RestClient wraps the IOException.

How is success determined? the client throws ResourceAccessException; the single event is ERROR with outcome failure, a "-> -" status placeholder in the message, no status field and the cause attached.

Why does it matter? a peer that is down produces no response object at all - the event must still exist, carry the cause, and not pretend a status it never received.

should log one complete event for a real call including template, headers and bodies

Rationale

What is tested? the full happy path through Boot's builder and the JDK engine - the customizer attached the interceptor, the template attribute is recorded, the response body is teed on a real stream, and the generated correlation header went out on the wire.

How is success determined? the peer saw the request with the correlation header; one INFO event with the client field family, format-identical to the WebClient twin.

Why does it matter? only a real client and a real engine prove the registration and the stream handling hold outside the mocks. Given

should log the offered request body of a refused call but record no size sample for it

Rationale

What is tested? the decoupled request-body contracts on the no-response path with the real engine - the FIELD shows the body the client handed to the wire call (evidence), the size METER records nothing because no response proves any byte reached the peer.

How is success determined? the event carries adapter_request_body "hello" and outcome failure; no adapter.request.body.size summary exists for the refused host.

Why does it matter? a size sample for a body the peer never saw would inflate payload distributions with every outage; the field, by contrast, is the only payload evidence a failed call leaves.

should measure the request body the peer received and count a byte array answer as complete

Rationale

What is tested? the two body-meter seams against the real client - the request sample is recorded because a response came back (the peer's record proves the bytes went out), and the response read state is COMPLETE although RestClient deserialises the answer as a byte[] through ByteArrayHttpMessageConverter, which reads exactly Content-Length bytes and never asks for the EOF.

How is success determined? the peer received "hello"; the request summary under the template records 5 bytes; adapter.response.body.read counts 1 under state=complete and nothing under state=partial for that template.

Why does it matter? only the real engine and the real converter prove the observation points (write proven by a response; completion by the declared length) hold outside the mocks. Given

ClientRequestLoggingInterceptorTest

31 tests.

Activation and start line

should announce the call before the wire call when enabled

Rationale

What is tested? logRequestStart with log-request-start on - the arrival line is emitted in intercept before execution.execute, under the exchange MDC, and the completion line follows at close.

How is success determined? during the execution the log holds exactly the "started" line; afterwards there are two events, the first without adapter_outcome but with the host and the request id in its MDC, the last with outcome success.

Why does it matter? an operator watching a hung call needs the line before the answer, and the arrival line must stay invisible to outcome-keyed dashboards.

should be active only for paths matching an include pattern and let an exclude win

Rationale

What is tested? ClientActivation.shouldNotFilter with an include pattern and an exclude prefix - a path outside the include is skipped, a path inside it but under the exclude prefix is skipped too.

How is success determined? of three calls only /api/things produces an event; the static asset and /api/internal/jobs are passed through unlogged.

Why does it matter? activation scoping is how an operator keeps noisy or sensitive routes out of the log, and an include that could override an exclude would be a silent leak.

should match activation on the decoded path segments so an encoded variant cannot slip past an exclude

Rationale

What is tested? activation sees the path the way a server router would - segments decoded for matching, once.

How is success determined? /%61pi/things is included by /api/** and logs the raw path; /api%2Fthings is NOT (one segment "api/things"); /%61ctuator/health is excluded.

Why does it matter? an exclude that an encoded spelling bypasses is not an exclude. Given

should not log a call to an excluded host at all

Rationale

What is tested? the client-side exclusion - by peer host, case-insensitively.

How is success determined? the call passes, nothing is logged, no correlation header is added.

Why does it matter? calls to a metrics gateway or a config server must be silenceable without knowing their paths. Given

should reject an invalid include pattern at construction time

Rationale

What is tested? the eager parse of include-path-patterns in ClientActivation - the PathPatternParser runs in the interceptor's constructor, not per call.

How is success determined? the constructor throws the parser's own PatternParseException whose detailed string names the malformed pattern.

Why does it matter? a configuration error must fail the context start with a readable message instead of failing, or silently skipping, every call at runtime. Given/When

Identity per ADR-0002

should adopt a correlation id already on the request and leave it untouched

Rationale

What is tested? ClientIdentity.resolve on a traceless request whose correlation header passes CorrelationHeader.accept - the header value becomes the request id and sendCorrelationHeader is false.

How is success determined? the header still holds exactly the one caller value, and the MDC and the message carry "caller-id".

Why does it matter? an id propagated from the inbound request must join the server line and the peer's line; overwriting or duplicating it would break the chain the caller established.

should fall back to the correlation contract when the traceparent is not conformant

Rationale

What is tested? an invalid traceparent counts as ABSENT (ADR-0002) - the traceless contract applies in full.

How is success determined? a fresh id is generated and sent, no trace decoration is emitted.

Why does it matter? half-trusting a malformed header would mint a request id from bytes the W3C validation rejected - the strict parser is the single gate for both the trace fields and the identity decision.

should generate a correlation id and SEND it on a traceless request without one

Rationale

What is tested? the outbound mirror of the inbound echo - a traceless call without a correlation header gets one added so the peer can quote it.

How is success determined? the header is on the outgoing request, the event carries the same id.

Why does it matter? without it the peer's own log line and this line share no identity. Given

should own the trace keys at emission so a stale bridge id on the thread cannot ride along

Rationale

What is tested? the emission scope OWNS traceId/spanId - on a traceless call an ambient traceId (a bridge's, from the server span) is removed for the event and restored after.

How is success determined? the event carries no traceId; the thread has it back afterwards.

Why does it matter? a stale id would join the client event to a trace the call was not part of.

should use the traceparent trace id as the request id and add no correlation header

Rationale

What is tested? the identity decision of ADR-0002 on the outbound side - a conformant traceparent's trace id IS the request id, a correlation header the caller added is ignored, and NO correlation header is added.

How is success determined? adapter_request_id equals the trace id in MDC and message; the request carries exactly the caller's headers.

Why does it matter? a client logger must be observationally neutral - on a traced call the wire already carries the identity, and adding a second, private id would make enabling the logger visible to the peer.

Levels and outcomes

should classify a failure while reading the body with the status already received

Rationale

What is tested? the read-side failure - the status line arrived, the body read then died.

How is success determined? the IOException propagates from the read; at close the event is ERROR, outcome failure, WITH the 200 that was received, cause attached.

Why does it matter? "200 but failed" is exactly what happened; hiding either half misleads.

should compare the slow threshold at full precision instead of truncated milliseconds

Rationale

What is tested? the threshold comparison - a 1.5 ms threshold must not truncate to 1 ms and flag a 1 ms call.

How is success determined? 1.0 ms is NOT slow, 1.5 ms IS slow, under a 1.5 ms threshold.

Why does it matter? truncating both sides inflates WARN logs for every threshold with sub-millisecond precision.

should escalate to WARN and flag a slow but successful call

Rationale

What is tested? the slow escalation in emitExchange - the injected clock advances by exactly the 200 ms threshold during the call, so the Duration comparison with >= holds while the classification stays success.

How is success determined? the event is WARN, carries adapter_slow true and keeps adapter_outcome success.

Why does it matter? severity and outcome are decoupled on purpose - a slow peer must show up on the level without being counted as a failure.

should escalate to WARN with outcome failure for a 5xx answer

Rationale

What is tested? classify for a response without an exception and a status >= 500 - the level is WARN, the outcome failure, the status kept.

How is success determined? one WARN event with "-> 503" in the message, adapter_outcome failure and status field 503.

Why does it matter? a 5xx is the peer's failure, not a broken call - WARN keeps it visible without paging on every upstream hiccup while the outcome tag still counts it as failed.

should log ERROR with outcome failure and no status when the call throws

Rationale

What is tested? the no-response path - the engine threw before a status line arrived.

How is success determined? the exception propagates unchanged; one ERROR event with the cause, -&gt; - in the message and no status field; emitted right away, there is nothing to close.

Why does it matter? a call that never got an answer must still be one line, with the truth about the missing status rather than an invented one.

should log WARN with outcome timeout when the call times out

Rationale

What is tested? the client-side disposition worth its own value - a timeout in the cause chain classifies the failure as timeout at WARN.

How is success determined? outcome timeout, WARN, cause attached, no status.

Why does it matter? an operator reads "the peer is slow" differently from "the call is broken".

should log a WARN breadcrumb on the module logger when the call throws

Rationale

What is tested? the breadcrumb of the catch block - a throwing execution logs one WARN line on the interceptor's own logger, separate from the ERROR event on the exchange logger.

How is success determined? exactly one WARN on the module logger with method, target, the exception's toString and the request id; the exchange logger still has exactly one event.

Why does it matter? the exchange log stream keeps its one-event-per-exchange contract for parsers, while the module logger shows the failure with its cause where the twins' streams look alike.

should measure the duration until close including the body read

Rationale

What is tested? duration = response occupancy, not bare round-trip time.

How is success determined? time spent between the interceptor returning and the close is included.

Why does it matter? a slow body read is the peer's slowness too; the inbound twin measures request occupancy for the same reason.

Read failures, errors and re-entries

should classify a body that could not even be opened as a failure with the received status

Rationale

What is tested? the engine call that OPENS the body stream (getBody throws IOException) is guarded like a read - not only the reads on the stream.

How is success determined? the exception reaches the application unchanged; at close the event is ERROR, outcome failure, WITH the 200 that was received and the cause attached.

Why does it matter? a body that failed to open logged as success is wrong on exactly the calls the outcome exists for.

should classify a response whose close throws as a failure and still complete exactly once

Rationale

What is tested? the close path of the response wrapper - the delegate's close throws (a pooled connection that cannot be returned); the exception is recorded on the exchange BEFORE the completion in the finally emits, and rethrown unchanged.

How is success determined? close() throws the IOException; exactly one event, ERROR, outcome failure WITH the 200 that was received, the close exception as its cause; a second close is a no-op.

Why does it matter? previously the success event was written immediately before the very exception the caller then saw - the two records of one exchange contradicted each other. Given

should classify a response whose status the client cannot read as a failure

Rationale

What is tested? the guard around the metadata accessors of the response wrapper - the snapshot at handover tolerates a refusing engine (status -, counted as wiring), but the CLIENT's own later getStatusCode propagates and must mark the exchange failed.

How is success determined? the client's status access throws unchanged; at close the event is ERROR, outcome failure, without a status field, the cause attached.

Why does it matter? the caller experienced a failed exchange; a success line for it would contradict the exception the caller is handling at that very moment.

should classify an unchecked exception thrown while reading the body as a failure

Rationale

What is tested? the read guard covers ANY exception, not only IOException - an engine's unchecked wrapper (UncheckedIOException, a runtime decoding error) is a failed read too.

How is success determined? rethrown unchanged; ERROR with outcome failure at close.

Why does it matter? an engine that wraps its I/O errors would otherwise log every dropped connection as a success.

should close the gauge without an event when the wire call dies with an Error

Rationale

What is tested? the Throwable boundary decided in FailOpenDiagnostics - an Error from the execution (an inner interceptor's AssertionError, a LinkageError in the engine) is outside the fail-open promise, but the open-exchange gauge must not drift over it.

How is success determined? the Error propagates unchanged, no exchange event is emitted, the gauge is back at zero.

Why does it matter? a permanently open exchange on the gauge is a false "never closed" baseline - the liveness signal crying wolf forever. Given/When

should keep counting the id as generated when a retrying outer interceptor re-enters with it

Rationale

What is tested? the origin counter across re-entries - attempt 1 generated and stamped the header, attempt 2 finds that header on the SAME request.

How is success determined? both attempts count generated, none header; the id is the same on both.

Why does it matter? counting the module's own write as propagation would dilute the very signal the counter exists for (a rising generated share).

should replace a correlation header outside the acceptance rule with a generated id

Rationale

What is tested? the CorrelationHeader rule at the interceptor - a value with control characters (or over-long, or non-ASCII) counts as absent.

How is success determined? the generated id goes on the wire INSTEAD of the foreign value, the event and the MDC carry the generated id, the origin counts generated.

Why does it matter? the value lands verbatim in the message and the MDC of every line of the call - a CR/LF in it forges lines in every plain-text sink.

The exchange line

should carry the identity in the MDC while the wire call runs as an additive overlay

Rationale

What is tested? the call scope - inner interceptors and the HTTP engine log under the client identity, and the ambient MDC (an inbound request's keys, a bridge's keys) stays.

How is success determined? inside the execution all three adapter_* keys are set beside a seeded ambient key; after the call the client keys are gone and the ambient key remains.

Why does it matter? the client line and every log line of the call must join the server line by MDC alone, and a pooled thread must not keep the identity.

should emit only when the response is closed and exactly once

Rationale

What is tested? the emission point - response close - and its exactly-once guard.

How is success determined? nothing is logged while the response is open (the body may still be read); the first close logs, a second close logs nothing more.

Why does it matter? emitting when the interceptor returns would log a body of zero bytes and a duration without the read; a double close (a client's finally after an explicit close) must not double the event.

should log a slash path for a URI without a path

Rationale

What is tested? RequestTarget.of on a bare authority - an empty rawPath is normalised to "/" in both the target and the path field.

How is success determined? the message reads "https://api.example.com/ -> 200" and adapter_url_path is "/".

Why does it matter? the engine sends "GET / HTTP/1.1" for such a URI; an empty path field would break grouping by path and make the target look truncated.

should log one line with the client field family at response close

Rationale

What is tested? the format contract - message, adapter_* key-values and MDC of the completion event, emitted when the client CLOSES the response.

How is success determined? the exact message string, the full field family for a successful GET, the request id in the MDC; 42 ms of measured work between send and close.

Why does it matter? the line is the module's product; the WebClient twin's test asserts the identical format, so this pins one half of the twin contract.

should log query, port and URI template as their own fields

Rationale

What is tested? the URL coordinates RequestTarget and the wiring split off the request - host with its explicit port, raw path, the query kept because include-query-string is on, and the uriTemplate request attribute RestClient records.

How is success determined? the message names the target without the query; adapter_url_host is "localhost:8081", adapter_url_path "/things/7", adapter_url_query "page=2" and adapter_url_template the template string.

Why does it matter? dashboards group by host and template, not by expanded URL - the fields must be split exactly so, and the query must stay out of the message and the MDC route.

should log the raw request target so percent-encoded control characters cannot forge log lines

Rationale

What is tested? the log-injection guard for the raw request target - java.net.URI decodes getPath()/getQuery(), so %0A in the target would become a real line break in the message, the MDC route and the fields.

How is success determined? path and query appear percent-encoded as sent in the message, in the adapter_url_path/adapter_url_query fields and in the adapter_route MDC entry; no sink contains a line break.

Why does it matter? a URL assembled from untrusted input could otherwise forge complete exchange lines in every plain-text appender.

ClientRequestLoggingMetricsTest

14 tests.

Body meters

should count a body the ByteArray converter reads to its declared length as complete

Rationale

What is tested? the declared-length completion rule - Spring's ByteArrayHttpMessageConverter reads exactly Content-Length bytes with readNBytes and never asks for the EOF; the capture learns the declared length at handover and completes when the count reaches it.

How is success determined? the real converter reads the 6-byte body; adapter.response.body.read counts 1 under state=complete and nothing under state=partial.

Why does it matter? before the rule every byte[] answer counted as partial - a dashboard alarm for abandoned body processing fired on healthy calls.

should count an unread response body and record no size sample for it

Rationale

What is tested? recordBodySizes for a response the application closed without opening the body - the capture stays UNREAD at zero bytes, a response exists, and no template attribute was recorded.

How is success determined? adapter.response.body.read counts 1.0 under uri=UNKNOWN, the peer host and state=unread; no response body size summary exists because recordBodySize skips zero bytes.

Why does it matter? the read-state counter is the one place a discarded payload becomes visible - the size summary cannot show it, and a zero sample there would distort the distribution of bodies that exist.

should fold a malformed Content-Length to unknown without counting a wiring failure

Rationale

What is tested? the peer-controlled header at the completeness seam - Spring parses Content-Length with Long.parseLong, so a non-numeric value throws; declaredBodyLength must fold it to UNKNOWN_LENGTH itself instead of letting the snapshot's catch count and warn.

How is success determined? the status is on the event, no failopen{stage=wiring} increment, no warning on the interceptor's logger, and the EOF rule alone decides: a length-exact read without an EOF counts partial.

Why does it matter? a peer must not be able to raise a warning and a fail-open count per answer with one garbage header; the header may only ever feed the comparison.

should keep counting a length-exact read as partial when a Content-Encoding makes the length untrustworthy

Rationale

What is tested? the conservative side of the declared-length rule - with a Content-Encoding on the response an engine may hand the application a decoded body of another length, so the capture must not trust Content-Length and falls back to the EOF.

How is success determined? the same length-exact read as above counts state=partial.

Why does it matter? a wrong complete is worse than a conservative partial - the rule may only fire where the declared length is the length the application reads.

should not record a read state when the call produced no response

Rationale

What is tested? the exchange.response != null guard in recordBodySizes - a call that threw before a status line leaves the measuring-mode capture with nothing to consume.

How is success determined? no adapter.response.body.read counter is created at all after the refused call.

Why does it matter? counting such a call as unread would blame the application for discarding a body the peer never sent, inflating exactly the share the counter exists to flag. Given

should not record a request body size sample when the call produced no response

Rationale

What is tested? the exchange.response != null guard on the REQUEST sample in recordBodySizes - the interceptor copies the serialized body before the wire call, and a refused connection means none of it reached the peer.

How is success determined? after a refused POST with a 4-byte body no adapter.request.body.size summary exists; after an answered POST with the same body the summary records 4.

Why does it matter? the meter is documented as bytes that actually flowed; a sample for a body the peer never saw would inflate payload distributions with every outage and make the twin comparison lie (the reactive twin tees at the connector write). Given

should record body sizes and the response read state under template and host, independent of the level gate

Rationale

What is tested? the opt-in body meters - sizes per direction tagged by template and host, plus the response read state; all recorded although the logger is OFF.

How is success determined? request 5 bytes, response 6 bytes, one complete count under the tags.

Why does it matter? a metric must not depend on how loud the logger is configured. Given

Counters and gauge

should count the request-id origin per source

Rationale

What is tested? metrics.requestId with the source ClientIdentity.resolve decides - a conformant traceparent counts as trace, an acceptable correlation header as header, neither as generated.

How is success determined? after one call of each kind the adapter.logging.correlation.id counter holds exactly 1.0 under each of the three source tags.

Why does it matter? a rising generated share is the only signal that the application stopped propagating its trace or correlation header onto outbound calls. Given/

should keep the open-exchanges gauge up until the response is closed

Rationale

What is tested? the gauge as the liveness signal of the close-based emission.

How is success determined? 1 while the response is open (body unread or read), 0 after close; a failed call goes up and down within the call.

Why does it matter? a response that is never closed must stay VISIBLE - the gauge baseline is the only signal for that silent-loss mode. Given/When

should pre-register the outcome vocabulary and count emitted events per outcome

Rationale

What is tested? every fixed-tag meter exists at zero before the first call, and the events counter counts emitted events by outcome.

How is success determined? success/failure/timeout exist at zero, the gauge under client=restclient; one call each moves its side.

Why does it matter? a rate() alert must see the zero before the first occurrence; the client tag is what keeps this twin's gauge apart from the reactive twin's in one host.

should share one metrics owner between two interceptors on the same registry

Rationale

What is tested? the per-registry ownership - a second interceptor against the SAME registry observes through the shared owner, not through a duplicate whose gauge registration Micrometer would silently ignore.

How is success determined? an exchange handled by the SECOND interceptor moves the registry's gauge to 1 mid-flight and back to 0 at close.

Why does it matter? with a duplicate owner the second interceptor's live calls were invisible. Given

Fail-open stages

should confine an arrival-line backend failure and count stage arrival

Rationale

What is tested? the arrival guard's coverage - the logger-level gate is a backend call and must sit INSIDE the fail-open guard.

How is success determined? with a logging backend whose level check throws (a throwing TurboFilter - logback consults turbo filters inside isInfoEnabled), the call is served untouched and the loss is counted as stage arrival; the completion event still follows.

Why does it matter? the arrival line is OPTIONAL observability; it failing the call would invert the module's central contract. Given

should count a broken emission as stage emission and never disturb the close

Rationale

What is tested? the emission guard covers the whole emission including its pre-gate section - here the injected time source throws on its emission-time read.

How is success determined? close() returns normally, no event, emission=1, gauge back at zero.

Why does it matter? the emission counter is the metric channel for exactly this loss.

should degrade to a pass-through and count stage wiring when the id generator throws

Rationale

What is tested? the wiring guard - a host-provided bean throwing at wiring time.

How is success determined? the call succeeds untouched, no event, wiring=1, gauge untouched.

Why does it matter? a logging component must never fail the call it describes. Given

ClientRequestLoggingTracingIntegrationTest

2 tests.

should join the event with the caller trace and add no correlation header on a traced call

Rationale

What is tested? the trace contract against a real bridge - an outer span is active, the client observation creates the child span and injects traceparent, the interceptor sees it.

How is success determined? the peer received a traceparent and NO X-Correlation-Id; the event's traceId is the outer span's trace id, its spanId is a DIFFERENT span (the client span), and the request id is the trace id.

Why does it matter? this is the observational neutrality and the log-to-trace join ADR-0002 promises, proven where the header actually comes from.

should treat a call without an outer span as traced too because the client observation roots a trace

Rationale

What is tested? the boundary an operator must know - with tracing configured, EVERY call is traced: the client observation roots a trace when none is active, so the module never generates a correlation id in such a host.

How is success determined? traceparent on the wire, no correlation header, trace keys on the event.

Why does it matter? a dashboard counting generated ids would otherwise be read as a propagation regression. Given/When

HttpComponentsRequestFactoryIntegrationTest

6 tests.

should log a 5xx answer through this request factory as WARN failure with the body

Rationale

What is tested? a peer answer the client turns into an exception - the response is still closed by the client, so the exchange completes with the status and the body it read.

How is success determined? the client throws HttpServerErrorException; the single event is WARN with outcome failure, status 500 and the body "boom".

Why does it matter? the 5xx path runs the engine's stream through the exception's body read; an engine that closed the stream early would lose the body on the one line that needs it. Given/When

should log a gzip answer the way this request factory hands it to the application

Rationale

What is tested? a Content-Encoding: gzip answer with the COMPRESSED length declared - the tee mirrors what the engine delivers, and the Content-Encoding rule keeps the capture from trusting a declared length the engine may have made meaningless by decompressing.

How is success determined? one INFO success event with status 200; when the engine decompresses transparently, application and log both see the plaintext; otherwise both see the compressed bytes and the plaintext appears nowhere on the line.

Why does it matter? the engines differ here, and an operator reading adapter_response_body must know which of the two a given engine produces - the contract pins it per engine instead of letting a deployment discover it. Given/When

should log a refused connection through this request factory as ERROR failure and not as a timeout

Rationale

What is tested? the control - a connection the peer actively refuses must stay a failure, whatever type this engine wraps it in.

How is success determined? ResourceAccessException; ERROR, outcome failure, -&gt; -, no status, the cause attached.

Why does it matter? a refusal misread as a timeout would send an operator looking for a slow peer that is in fact down. Given/When

should log this request factory's connect timeout as WARN timeout

Rationale

What is tested? the classification against the exception this engine raises when the TCP connect never completes - for some engines a different type than its read timeout.

How is success determined? against a tarpit that never completes the handshake, with a 200 ms connect timeout, the single event is WARN with outcome timeout, -&gt; - and no status.

Why does it matter? an unreachable peer is a timeout to an operator, not a refusal; the two dispositions call for different reactions. Given

should log this request factory's read timeout as WARN timeout

Rationale

What is tested? the classification against the exception this engine really raises when the peer's answer does not arrive in time, wrapped in RestClient's ResourceAccessException.

How is success determined? against a peer answering after 1.5 s with a 200 ms read timeout, the call throws ResourceAccessException and the single event is WARN with outcome timeout and no status.

Why does it matter? the timeout types differ per engine; only the real engine proves the list in Timeouts covers it through the client's wrapper. Given/When

should tee both bodies and send the correlation header through this request factory

Rationale

What is tested? the seams that touch the engine - the request body the interceptor is handed, the response tee on the engine's own stream (including the EOF or Content-Length observation that marks the read complete) - and the header the interceptor added to the request the engine sent.

How is success determined? the peer saw the correlation header and the body; one INFO event carries template, both bodies and the id, format-identical across engines; the read-state counter for this peer shows exactly one complete read.

Why does it matter? an engine whose stream never returned the EOF the tee waits for, or that handed out a body the converter reads differently, would log a partial read or empty bodies without any other symptom. Given/When

JdkClientRequestFactoryIntegrationTest

6 tests.

should log a 5xx answer through this request factory as WARN failure with the body

Rationale

What is tested? a peer answer the client turns into an exception - the response is still closed by the client, so the exchange completes with the status and the body it read.

How is success determined? the client throws HttpServerErrorException; the single event is WARN with outcome failure, status 500 and the body "boom".

Why does it matter? the 5xx path runs the engine's stream through the exception's body read; an engine that closed the stream early would lose the body on the one line that needs it. Given/When

should log a gzip answer the way this request factory hands it to the application

Rationale

What is tested? a Content-Encoding: gzip answer with the COMPRESSED length declared - the tee mirrors what the engine delivers, and the Content-Encoding rule keeps the capture from trusting a declared length the engine may have made meaningless by decompressing.

How is success determined? one INFO success event with status 200; when the engine decompresses transparently, application and log both see the plaintext; otherwise both see the compressed bytes and the plaintext appears nowhere on the line.

Why does it matter? the engines differ here, and an operator reading adapter_response_body must know which of the two a given engine produces - the contract pins it per engine instead of letting a deployment discover it. Given/When

should log a refused connection through this request factory as ERROR failure and not as a timeout

Rationale

What is tested? the control - a connection the peer actively refuses must stay a failure, whatever type this engine wraps it in.

How is success determined? ResourceAccessException; ERROR, outcome failure, -&gt; -, no status, the cause attached.

Why does it matter? a refusal misread as a timeout would send an operator looking for a slow peer that is in fact down. Given/When

should log this request factory's connect timeout as WARN timeout

Rationale

What is tested? the classification against the exception this engine raises when the TCP connect never completes - for some engines a different type than its read timeout.

How is success determined? against a tarpit that never completes the handshake, with a 200 ms connect timeout, the single event is WARN with outcome timeout, -&gt; - and no status.

Why does it matter? an unreachable peer is a timeout to an operator, not a refusal; the two dispositions call for different reactions. Given

should log this request factory's read timeout as WARN timeout

Rationale

What is tested? the classification against the exception this engine really raises when the peer's answer does not arrive in time, wrapped in RestClient's ResourceAccessException.

How is success determined? against a peer answering after 1.5 s with a 200 ms read timeout, the call throws ResourceAccessException and the single event is WARN with outcome timeout and no status.

Why does it matter? the timeout types differ per engine; only the real engine proves the list in Timeouts covers it through the client's wrapper. Given/When

should tee both bodies and send the correlation header through this request factory

Rationale

What is tested? the seams that touch the engine - the request body the interceptor is handed, the response tee on the engine's own stream (including the EOF or Content-Length observation that marks the read complete) - and the header the interceptor added to the request the engine sent.

How is success determined? the peer saw the correlation header and the body; one INFO event carries template, both bodies and the id, format-identical across engines; the read-state counter for this peer shows exactly one complete read.

Why does it matter? an engine whose stream never returned the EOF the tee waits for, or that handed out a body the converter reads differently, would log a partial read or empty bodies without any other symptom. Given/When

JettyRequestFactoryIntegrationTest

6 tests.

should log a 5xx answer through this request factory as WARN failure with the body

Rationale

What is tested? a peer answer the client turns into an exception - the response is still closed by the client, so the exchange completes with the status and the body it read.

How is success determined? the client throws HttpServerErrorException; the single event is WARN with outcome failure, status 500 and the body "boom".

Why does it matter? the 5xx path runs the engine's stream through the exception's body read; an engine that closed the stream early would lose the body on the one line that needs it. Given/When

should log a gzip answer the way this request factory hands it to the application

Rationale

What is tested? a Content-Encoding: gzip answer with the COMPRESSED length declared - the tee mirrors what the engine delivers, and the Content-Encoding rule keeps the capture from trusting a declared length the engine may have made meaningless by decompressing.

How is success determined? one INFO success event with status 200; when the engine decompresses transparently, application and log both see the plaintext; otherwise both see the compressed bytes and the plaintext appears nowhere on the line.

Why does it matter? the engines differ here, and an operator reading adapter_response_body must know which of the two a given engine produces - the contract pins it per engine instead of letting a deployment discover it. Given/When

should log a refused connection through this request factory as ERROR failure and not as a timeout

Rationale

What is tested? the control - a connection the peer actively refuses must stay a failure, whatever type this engine wraps it in.

How is success determined? ResourceAccessException; ERROR, outcome failure, -&gt; -, no status, the cause attached.

Why does it matter? a refusal misread as a timeout would send an operator looking for a slow peer that is in fact down. Given/When

should log this request factory's connect timeout as WARN timeout

Rationale

What is tested? the classification against the exception this engine raises when the TCP connect never completes - for some engines a different type than its read timeout.

How is success determined? against a tarpit that never completes the handshake, with a 200 ms connect timeout, the single event is WARN with outcome timeout, -&gt; - and no status.

Why does it matter? an unreachable peer is a timeout to an operator, not a refusal; the two dispositions call for different reactions. Given

should log this request factory's read timeout as WARN timeout

Rationale

What is tested? the classification against the exception this engine really raises when the peer's answer does not arrive in time, wrapped in RestClient's ResourceAccessException.

How is success determined? against a peer answering after 1.5 s with a 200 ms read timeout, the call throws ResourceAccessException and the single event is WARN with outcome timeout and no status.

Why does it matter? the timeout types differ per engine; only the real engine proves the list in Timeouts covers it through the client's wrapper. Given/When

should tee both bodies and send the correlation header through this request factory

Rationale

What is tested? the seams that touch the engine - the request body the interceptor is handed, the response tee on the engine's own stream (including the EOF or Content-Length observation that marks the read complete) - and the header the interceptor added to the request the engine sent.

How is success determined? the peer saw the correlation header and the body; one INFO event carries template, both bodies and the id, format-identical across engines; the read-state counter for this peer shows exactly one complete read.

Why does it matter? an engine whose stream never returned the EOF the tee waits for, or that handed out a body the converter reads differently, would log a partial read or empty bodies without any other symptom. Given/When

ReactorNettyRequestFactoryIntegrationTest

6 tests.

should log a 5xx answer through this request factory as WARN failure with the body

Rationale

What is tested? a peer answer the client turns into an exception - the response is still closed by the client, so the exchange completes with the status and the body it read.

How is success determined? the client throws HttpServerErrorException; the single event is WARN with outcome failure, status 500 and the body "boom".

Why does it matter? the 5xx path runs the engine's stream through the exception's body read; an engine that closed the stream early would lose the body on the one line that needs it. Given/When

should log a gzip answer the way this request factory hands it to the application

Rationale

What is tested? a Content-Encoding: gzip answer with the COMPRESSED length declared - the tee mirrors what the engine delivers, and the Content-Encoding rule keeps the capture from trusting a declared length the engine may have made meaningless by decompressing.

How is success determined? one INFO success event with status 200; when the engine decompresses transparently, application and log both see the plaintext; otherwise both see the compressed bytes and the plaintext appears nowhere on the line.

Why does it matter? the engines differ here, and an operator reading adapter_response_body must know which of the two a given engine produces - the contract pins it per engine instead of letting a deployment discover it. Given/When

should log a refused connection through this request factory as ERROR failure and not as a timeout

Rationale

What is tested? the control - a connection the peer actively refuses must stay a failure, whatever type this engine wraps it in.

How is success determined? ResourceAccessException; ERROR, outcome failure, -&gt; -, no status, the cause attached.

Why does it matter? a refusal misread as a timeout would send an operator looking for a slow peer that is in fact down. Given/When

should log this request factory's connect timeout as WARN timeout

Rationale

What is tested? the classification against the exception this engine raises when the TCP connect never completes - for some engines a different type than its read timeout.

How is success determined? against a tarpit that never completes the handshake, with a 200 ms connect timeout, the single event is WARN with outcome timeout, -&gt; - and no status.

Why does it matter? an unreachable peer is a timeout to an operator, not a refusal; the two dispositions call for different reactions. Given

should log this request factory's read timeout as WARN timeout

Rationale

What is tested? the classification against the exception this engine really raises when the peer's answer does not arrive in time, wrapped in RestClient's ResourceAccessException.

How is success determined? against a peer answering after 1.5 s with a 200 ms read timeout, the call throws ResourceAccessException and the single event is WARN with outcome timeout and no status.

Why does it matter? the timeout types differ per engine; only the real engine proves the list in Timeouts covers it through the client's wrapper. Given/When

should tee both bodies and send the correlation header through this request factory

Rationale

What is tested? the seams that touch the engine - the request body the interceptor is handed, the response tee on the engine's own stream (including the EOF or Content-Length observation that marks the read complete) - and the header the interceptor added to the request the engine sent.

How is success determined? the peer saw the correlation header and the body; one INFO event carries template, both bodies and the id, format-identical across engines; the read-state counter for this peer shows exactly one complete read.

Why does it matter? an engine whose stream never returned the EOF the tee waits for, or that handed out a body the converter reads differently, would log a partial read or empty bodies without any other symptom. Given/When

SimpleRequestFactoryIntegrationTest

6 tests.

should log a 5xx answer through this request factory as WARN failure with the body

Rationale

What is tested? a peer answer the client turns into an exception - the response is still closed by the client, so the exchange completes with the status and the body it read.

How is success determined? the client throws HttpServerErrorException; the single event is WARN with outcome failure, status 500 and the body "boom".

Why does it matter? the 5xx path runs the engine's stream through the exception's body read; an engine that closed the stream early would lose the body on the one line that needs it. Given/When

should log a gzip answer the way this request factory hands it to the application

Rationale

What is tested? a Content-Encoding: gzip answer with the COMPRESSED length declared - the tee mirrors what the engine delivers, and the Content-Encoding rule keeps the capture from trusting a declared length the engine may have made meaningless by decompressing.

How is success determined? one INFO success event with status 200; when the engine decompresses transparently, application and log both see the plaintext; otherwise both see the compressed bytes and the plaintext appears nowhere on the line.

Why does it matter? the engines differ here, and an operator reading adapter_response_body must know which of the two a given engine produces - the contract pins it per engine instead of letting a deployment discover it. Given/When

should log a refused connection through this request factory as ERROR failure and not as a timeout

Rationale

What is tested? the control - a connection the peer actively refuses must stay a failure, whatever type this engine wraps it in.

How is success determined? ResourceAccessException; ERROR, outcome failure, -&gt; -, no status, the cause attached.

Why does it matter? a refusal misread as a timeout would send an operator looking for a slow peer that is in fact down. Given/When

should log this request factory's connect timeout as WARN timeout

Rationale

What is tested? the classification against the exception this engine raises when the TCP connect never completes - for some engines a different type than its read timeout.

How is success determined? against a tarpit that never completes the handshake, with a 200 ms connect timeout, the single event is WARN with outcome timeout, -&gt; - and no status.

Why does it matter? an unreachable peer is a timeout to an operator, not a refusal; the two dispositions call for different reactions. Given

should log this request factory's read timeout as WARN timeout

Rationale

What is tested? the classification against the exception this engine really raises when the peer's answer does not arrive in time, wrapped in RestClient's ResourceAccessException.

How is success determined? against a peer answering after 1.5 s with a 200 ms read timeout, the call throws ResourceAccessException and the single event is WARN with outcome timeout and no status.

Why does it matter? the timeout types differ per engine; only the real engine proves the list in Timeouts covers it through the client's wrapper. Given/When

should tee both bodies and send the correlation header through this request factory

Rationale

What is tested? the seams that touch the engine - the request body the interceptor is handed, the response tee on the engine's own stream (including the EOF or Content-Length observation that marks the read complete) - and the header the interceptor added to the request the engine sent.

How is success determined? the peer saw the correlation header and the body; one INFO event carries template, both bodies and the id, format-identical across engines; the read-state counter for this peer shows exactly one complete read.

Why does it matter? an engine whose stream never returned the EOF the tee waits for, or that handed out a body the converter reads differently, would log a partial read or empty bodies without any other symptom. Given/When

TwinContractTest

2 tests.

should pin the exchange and arrival message format to the literal twin contract

Rationale

What is tested? the MESSAGE half of the twin contract - the field names are locked by ClientLogFieldTest, the message text is pinned here in both twins.

How is success determined? a pinned interceptor renders the literal messages both twins ship.

Why does it matter? plain-text appenders and the README's parity promise key on this text; a divergence in one twin would otherwise ship silently. Given

should pin this stack's client tag and outcome vocabulary

Rationale

What is tested? the ClientStack.RESTCLIENT facts the shared metrics owner is parameterised with - the client tag of the gauge and the outcomes pre-registered on the events counter.

How is success determined? client=restclient, and exactly success, failure and timeout in this order; cancelled belongs to the reactive twin alone.

Why does it matter? alerts on adapter.logging.events{outcome=...} for this stack must find every value at zero from the start, and none the blocking stack can never produce. Given/When/Then

UriTemplateAttributeTest

2 tests.

should mirror the private constant of DefaultRestClient literally

Rationale

What is tested? the package-private DefaultRestClient.URI_TEMPLATE_ATTRIBUTE read reflectively, compared with the module's own derived constant.

How is success determined? both strings are identical.

Why does it matter? the constant cannot be referenced, so the module repeats its derivation; a Spring upgrade that renames the attribute would otherwise silently drop adapter_url_template and fold every body meter into the UNKNOWN uri tag. Given/

should see the URI template RestClient records for the template form of uri

Rationale

What is tested? the mirrored attribute name - DefaultRestClient's constant is package-private, so the module derives it the same way and this test proves the derivation against the real client.

How is success determined? a call through uri("/things/{id}", 7) shows the expanded path on the request and the template under the mirrored attribute; an expanded URI shows no attribute.

Why does it matter? a renamed attribute upstream would silently drop adapter_url_template from every event.

legatium-webclient-logging

108 tests.

BoundedBodyCaptureTest

9 tests.

Freeze semantics

should freeze idempotently and keep a zero-byte capture absent

Rationale

What is tested? freeze on a capture that never saw a byte, called twice.

How is success determined? the second freeze neither throws nor changes anything - loggedValue stays null and totalBytes stays 0.

Why does it matter? the emitter freezes both captures unconditionally; a null body field is what lets a count-only or bodiless exchange omit the key instead of logging an empty string.

should ignore every mutation after freeze and keep the snapshot stable

Rationale

What is tested? post-freeze capture and count are no-ops.

How is success determined? the logged value and totalBytes after the late mutations equal the values at freeze time.

Why does it matter? the emitter reads body text and size as two separate calls; a mutation between them - or during them - would make the logged body and the metric disagree.

Late delivery through the tee

should not let a buffer delivered after the freeze reach the capture

Rationale

What is tested? the hand-off the cancellation race exercises - the body tee is still subscribed when the emission freezes the capture, and the publisher then delivers an already-requested buffer.

How is success determined? the buffer passes the tee (downstream is unaffected) but the capture's text and count are unchanged.

Why does it matter? doFinally(CANCEL) runs immediately after cancellation is forwarded while an onNext may still be in flight; without the freeze the log snapshot would be taken from a buffer that another thread is mutating.

Read state

should ignore marks once frozen so the emitted state is a consistent snapshot

Rationale

What is tested? the read state follows the freeze contract of every other mutation.

How is success determined? a completion signal arriving after freeze leaves the state at PARTIAL.

Why does it matter? the emitter freezes first and reads second; a mark slipping in between would make the counter disagree with the body text and size logged for the same exchange.

should start unread and move to partial on start and to complete on completion, never backwards

Rationale

What is tested? the read-state machine of the response capture - markStarted and markCompleted.

How is success determined? UNREAD before any mark, PARTIAL after the subscription, COMPLETE after the completion signal, and a later markStarted leaves COMPLETE untouched.

Why does it matter? the state becomes the state tag of adapter.response.body.read; a regression to PARTIAL would report fully consumed bodies as discarded payload.

Truncation at a character boundary

should drop an incomplete trailing UTF-8 sequence instead of decoding a replacement character

Rationale

What is tested? byte-bounded truncation of multi-byte text - the cap counts bytes, so it can split a character.

How is success determined? with a 2-byte cap over "h\u00e9" (3 bytes: 68 c3 a9) the logged prefix is "h", not "h\uFFFD"; the byte count stays exact.

Why does it matter? a replacement character in the logged prefix is corruption the reader cannot distinguish from corrupt input.

should drop an incomplete trailing sequence of another variable-width charset

Rationale

What is tested? decodeTruncated with the charset the peer declared instead of UTF-8 - Shift_JIS, where the second character is a two-byte sequence the 2-byte cap splits.

How is success determined? the logged prefix is "a" plus the truncation note with the exact 3-byte total, no replacement character.

Why does it matter? the prefix decoding must follow the declared charset, not a UTF-8 assumption, or every non-UTF-8 peer would log a corrupted tail.

should keep a complete multi-byte character that ends exactly at the cap

Rationale

What is tested? the boundary case of the byte cap - the last captured byte completes a character.

How is success determined? the character is logged in full, followed by the truncation note with the 3-byte total.

Why does it matter? underflow handling must not eat a character that happens to end on the cap, or the logged prefix would be one character short of what was actually captured.

should still replace malformed bytes inside the prefix

Rationale

What is tested? the distinction decodeTruncated draws between an incomplete TAIL (underflow, dropped) and a malformed byte INSIDE the prefix (replaced).

How is success determined? the lone continuation byte renders as U+FFFD while the surrounding characters and the truncation note stay intact.

Why does it matter? genuinely corrupt input must still show as corrupt in the log; only the artefact of the byte cap is suppressed.

ClientLoggingAutoConfigurationTest

9 tests.

should attach the filter to every WebClient builder Boot hands out as its last filter

Rationale

What is tested? the customizer path - the filter bean is only useful if Boot's builder carries it.

How is success determined? the builder's filter list contains the module's filter as its LAST entry.

Why does it matter? the shipped activation is this customizer, not the bean. Given/When

should back off entirely when disabled by the property

Rationale

What is tested? the class-level @ConditionalOnProperty on adapter-logging.enabled.

How is success determined? with the property false neither the filter, the defaults, the bound properties nor the customizer exist.

Why does it matter? the switch-off must leave no trace - a lingering customizer would still attach a filter, a lingering default bean could collide with a host's own. Given/When

should bind the identical adapter-logging namespace

Rationale

What is tested? @EnableConfigurationProperties binding of the shared ClientLoggingProperties under the adapter-logging prefix - a scalar, a list, a nested header section and a boolean.

How is success determined? the bound bean carries the four configured values.

Why does it matter? the RestClient twin binds the same class under the same prefix; a host with both modules configures them once, so the keys must resolve identically here. Given/When

should keep the filter bean without the customizer when Boot's webclient module is absent

Rationale

What is tested? the @ConditionalOnClass(WebClientCustomizer) guard on the nested customization, with the class hidden by a FilteredClassLoader.

How is success determined? the context starts, the filter bean exists, the customizer bean does not.

Why does it matter? spring-boot-webclient is an optional dependency; a host building its clients by hand must still get the filter bean without a ClassNotFoundError at context start. Given/

should key the default masker from the masking-key property

Rationale

What is tested? the property path to a guess-proof fingerprint - no host bean needed.

How is success determined? with masking-key set, the masker bean renders the keyed fingerprint, not the unkeyed default.

Why does it matter? keying is the documented answer to "masked is not a security boundary for guessable values"; it must be reachable from application.yml alone. Given/When

should let a host filter bean win and consume a host registry

Rationale

What is tested? the @ConditionalOnMissingBean back-off for the filter and the masker, and the ObjectProvider consumption of a host MeterRegistry.

How is success determined? the host filter is the single filter bean and the one the builder carries, the host registry holds the module's three fail-open counters, and the host masker renders ***.

Why does it matter? a host that replaces the filter must not get a second one, and the module must export into the host's registry rather than define one of its own. Given/When

should let host time source and id generator beans back the defaults off

Rationale

What is tested? the @ConditionalOnMissingBean back-off for the two remaining collaborators, NanoTimeSource and CorrelationIdGenerator, with the host pinning both and nothing else - the filter and the masker stay the auto-configured defaults.

How is success determined? exactly one bean of each collaborator type, each the host's instance (same reference, host behaviour on a call), while the filter and the masker still exist exactly once.

Why does it matter? a deterministic clock and id generator are the documented override for a test profile and for a peer that insists on an id format; the back-off is what makes the host bean reach the filter's constructor injection instead of colliding with a second bean of the same type. Given/When

should register the filter, the defaults and the customizer

Rationale

What is tested? the default bean set of the auto-configuration in a plain context without a host bean - filter, time source, id generator, masker and the WebClientCustomization nested config.

How is success determined? each type is present exactly once and the customizer bean exists by name.

Why does it matter? dropping the module on the classpath is the whole activation story; a missing default bean would fail the context of every host that does not define its own. Given/When

should ship the auto-configuration through the imports resource

Rationale

What is tested? the AutoConfiguration.imports resource under META-INF/spring on the test classpath.

How is success determined? one of the imports files names ClientLoggingAutoConfiguration by its FQCN.

Why does it matter? Boot discovers auto-configurations only through this file - without the entry the module is inert on every classpath and no other test would notice. Given/When

ClientRequestLoggingFilterBodyAndHeaderTest

22 tests.

Header selection and masking

should key the built-in fingerprint from the properties when constructed without a masker

Rationale

What is tested? the masker default of the public four-argument constructor - the manual wiring path the guides recommend - derives from properties.maskingKey through HeaderValueMasker.forKey, exactly as the auto-configuration's default bean does.

How is success determined? with masking-key set and no masker passed, the masked Authorization value is the keyed HMAC fingerprint and NOT the unkeyed default fingerprint of the same value.

Why does it matter? a host that configured a secret and wired the filter by hand silently logged unkeyed, guessable fingerprints - the configured guess-resistance depended on how the filter was constructed instead of on the property.

should log selected request headers multi-value, mask the configured ones stably and include the sent correlation header

Rationale

What is tested? HeaderLogProperties.select over the OUTGOING request's HttpHeaders - explicit includes, a two-valued header joined, a masked name rendered by the default fingerprint, and the correlation header the filter itself added.

How is success determined? the field joins both Accept values with ", ", renders Authorization as the DEFAULT masker's output without the plaintext, and shows X-Correlation-Id as "generated-42".

Why does it matter? the field must describe the request as it went over the wire, including the header this module put there, and a masked value must be the stable fingerprint a reader can correlate. Given

should log the selected response headers as the peer sent them

Rationale

What is tested? the response-side selection at emission - a wildcard include, an exclude that wins over it, and an unmasked name rendered in plaintext.

How is success determined? Content-Type appears with its literal value, Set-Cookie appears nowhere in the field.

Why does it matter? a wildcard include is the debugging configuration; the exclude and the plaintext allowlist are what keep a session cookie out of the line while the content type stays readable.

should render masked values through a host-provided masker

Rationale

What is tested? the masker is an injected collaborator - the filter built with a host bean masks request AND response headers with it.

How is success determined? both selected, masked headers carry the host masker's output, never the plaintext and never the built-in fingerprint.

Why does it matter? a compliance regime forbidding unkeyed hashes must be satisfiable without forking the module. Given

Mask by default

should mask every selected header by default so a wildcard include never leaks plaintext

Rationale

What is tested? ADR-0005 at the filter - includes: ["*"] with nothing said about masking.

How is success determined? every logged request header is a fingerprint; the secret appears nowhere.

Why does it matter? with masking as a second, empty list the same configuration logged everything in plaintext - the unsafe combination was the convenient one. Given

Outcome-gated bodies

should log a decoding failure of the application as a successful exchange without bodies in on-failure mode

Rationale

What is tested? the boundary of the outcome gate - a 200 whose body the client's Jackson decoder cannot map to the requested type. The decoder fails DOWNSTREAM of the body tee, after the body flux completed normally; the exchange itself saw a clean completion.

How is success determined? the caller gets DecodingException; the single event is INFO with outcome success, status 200 and NO body fields - on-failure withholds them.

Why does it matter? this is the one case where the line's outcome and the caller's outcome differ, decided and documented (guide §6.3, ADR-0006): the module observes the wire, not the application's decoding, and no signal of the decoder reaches the filter. Pinned so a change here is a decision, not an accident. Given/When

should log both bodies of a 4xx answer although its outcome stays success

Rationale

What is tested? the gate is wider than the outcome vocabulary by one status class - a 4xx keeps its success outcome (the peer answered; the request was wrong) but is exactly the case a body explains.

How is success determined? outcome success, and BOTH bodies on the line.

Why does it matter? a validation error\'s response body is the most wanted body of all; hiding it behind the outcome vocabulary would make on-failure useless for client errors. Given/

should log both bodies of a 5xx answer in on-failure mode

Rationale

What is tested? the on-failure gate for a 5xx - classified as outcome failure without an error signal, so both captures are written.

How is success determined? outcome failure, the request body "sent" and the response body "upstream down" on the line.

Why does it matter? a 5xx is the case an operator wants the bodies for, and the request body was teed before the outcome was known - the on-failure capture must not have thrown it away. Given/

should log the raw body the decoder read when the application's decoding fails in always mode

Rationale

What is tested? the same decoding failure with log-response-body=always - the tee logged the buffers the decoder consumed before it gave up.

How is success determined? the caller gets DecodingException; the single event carries the raw JSON as adapter_response_body, outcome success.

Why does it matter? always is the documented way to see what a peer really sent when the application cannot make sense of it - the body must be the bytes, not the failure. Given/When

should log the teed request body of a call that failed after sending it

Rationale

What is tested? on-failure with an error signal AFTER the request body was written - no response exists, the exchange completes through doFinally on the response Mono.

How is success determined? the caller gets the IOException, the line carries outcome failure and the request body, and no response body key.

Why does it matter? for a connection that dropped after the upload the request body is the only payload evidence there is; it must survive to the failure line.

should still measure the size of a body it withholds

Rationale

What is tested? on-failure plus measureRequestBodySize on a successful call - the capture is installed for logging, the emitter records its size and then discards the text.

How is success determined? the request body size summary totals 4 bytes while the event has no adapter_request_body key.

Why does it matter? metrics run before the level and outcome gates; a size sample must not depend on whether the body ends up on the line.

should withhold both bodies from a successful exchange in on-failure mode

Rationale

What is tested? the volume switch - on-failure tees the request body (the outcome is unknown while it is written) and discards both captures at emission when the outcome is success.

How is success determined? the application receives the response body; the line carries neither body.

Why does it matter? this is the mode that keeps body logging affordable outside a debug session. Given/When

Request body tee

should log the request body as it is written to the connector and deliver it unchanged

Rationale

What is tested? the inserter wrap - the body is observed at the connector's writeWith, and the connector receives the identical bytes.

How is success determined? the mock connector request holds the body; the event logs it.

Why does it matter? the tee must be a passive copy at the one place every encoder passes. Given

should omit the request body key for a bodiless request

Rationale

What is tested? a GET without an inserter body under logRequestBody=ALWAYS - the tee sees setComplete only, the capture stays at zero bytes and loggedValue returns null.

How is success determined? the event carries no adapter_request_body key at all.

Why does it matter? an empty-string field on every bodiless call would be noise and would make "no body" indistinguishable from "an empty body". Given/When

should truncate the logged request body at the capture limit and say so

Rationale

What is tested? a request-side capture with maxBodyBytes below the body length, fed through the inserter wrap's tee.

How is success determined? the field holds the first 4 bytes plus the truncation note with the exact 10-byte total.

Why does it matter? the cap bounds log volume and heap per call; the note tells the reader that the body shown is a prefix and how much really went out. Given

Response body tee

should copy nothing at all in count-only mode while still counting every byte

Rationale

What is tested? tee() against a capture with limit 0 - remainingCapacity is 0, so the copy branch is skipped and the whole buffer goes to count().

How is success determined? totalBytes is 7 and the logged value is the bare truncation note with that total.

Why does it matter? measure-only mode installs exactly this capture on every call; it must cost no allocation per buffer while keeping the size sample exact.

should decode the response body with the charset the peer declared

Rationale

What is tested? declaredCharsetOrUtf8 on the RESPONSE headers at emission - the Content-Type charset parameter selects the decoder for the captured bytes.

How is success determined? the ISO-8859-1 bytes of "café" log as "café", not as a mojibake sequence.

Why does it matter? a peer that does not speak UTF-8 must still yield a readable body field; the decoder must follow the header, not a default. Given

should forward the original buffer untouched and copy only the bounded prefix

Rationale

What is tested? the tee's memory contract - counting never clones the buffer; at most the capture's remaining capacity is copied via a non-advancing read, and the ORIGINAL buffer flows downstream with its read position untouched.

How is success determined? downstream receives the identical buffer instance, fully readable; the capture holds exactly the 8-byte prefix and counted all 16 bytes.

Why does it matter? an operator sizing heap by the cap must be able to rely on the bound. Given

should log the response body the application consumed and deliver identical content

Rationale

What is tested? the ObservedBody tee in the default consumption path - bodyToMono(String) reads the body through the mutated response.

How is success determined? the caller receives "hello" unchanged and the event logs the same text.

Why does it matter? the tee is a passive copy; a body that arrived altered or a log that showed something else than what the application read would both be bugs of the same mechanism. Given

should omit the response body key when no bytes flowed

Rationale

What is tested? an empty response body under logResponseBody=ALWAYS - the tee sees no buffer, loggedValue returns null and the *IfPresent helper drops the field.

How is success determined? the event carries no adapter_response_body key.

Why does it matter? an empty field on every bodiless answer would be noise and would hide the difference between "nothing sent" and "empty body sent". Given/

should record the read state as partial for a cancelled body and complete for a consumed one

Rationale

What is tested? the observation points of the read state on the reactive tee - the subscription marks PARTIAL, the completion signal marks COMPLETE, a cancellation leaves PARTIAL.

How is success determined? read off the captures of two exchanges.

Why does it matter? the state is the one signal that tells a discarded body from an absent one.

should truncate the logged response body at the capture limit and keep the exact total

Rationale

What is tested? a response-side capture with maxBodyBytes below the body length, fed through the ObservedBody tee.

How is success determined? the field holds the first 4 bytes plus the truncation note with the exact 10-byte total.

Why does it matter? the cap protects heap and log volume on the response side too, and the total must stay exact even though only a prefix was copied. Given

ClientRequestLoggingFilterIntegrationTest

6 tests.

should log a 5xx answer as WARN failure with the body the client read for its exception

Rationale

What is tested? the 5xx classification through a real Reactor Netty exchange - retrieve() reads the body to build its WebClientResponseException, and that read is what the tee observes.

How is success determined? the caller gets the response exception; the single event is WARN with outcome failure, status 500 and the response body "boom".

Why does it matter? a 5xx is not an error signal in the reactive chain; the body on the line comes from the client's own error-handling read, which only a real connector exercises. Given

should log a bodiless 204 without body fields

Rationale

What is tested? a real 204 answered by the peer with no body, consumed via toBodilessEntity - the release path on a body that yields no buffer.

How is success determined? status 204 on the entity and in the event; neither body key is present although both body modes are always.

Why does it matter? a bodiless answer is the normal shape of a DELETE or PUT; an empty body field on each of them would be noise in the most frequent healthy line. Given

should log a downstream timeout operator as cancelled

Rationale

What is tested? the documented boundary - a timeout() the CALLER applies cancels the exchange; the filter sees a CANCEL, not an error.

How is success determined? the caller gets the TimeoutException, the single event is WARN with outcome cancelled and no status.

Why does it matter? an operator reading cancelled must know it may be their own timeout. Given

should log a refused connection as ERROR failure without a status

Rationale

What is tested? the no-response path against a real closed port - the connector errors before a status line, the exchange completes through doFinally on the response Mono.

How is success determined? the caller gets a WebClientRequestException; the single event is ERROR with outcome failure, "-> -" in the message and no status field.

Why does it matter? a connection refused is the most common outage signature; the line must say so loudly and must not invent a status the peer never sent. Given

should log one complete event for a real call including template, headers and bodies

Rationale

What is tested? the full happy path through Boot's builder and Reactor Netty - the customizer attached the filter, the template attribute is recorded, both bodies are teed on pooled buffers, and the generated correlation header went out on the wire.

How is success determined? the peer saw the request with the correlation header; one INFO event with the client field family, format-identical to the RestClient twin.

Why does it matter? only a real connector proves the buffer handling and the registration hold outside the stubs. Given

should log the connector's response timeout as WARN timeout

Rationale

What is tested? the timeout classification against the REAL connector's exception - Reactor Netty's ReadTimeoutException, recognised by name without a Netty dependency in the module.

How is success determined? with a 200 ms response timeout against a peer that answers after 1.5 s, the client errors and the single event is WARN with outcome timeout and no status.

Why does it matter? only a real connector proves the names are the ones that actually occur. Given

ClientRequestLoggingFilterTest

30 tests.

Activation and start line

should announce the call before the exchange when enabled

Rationale

What is tested? logRequestStart - emitter.logRequestStart runs inside the defer BEFORE next.exchange, with the exchange identity in its MDC scope.

How is success determined? the connector already sees the started line; two events in total, the first without an outcome but with the request id in the MDC, the last with outcome success.

Why does it matter? the arrival line is what shows a call that never returns; it must carry the same id as the completion line to be joined with it. Given

should be active only for paths matching an include pattern and let an exclude win

Rationale

What is tested? include patterns and exclude prefixes together - a matching path, a non-matching path, and a path that matches the include but starts with the exclude.

How is success determined? exactly one event, for /api/things.

Why does it matter? the include narrows logging to the calls that matter and the exclude must win over it, or a noisy internal endpoint could not be silenced inside an included tree. Given

should match activation on the decoded path segments so an encoded variant cannot slip past an exclude

Rationale

What is tested? the PathContainer-based matching - includes match decoded segments, an encoded slash stays one segment, and the exclude compares against the decoded path.

How is success determined? /%61pi/things is logged with its raw path, /api%2Fthings is not, and /%61ctuator/health is excluded.

Why does it matter? a caller that percent-encodes a letter must neither escape an exclude nor be denied an include; the logged path stays the raw one that went over the wire. Given

should not log a call to an excluded host at all

Rationale

What is tested? ClientActivation.shouldNotFilter on the case-insensitive host exclusion - the filter returns next.exchange(request) before any wiring.

How is success determined? the connector received the very same request instance and no event exists.

Why does it matter? a metrics push or health probe target must cost nothing - no rebuild, no correlation header, no line. Given

should reject an invalid include pattern at construction time

Rationale

What is tested? ClientActivation parsing includePathPatterns once in its constructor.

How is success determined? constructing the filter throws PatternParseException naming the pattern.

Why does it matter? a bad pattern is a configuration error that must fail the context start, not throw per call inside the fail-open path and silently degrade every exchange. Given/When

Identity per ADR-0002

should adopt a correlation id already on the request and leave the request untouched

Rationale

What is tested? the acceptance path of the correlation contract - a conformant header value becomes the request id and sendCorrelationHeader is false, so no rebuild happens.

How is success determined? the connector received the SAME ClientRequest instance and the message carries "caller-id".

Why does it matter? a caller that propagates its own id must see it unchanged on the wire, and the rebuild must be skipped when there is nothing to add. Given

should fall back to the correlation contract when the traceparent is not conformant

Rationale

What is tested? Traceparent.parse rejecting an all-zero trace id, so ClientIdentity treats the call as traceless.

How is success determined? the connector received the generated correlation header and the event has the generated id without a traceId key.

Why does it matter? an invalid traceparent must not become the request id - the W3C rule forbids the value and a downstream join on it would be meaningless.

should generate a correlation id and SEND it on a traceless request without one

Rationale

What is tested? ClientIdentity.resolve for a traceless request without a correlation header - sendCorrelationHeader is true, so wireExchange rebuilds the request with the header set.

How is success determined? the connector received the generated id in X-Correlation-Id and the event's MDC carries the same id.

Why does it matter? the peer's inbound line and this outbound line join on that id; a generated id that stayed local would leave the call unjoinable on the other side.

should use the traceparent trace id as the request id and add no correlation header

Rationale

What is tested? the identity decision of ADR-0002 on the outbound side.

How is success determined? adapter_request_id equals the trace id; the connector got the caller's request untouched although it carried a correlation header too.

Why does it matter? observational neutrality on a traced call. Given

Levels and outcomes

should classify a body error with the status already received

Rationale

What is tested? the read-side failure - the status arrived, the body then errored.

How is success determined? ERROR, outcome failure, WITH the 200 that was received, cause attached.

Why does it matter? "200 but failed" is exactly what happened; hiding either half misleads. Given

should compare the slow threshold at full precision instead of truncated milliseconds

Rationale

What is tested? the Duration comparison of elapsed time against slowRequestThreshold at nanosecond precision - a 1.5 ms threshold against 1.0 ms and 1.5 ms of elapsed time.

How is success determined? 1.0 ms is not flagged, 1.5 ms is.

Why does it matter? a toMillis truncation would turn the 1.5 ms threshold into 1 ms and flag calls the operator explicitly configured as fast enough. Given

should escalate to WARN and flag a slow but successful call

Rationale

What is tested? the slow escalation in emitExchange - elapsed nanos reaching the 200 ms threshold lifts INFO to WARN without touching the outcome.

How is success determined? one WARN event with adapter_slow=true and outcome success.

Why does it matter? level carries severity and outcome carries semantics; a slow call must alert without being counted as a failure. Given

should escalate to WARN with outcome failure for a 5xx answer

Rationale

What is tested? the classify branch for a status >= 500 without an error signal.

How is success determined? one WARN event with outcome failure and status 503.

Why does it matter? the peer answered, so the chain completes normally; without this branch a broken upstream would log at INFO as a success. Given/When

should log ERROR with outcome failure and no status when the exchange errors before a response

Rationale

What is tested? the no-response path - the connector errored before a status line.

How is success determined? the error signal propagates unchanged; one ERROR event with the cause, -&gt; - and no status field.

Why does it matter? a call that never got an answer must still be one truthful line. Given

should log Spring's body skip for a Void body type as success

Rationale

What is tested? bodyToMono(Void.class) (and toEntity(Void.class), an unsupported media type) drains a body-carrying ClientHttpResponse through takeWhile(release; false), which cancels upstream in onNext of the FIRST buffer - a framework-internal cancel.

How is success determined? INFO, outcome success, the received 200, one event.

Why does it matter? the fire-and-forget idiom of every WebClient user was logged as cancelled at WARN, and in on-failure body mode both bodies of the healthy call were written.

should log WARN with outcome timeout when the connector raises a timeout

Rationale

What is tested? Timeouts.isTimeout walking the cause chain of the error signal - the TimeoutException is wrapped one level down, as connectors wrap.

How is success determined? one WARN event with outcome timeout and no status field.

Why does it matter? an operator reads a timeout as "peer slow or unreachable", a failure as "peer broken"; the distinction must survive the connector's wrapping.

should log a body the consumer stopped reading from within its delivery as success, partially read

Rationale

What is tested? a take(1) cancels the body from WITHIN onNext of the first buffer - the consumer decided it has read enough. That is consumption, not abandonment.

How is success determined? INFO, outcome success, the received 200, and the read-state counter shows partial for the exchange.

Why does it matter? the same signal shape is Spring's own body skip (next test); logging it as cancelled flags healthy calls at WARN.

should log outcome cancelled with a dash status when the caller cancels before the response

Rationale

What is tested? the reactive disposition the blocking twin cannot have - a cancelled subscription before any response (a downstream timeout operator, a disposed caller).

How is success determined? one WARN event, outcome cancelled, -&gt; -, no status field.

Why does it matter? a torn-down call must neither log as a success nor invent a status. Given/

should log outcome cancelled with the received status when the body is cancelled out of band

Rationale

What is tested? a cancel from OUTSIDE a delivery - a timeout operator's timer, a disposed caller, a client that disconnected - is the caller walking away.

How is success determined? one WARN event, outcome cancelled, with the received 200.

Why does it matter? this is the disposition the reactive stack adds; it must survive the consumption-limited distinction above.

should turn a downstream filter that throws while assembling into the exchange's error signal

Rationale

What is tested? the Mono.defer around the exchange call - a filter that THROWS instead of returning Mono.error.

How is success determined? the caller sees the error as a signal, one ERROR event, gauge back at 0.

Why does it matter? invoked bare, the throw would skip the callbacks and leak the gauge. Given/When

Subscription and completion shapes

should complete the exchange as cancelled when the caller cancels while the response is being delivered

Rationale

What is tested? the handover race - the response is inside the downstream's onNext (state DELIVERING) when the caller cancels from ANOTHER thread; a downstream that is cancelling drops the response and never subscribes to the body, so the body can never complete the exchange. Barrier-driven: the subscriber's onNext waits for the cancel.

How is success determined? exactly one WARN event, outcome cancelled with the received 200, and the open-exchanges gauge back at zero - without the body ever being consumed.

Why does it matter? with the state set to RESPONDED before the handover, this cancel was ignored as "the body owns it" and the exchange stayed open forever: no event, the gauge one too high for the life of the process.

should let the body own the exchange when a host operator cancels the response Mono after delivery

Rationale

What is tested? an operator between this filter and the client that cancels the response Mono right after onNext (next(), a future bridge) - the response was delivered, the body is consumed afterwards.

How is success determined? one success line at the body's completion, not a cancelled line at the operator's cancel.

Why does it matter? the outer cancel arrives while the exchange is RESPONDED; ending it there would misclassify the call and truncate the logged body. Given/When

should log an empty completion of the connector as ERROR failure without a status

Rationale

What is tested? a connector (or a host filter swallowing an error into Mono.empty()) that completes without a response.

How is success determined? ERROR, outcome failure, -&gt; -, the cause naming the missing response - the same words WebClient raises for the caller.

Why does it matter? the caller sees an error; a success line for it would be a lie. Given/When

should log one line per subscription when an outer filter resubscribes the call

Rationale

What is tested? wiring runs per SUBSCRIPTION - a retrying outer filter that resubscribes this filter's Mono (retry, retryWhen) must see one exchange per attempt.

How is success determined? two subscriptions, two success lines, the gauge back at zero.

Why does it matter? wired at assembly time, attempt 2 hit a completed exchange and was never logged - the class documentation promised one line per attempt.

should replace a correlation header outside the acceptance rule with a generated id

Rationale

What is tested? the CorrelationHeader rule at the filter - a forged value counts as absent.

How is success determined? the connector receives the generated id INSTEAD of the foreign value; the event carries the generated id and no trace of the forged one.

Why does it matter? the value lands verbatim in the message and the MDC. Given

The exchange line

should emit only at the body's terminal signal and exactly once

Rationale

What is tested? the emission point - the response body's completion - and its exactly-once guard.

How is success determined? a delivered response logs nothing until its body is consumed; releasing the body logs; consuming the body a second time logs nothing more.

Why does it matter? emitting when the response Mono completes would log a body of zero bytes and a duration without the read; a double subscription must not double the event.

should log query, port and URI template as their own fields

Rationale

What is tested? the URL coordinates RequestTarget splits out of the request URI - an explicit port in the host, the raw query, and the URI_TEMPLATE_ATTRIBUTE the client records.

How is success determined? the message target carries the port and no query; host, path, query and template land in their own adapter_url_* fields.

Why does it matter? the query is what changes per call and must not pollute the route coordinate; the template is the low-cardinality key dashboards group on. Given

should log the identical line format of the RestClient twin when the body completes

Rationale

What is tested? the format contract - message and adapter_* key-values must be indistinguishable from legatium-restclient-logging's output.

How is success determined? the exact message string and the full field family for a successful GET, 42 ms of measured work between send and the body's completion.

Why does it matter? identical logging is this module's core requirement - dashboards must not care which client produced an event.

should log the raw request target so percent-encoded control characters cannot forge log lines

Rationale

What is tested? the log-injection guard for the raw request target.

How is success determined? path and query appear percent-encoded as sent in every sink; no sink contains a line break.

Why does it matter? a URL assembled from untrusted input could otherwise forge complete exchange lines in every plain-text appender. Given

should preserve the ambient MDC beside the identity on the emitted event

Rationale

What is tested? the emission scope is an additive overlay - the completing thread's MDC (here: the caller's, since everything runs synchronously) stays visible.

How is success determined? the event carries the seeded key beside adapter_request_id; afterwards the thread has the seeded key and no client key.

Why does it matter? the client line must join an inbound request's line by MDC alone. Given

ClientRequestLoggingMetricsTest

10 tests.

Body meters

should not record a read state when the call produced no response

Rationale

What is tested? the exchange.response != null guard around metrics.responseBodyRead for a connector error before any status line.

How is success determined? no counter exists under adapter.response.body.read after the failed call.

Why does it matter? a call that never got an answer has no body to consume; counting it as unread would inflate the discarded-payload share the counter exists to show. Given

should record body sizes and the response read state under template and host, independent of the level gate

Rationale

What is tested? recordBodySizes in the emitter with both measure flags on and the logger at OFF - the request tee through the inserter wrap, the response tee, and the read-state counter.

How is success determined? 5 request bytes and 6 response bytes recorded under the template and host tags, the read counter at 1 for state=complete, and no event emitted.

Why does it matter? metrics run before the level gate; a size sample that vanished when the logger is quiet would make the meters depend on log configuration. Given

Counters and gauge

should count the request-id origin per source

Rationale

What is tested? metrics.requestId with the RequestIdSource ClientIdentity resolved - a conformant traceparent, an accepted correlation header, and neither.

How is success determined? the correlation counter shows one increment under each of trace, header and generated.

Why does it matter? a rising generated share is the one signal that the host stopped propagating its trace or correlation context onto outbound calls. Given/When

should keep the open-exchanges gauge up until the body's terminal signal

Rationale

What is tested? the gauge as the liveness signal of the body-driven emission.

How is success determined? 1 after the response was delivered but before the body was consumed, 0 after; a failed call goes up and down within the signal.

Why does it matter? a body nobody consumes must stay VISIBLE - the gauge baseline is the only signal for that silent-loss mode. Given/When

should pre-register the reactive outcome vocabulary and count emitted events per outcome

Rationale

What is tested? every fixed-tag meter exists at zero before the first call - including the cancelled outcome the blocking twin does not have - and the events counter counts by outcome.

How is success determined? four outcomes at zero, the gauge under client=webclient; one call each moves its side.

Why does it matter? a rate() alert must see the zero before the first occurrence; the client tag is what keeps this twin's gauge apart from the blocking twin's in one host. Given

should share one metrics owner between two filters on the same registry

Rationale

What is tested? ClientLoggingMetrics.forRegistry handing a second filter on the same registry the existing owner instead of a new one with an ignored gauge registration.

How is success determined? the gauge read through the FIRST filter's registry moves to 1 and back to 0 for an exchange the SECOND filter carries.

Why does it matter? a duplicate owner's gauge would be silently dropped by Micrometer and that filter's open exchanges would become invisible on the liveness signal. Given

Fail-open stages

should confine a terminal-callback failure and still complete the exchange

Rationale

What is tested? the completion guard - the gauge decrement is a host-registry call made inside Reactor's signal propagation.

How is success determined? with a registry whose gauge bookkeeping cannot fail but whose events counter throws, the body completes for the caller, the event is emitted, wiring counted.

Why does it matter? an escaping exception there would be rethrown into the caller's pipeline.

should confine an arrival-line backend failure and count stage arrival

Rationale

What is tested? the failOpen guard of logRequestStart - a TurboFilter throws on the INFO level check of the exchange logger while the arrival line is being written.

How is success determined? the exchange proceeds, the caller gets the body, the fail-open counter shows stage=arrival at 1, and the completion line (armed off by then) is still emitted.

Why does it matter? the start line is optional and must never take the call or the completion line down with it when the logging backend misbehaves.

should count a broken emission as stage emission and never disturb the body

Rationale

What is tested? the emission guard - the injected time source throws at emission time, inside the body's terminal callback.

How is success determined? the body completes normally for the caller, no event, emission=1.

Why does it matter? the emission counter is the metric channel for exactly this loss, and the callback runs inside Reactor's signal propagation. Given

should degrade to a pass-through and count stage wiring when the id generator throws

Rationale

What is tested? wireOrNull catching an exception from ClientIdentity.resolve and returning null, so filter() falls through to next.exchange(request).

How is success determined? the caller receives the body, no event is logged, the fail-open counter shows stage=wiring at 1 and the gauge is back at 0.

Why does it matter? a broken host collaborator must cost the log line, never the call - and the gauge must not be left up by an exchange that was never opened. Given

ClientRequestLoggingTracingIntegrationTest

2 tests.

should join the event with the caller trace and add no correlation header on a traced call

Rationale

What is tested? the trace contract against a real bridge - an outer span is active on the subscribing thread, the client observation creates the child span and injects traceparent, the filter sees it.

How is success determined? the peer received a traceparent and NO X-Correlation-Id; the event's traceId is the outer span's trace id, its spanId a DIFFERENT span, the request id the trace id.

Why does it matter? observational neutrality and the log-to-trace join, proven where the header actually comes from. Given

should treat a call without an outer span as traced too because the client observation roots a trace

Rationale

What is tested? the identity decision when NO span is active on the subscribing thread - Boot's client observation still opens a span, so the request reaches the filter with a traceparent.

How is success determined? the peer received a traceparent and no X-Correlation-Id; the event's MDC carries traceId and spanId.

Why does it matter? with a tracing bridge present there is no traceless call at all, so the correlation header must never appear next to a bridge - the trace id is the identity. Given/When

HttpComponentsConnectorIntegrationTest

4 tests.

should log a refused connection through this connector as ERROR failure and not as a timeout

Rationale

What is tested? the control - a connection the peer actively refuses must stay a failure, whatever type this engine wraps it in.

How is success determined? ERROR, outcome failure, -&gt; -, no status.

Why does it matter? a refusal misread as a timeout would send an operator looking for a slow peer that is in fact down. Given/When

should log this connector's connect timeout as WARN timeout

Rationale

What is tested? the classification against the exception this engine raises when the TCP connect never completes - a type that is NOT the same as its response timeout (Netty's ConnectTimeoutException extends ConnectException, not its TimeoutException).

How is success determined? against a tarpit that never completes the handshake, with a 200 ms connect timeout, the single event is WARN with outcome timeout, -&gt; - and no status.

Why does it matter? an unreachable peer is a timeout to an operator, not a refusal; the two dispositions call for different reactions. Given

should log this connector's response timeout as WARN timeout

Rationale

What is tested? the classification against the exception this engine really raises when the peer's status line does not arrive in time.

How is success determined? against a peer answering after 1.5 s with a 200 ms response timeout, the call errors and the single event is WARN with outcome timeout and no status.

Why does it matter? the timeout names differ per engine; only the real engine proves the list. Given/When

should tee both bodies and send the correlation header through this connector

Rationale

What is tested? the two seams that touch the engine - the request tee wrapping the engine's ClientHttpRequest as the inserter writes, the response tee on the engine's body buffers - and the header the filter added to the request the engine sent.

How is success determined? the peer saw the correlation header and the body; one INFO event carries template, both bodies and the id, format-identical across connectors.

Why does it matter? a connector that bypassed the decorator or handed out buffers the tee cannot read would log empty bodies without any other symptom. Given/When

JdkHttpClientConnectorIntegrationTest

4 tests.

should log a refused connection through this connector as ERROR failure and not as a timeout

Rationale

What is tested? the control - a connection the peer actively refuses must stay a failure, whatever type this engine wraps it in.

How is success determined? ERROR, outcome failure, -&gt; -, no status.

Why does it matter? a refusal misread as a timeout would send an operator looking for a slow peer that is in fact down. Given/When

should log this connector's connect timeout as WARN timeout

Rationale

What is tested? the classification against the exception this engine raises when the TCP connect never completes - a type that is NOT the same as its response timeout (Netty's ConnectTimeoutException extends ConnectException, not its TimeoutException).

How is success determined? against a tarpit that never completes the handshake, with a 200 ms connect timeout, the single event is WARN with outcome timeout, -&gt; - and no status.

Why does it matter? an unreachable peer is a timeout to an operator, not a refusal; the two dispositions call for different reactions. Given

should log this connector's response timeout as WARN timeout

Rationale

What is tested? the classification against the exception this engine really raises when the peer's status line does not arrive in time.

How is success determined? against a peer answering after 1.5 s with a 200 ms response timeout, the call errors and the single event is WARN with outcome timeout and no status.

Why does it matter? the timeout names differ per engine; only the real engine proves the list. Given/When

should tee both bodies and send the correlation header through this connector

Rationale

What is tested? the two seams that touch the engine - the request tee wrapping the engine's ClientHttpRequest as the inserter writes, the response tee on the engine's body buffers - and the header the filter added to the request the engine sent.

How is success determined? the peer saw the correlation header and the body; one INFO event carries template, both bodies and the id, format-identical across connectors.

Why does it matter? a connector that bypassed the decorator or handed out buffers the tee cannot read would log empty bodies without any other symptom. Given/When

JettyConnectorIntegrationTest

4 tests.

should log a refused connection through this connector as ERROR failure and not as a timeout

Rationale

What is tested? the control - a connection the peer actively refuses must stay a failure, whatever type this engine wraps it in.

How is success determined? ERROR, outcome failure, -&gt; -, no status.

Why does it matter? a refusal misread as a timeout would send an operator looking for a slow peer that is in fact down. Given/When

should log this connector's connect timeout as WARN timeout

Rationale

What is tested? the classification against the exception this engine raises when the TCP connect never completes - a type that is NOT the same as its response timeout (Netty's ConnectTimeoutException extends ConnectException, not its TimeoutException).

How is success determined? against a tarpit that never completes the handshake, with a 200 ms connect timeout, the single event is WARN with outcome timeout, -&gt; - and no status.

Why does it matter? an unreachable peer is a timeout to an operator, not a refusal; the two dispositions call for different reactions. Given

should log this connector's response timeout as WARN timeout

Rationale

What is tested? the classification against the exception this engine really raises when the peer's status line does not arrive in time.

How is success determined? against a peer answering after 1.5 s with a 200 ms response timeout, the call errors and the single event is WARN with outcome timeout and no status.

Why does it matter? the timeout names differ per engine; only the real engine proves the list. Given/When

should tee both bodies and send the correlation header through this connector

Rationale

What is tested? the two seams that touch the engine - the request tee wrapping the engine's ClientHttpRequest as the inserter writes, the response tee on the engine's body buffers - and the header the filter added to the request the engine sent.

How is success determined? the peer saw the correlation header and the body; one INFO event carries template, both bodies and the id, format-identical across connectors.

Why does it matter? a connector that bypassed the decorator or handed out buffers the tee cannot read would log empty bodies without any other symptom. Given/When

ReactorNettyConnectorIntegrationTest

4 tests.

should log a refused connection through this connector as ERROR failure and not as a timeout

Rationale

What is tested? the control - a connection the peer actively refuses must stay a failure, whatever type this engine wraps it in.

How is success determined? ERROR, outcome failure, -&gt; -, no status.

Why does it matter? a refusal misread as a timeout would send an operator looking for a slow peer that is in fact down. Given/When

should log this connector's connect timeout as WARN timeout

Rationale

What is tested? the classification against the exception this engine raises when the TCP connect never completes - a type that is NOT the same as its response timeout (Netty's ConnectTimeoutException extends ConnectException, not its TimeoutException).

How is success determined? against a tarpit that never completes the handshake, with a 200 ms connect timeout, the single event is WARN with outcome timeout, -&gt; - and no status.

Why does it matter? an unreachable peer is a timeout to an operator, not a refusal; the two dispositions call for different reactions. Given

should log this connector's response timeout as WARN timeout

Rationale

What is tested? the classification against the exception this engine really raises when the peer's status line does not arrive in time.

How is success determined? against a peer answering after 1.5 s with a 200 ms response timeout, the call errors and the single event is WARN with outcome timeout and no status.

Why does it matter? the timeout names differ per engine; only the real engine proves the list. Given/When

should tee both bodies and send the correlation header through this connector

Rationale

What is tested? the two seams that touch the engine - the request tee wrapping the engine's ClientHttpRequest as the inserter writes, the response tee on the engine's body buffers - and the header the filter added to the request the engine sent.

How is success determined? the peer saw the correlation header and the body; one INFO event carries template, both bodies and the id, format-identical across connectors.

Why does it matter? a connector that bypassed the decorator or handed out buffers the tee cannot read would log empty bodies without any other symptom. Given/When

TwinContractTest

2 tests.

should pin the exchange and arrival message format to the literal twin contract

Rationale

What is tested? the MESSAGE half of the twin contract - the field names are locked by ClientLogFieldTest, the message text is pinned here in both twins.

How is success determined? a pinned filter renders the literal messages both twins ship.

Why does it matter? plain-text appenders and the README's parity promise key on this text; a divergence in one twin would otherwise ship silently. Given

should pin this stack's client tag and outcome vocabulary

Rationale

What is tested? the ClientStack.WEBCLIENT facts the shared metrics owner is parameterised with - the client tag of the gauge and the outcomes pre-registered on the events counter.

How is success determined? client=webclient, and exactly success, failure, timeout and cancelled in this order - the shared three plus the reactive disposition.

Why does it matter? alerts on adapter.logging.events{outcome="cancelled"} must find the value at zero from the start; a lost cancelled would silently empty the abandoned-call signal. Given/When/Then

UriTemplateAttributeTest

2 tests.

should mirror the private constant of DefaultWebClient literally

Rationale

What is tested? the mirrored URI_TEMPLATE_ATTRIBUTE against the private field of Spring's DefaultWebClient, read via reflection.

How is success determined? both strings are identical.

Why does it matter? the constant is private upstream and can only be mirrored; a change in Spring's derivation would drop adapter_url_template from every event without a compile error. Given/When

should see the URI template WebClient records for the template form of uri

Rationale

What is tested? the mirrored attribute name - DefaultWebClient's constant is private, so the module derives it the same way and this test proves the derivation against the real client.

How is success determined? a call through uri("/things/{id}", 7) shows the expanded URL on the request and the template under the mirrored attribute; an expanded URI shows no attribute.

Why does it matter? a renamed attribute upstream would silently drop adapter_url_template.