Skip to content
Kien

What to log when the bug is elsewhere

· 6 min read

In integration work the defect usually lives in a system you cannot read, so a log stops being a debugging aid and becomes evidence you may have to show a third party. What that changes about the design of the record — capture as it left, keep the body on a 200, make it re-firable — and the three constraints that stop the evidence from becoming its own outage.

  • logging
  • integrations
  • observability
  • evidence
On this page
  1. As it left, not as you intended
  2. Keep the body even when the status was 200
  3. A record that can be re-fired
  4. Let the schema decide which fields are recorded
  5. Absent, empty and null are three different facts
  6. Three constraints, or the evidence becomes the incident
  7. The rule

Most logging advice assumes the bug is yours. Under that assumption a log line is a note to yourself: enough to jog your memory about code you wrote and can re-read at will.

Integration work breaks that assumption. When a booking behaves strangely, the defect is often in a system I have no access to — a supplier's platform, a partner's middleware, something behind an endpoint and a PDF. I cannot read that code or add a print statement to it. What I can do is show what crossed the boundary, and for that to settle anything it has to convince a reader who does not work here and starts from the assumption that the problem is mine.

So the design goal changes. The log is not an aid to my memory. It is evidence, and it will occasionally be attached to a message to another company. Everything below follows from that. (Reading these records is a different discipline and another post; this one is about what has to be in them.)

As it left, not as you intended

The first rule is the one most implementations get wrong: capture the request as it actually went out, not the object you assembled.

Those are different artifacts, and they differ exactly where it matters. An HTTP client adds headers, an interceptor injects a token, a serialiser drops keys and formats a date its own way, a retry wrapper rewrites something on the second attempt. Log the object you built and you have logged your intention — a faithful record of what you believed you were sending, and worthless in an argument about what the other side received.

So the capture point is the last one before the bytes leave: the serialised body, the final header set, the resolved URL. And it must sit where no later code can mutate it. A record that some middleware can still touch after it is written is not evidence, because the first question a careful reader asks is whether the log reflects the call or the code's opinion of the call.

Keep the body even when the status was 200

Some suppliers answer HTTP 200 with an error inside the body. That is normal in this trade, and it destroys the most common logging shortcut there is: keep responses for failures, drop them on success. Under that policy the one record you needed — the successful-looking call carrying a rejection — is the one you discarded, and you find out days later when someone asks why a booking never confirmed.

The status code is a transport opinion. In integration logging it is a field in the record, not a filter on whether the record exists. Keep the response body, on every call, at whatever retention you can afford — and if you must sample, sample by traffic, never by outcome.

A record that can be re-fired

The property that makes a log entry stop being an argument is reproducibility. Our provider logs are kept complete enough that a failed call can be re-fired as the exact request that was sent, rebuilt as a curl.

The effect on a conversation is out of proportion to the effort. "We believe we sent a valid request" invites a discussion about beliefs. "Here is the request; run it and you will see what we saw" ends it — either it reproduces, and the ball is on their side, or it does not, and something about the environment differs, which is now the finding. Both beat a thread comparing recollections. A claim you can replay is a claim you can test.

Let the schema decide which fields are recorded

When a record summarises the fields of a request, the field list should come from the endpoint's own validation schema rather than be hand-picked. The schema is the complete statement of what the endpoint accepts; a hand-written list is a snapshot of what somebody thought mattered that day, and it goes stale on the next release without anyone noticing.

Deriving it also removes a maintenance trap: one list, in one place, moving when the contract moves. A hand-kept second list will eventually omit the field that turns out to be the whole problem, and omit it silently.

Absent, empty and null are three different facts

A field that was never sent, a field sent blank, and a field sent explicitly null mean three different things, and often produce three different behaviours downstream: one falls through to a default, one overrides it, one is rejected outright.

The important word is preserve, and it belongs at capture time rather than at reading time. Most serialisers erase this distinction for free — a key with an undefined value simply vanishes on the way to JSON — so by the time anyone looks at the record, the evidence that separates the three cases is already gone. That is a capture decision disguised as a formatting detail, and it has to be made deliberately, once, in the code that writes the record.

Three constraints, or the evidence becomes the incident

Everything above pushes toward recording more. Three things push back, and they are not negotiable.

Credentials never enter the record. Tokens, passwords, API keys and card data are stripped at the point of capture — not masked on display, not filtered by the viewer. A record that ever held a secret has held it, and the whole point of these records is that they get zipped into folders and sent to other companies.

Volume has to be bounded. A full request and response for every supplier call is, at search volume across dozens of integrations, one of the largest write streams in the system. So it lives behind a flag, with retention, and a deliberate decision about which environments run it wide open. I have no figure for what a day of full provider logging costs in storage — nobody has priced it in a way I could quote — but the shape of that number is why the flag exists.

The logging path must fail soft. If the log write throws, times out or fills a disk, the request it describes must still complete. A service that falls over because it could not write a log has converted an observability feature into an outage — and it will do that when traffic is highest, which is also when the logs would have been most useful.

The rule

Log for the reader who cannot read your code. That is the whole design: what left the process, what came back, verbatim, replayable, secrets removed, distinctions intact. Everything you keep is something you will not have to argue about later. Everything you summarised away is something you will one day be asked to prove.

Related posts

· 4 min read

In most systems the expensive test outcome is red. In a booking system it is green — a passing test just bought something. How the full flow gets tested anyway: a selection rule that doubles as the safety mechanism, and an evidence pack, not a checkmark, as the thing partners actually review.

  • testing
  • integrations
  • QA
  • evidence

· 5 min read

"So you call other people's APIs" is the usual summary of my job, and it is wrong in an instructive way. Around 150 supplier codes on about 90 integrations, two documented offer patterns, two execution models behind one public contract, and suppliers that answer 200 with an error inside — a tour of what the work actually consists of, and why it is architecture.

  • integrations
  • architecture
  • API design
  • distributed systems