ADR-0006: Body logging is a mode per direction, gated by the outcome¶
- Status: accepted
- Date: 2026-09-03
- Context:
log-request-body/log-response-bodywere booleans:truemeant every body of every call. That is the switch that decides the log VOLUME, and it had only two positions - off, or everything. What is nearly always wanted in practice is "bodies only when something went wrong": that reduces the volume by orders of magnitude and hits exactly the calls a body is wanted for. The structure for it already existed - the outcome is final before the line is written - only the third position was missing. The catch is the request body: it flows before the outcome is known, so it has to be captured and, in the common case, thrown away. That costs the capture but saves the output, and the output is what burdens the log pipeline (ELK, the shippers, the indices).
Decision¶
Each body direction has a mode, never | on-failure | always, not a
switch:
never(the default) captures nothing for logging. A size meter may still install a count-only capture, exactly as before.on-failurecaptures the body on every call - bounded bymax-body-bytes, likealways- and writes it to the line only whenadapter_outcomeis notsuccess:failure,timeout, and on the WebClient twincancelled, or when the status is a 4xx. The response side decides at emission, when the outcome is final; the request side captures ahead and discards.alwayscaptures and logs on every call - the formertrue.- The gate is wider than the outcome vocabulary by exactly one status
class: a 4xx answer keeps its
successoutcome (the peer answered; the request was wrong) - levels, metrics and dashboards are untouched - but its bodies are logged, because a client's error is exactly the case a body explains. A 5xx is afailureand logs as well. A slow but healthy call stayssuccessand logs no bodies. Amended the same day: the first cut followed the vocabulary strictly and withheld 4xx bodies; that hid validation errors, the bodies most often wanted. - The former booleans are refused at binding time (
trueis not a mode name): an operator who believed body logging on must see the migration at startup, not discover a silentneverin production.
The mode lives in the shared core (BodyLogMode), so both twins and the
sibling project limesium, whose namespace mirrors this one, gate the same
way.
Consequences¶
- Body logging becomes affordable outside a debug session:
on-failurelogs the bodies that explain an incident and nothing else. on-failurecosts the request-body capture on every call (memory up tomax-body-bytesper in-flight exchange, and on the reactive twin the tee's transient copy per buffer), whether or not the line ends up with a body.measure-*-body-sizeis unchanged: it still measures what flowed, in every mode.- Source and configuration change:
true/falsebecomealways/never; the properties' type changes fromBooleantoBodyLogMode. The reference configuration, the lockstep tests and both twins' guides carry the new vocabulary.
Amendment (2026-09-05): the gate follows the exchange's outcome, not the caller's¶
A 200 whose body the application's Jackson converter or decoder cannot map to the requested type is a
failure for the caller and a success for the exchange: every byte flowed, the status is final, and the
decoding happens above the interceptor resp. downstream of the body tee, where neither client offers a
seam that could inform the module. on-failure therefore withholds the bodies of exactly the answer an
operator would want to see. This is accepted as the boundary of the design - the module observes the
wire - and documented in the guide (ยง6.3): always is the way to capture such a peer's answers during
an analysis. Both twins pin the behaviour with a test, so a future change is a decision.