What to Log So an API Failure Is Traceable
An API failure at 2:14 on a Tuesday morning leaves behind three artifacts: a stack trace that points at a line of application code, a metric graph that dipped for forty seconds, and a support ticket that says "the app is broken." None of those three things tells the engineer who gets paged where the failure started. Traceability is the gap between the alert and the answer, and logs are what fill it.
The distinction worth holding onto is between logging that helps a developer debug locally and logging that lets a stranger reconstruct an incident across services, hours later, with no access to the running system. Most teams do the first well and the second badly, often in the same codebase.
The correlation identifier is the spine
Every request that enters a system needs an identifier that travels with it through every hop. This is the single most important logging decision, and it is also the one teams most often get wrong in a quiet way: they generate a request ID at the edge, log it once at the gateway, and then let it evaporate before it reaches the service that fails.
The identifier has to be threaded. A payment request that touches an API gateway, an authentication service, an order service, and a payment processor needs the same string on every log line each of those produces. When the payment processor times out and the order service logs a 504, an engineer should be able to paste that string into the log search and see the entire journey in order, from first byte to final error, without guessing which timestamp belongs to which service.
A correlation ID that stops at the gateway is worse than none, because it creates the illusion of traceability. The dashboard looks instrumented. The logs look structured. Then an incident happens and the string appears on four lines out of forty, all from the same host. The team learns, mid-incident, that the thing they trusted was decorative.
Log the decision, not just the outcome
A 200 does not explain why a request got a 200. A 403 does not explain which rule rejected the request or which identity was in play. The pattern that makes failures traceable is to log the branch the code took and the inputs that caused it, not only the status code at the end.
Consider an authorization check that returns 403. The failure log that says "authorization denied" is a dead end. The log that says "authorization denied: principal 4471 lacks role billing:write on resource invoice/88213, decision from policy rev 19" gives the on-call engineer a place to look. It names the identity, the missing permission, the resource, and the policy version. The engineer can then ask a concrete question: did the policy change, did the role assignment get lost during a migration, or was the request simply wrong?
The same logic applies to timeouts. A log line reading "upstream timeout" tells nobody anything. A line reading "upstream timeout after 3000 ms calling inventory-service GET /stock/88213, attempt 2 of 3, circuit breaker state closed" tells an engineer the timeout budget, the target, the retry position, and whether the breaker was already tracking failures. Those are the facts that separate a one-off blip from a dependency that is degrading.
A timestamp is not enough; the clock source matters
Cross-service reconstruction lives or dies on timestamp quality. Logs from three hosts with ten seconds of clock drift between them will produce a timeline where the cart service appears to respond before the request reached it. That impossible ordering sends people down false trails.
The fix is unglamorous. Hosts should sync clocks, and log lines should carry a monotonic sequence number alongside the wall-clock time so that ordering within a single process survives. If a service processes two requests on the same thread, the sequence number disambiguates them. Log pipelines should preserve the original timestamp from the emitting service rather than stamping ingestion time over it, because ingestion time shuffles under backpressure and makes a burst look like a calm trickle.
When logs from two services disagree about ordering, the engineer needs a way to tell which clock is lying. A clock_drift_ms field emitted periodically by each host is a small thing, but it turns a mystery into a lookup.
Structured fields beat prose every time
A human reads "failed to charge card for user 4471 due to processor error" and understands it. A log aggregator reads it and sees a string. Traceability at scale requires that every fact worth filtering on is a separate field with a stable name.
That means a log line for a failed charge carries, at minimum: event, request_id, user_id, order_id, processor, processor_error_code, amount_cents, currency, attempt, duration_ms, service, version. The message field can then hold whatever prose the developer likes, and nobody has to write a regex to extract a number from it.
The upgrade from unstructured to structured logging is not a rewrite. It is a slow accumulation of discipline, usually starting with the three or four fields that appear in every incident, then extending as new failure modes show up. The test is simple: if an on-call engineer has ever wanted to filter logs by a fact, that fact should be a field.
Log the version and the configuration in force
Half of the traceability problem is time, and the other half is state. A request that failed at 09:12 on a Thursday ran against a specific deployment, a specific feature-flag configuration, and a specific set of downstream endpoints. If the logs do not record those, the incident becomes an exercise in archaeology across deployment records and config management.
Every log line should carry the service name and the build version. That is table stakes. The more useful addition is the flag state for any request that touched a feature-gated path. A single field like flags=[checkout_v2, express_pay] on the relevant lines tells the engineer whether the failure correlates with a rollout. When an incident turns out to be caused by a flag flipped at 09:05, the logs that record flag state turn a two-hour hunt into a five-minute confirmation.
Redaction belongs inside the logger
The reason engineers log too little is often fear: logging the request body may write a credit card number or a session token to disk, and that is a compliance incident waiting to happen. The response most teams choose is to log less. The better response is to build redaction into the logging layer so that the safe thing and the useful thing are the same thing.
A redacting logger can strip known-sensitive fields by name, hash identifiers that need correlation but not exposure, and truncate payloads to a safe length while preserving structure. Once that layer exists, logging the request shape, the headers minus authorization, and the response shape becomes routine rather than risky. The traceability problem gets easier the moment logging is not a security hazard.
Sampling that keeps the failures
High-volume services cannot log every request at full fidelity. Sampling is necessary. The mistake is uniform sampling, which drops the rare failure at the same rate as the common success. A payment service that logs one in a hundred requests will, over a bad hour, lose almost every trace of the actual errors and keep a rich archive of the requests that worked.
The sane policy is to sample successes aggressively and failures not at all. Errors, timeouts, and 5xx responses get logged in full. The 200s get sampled. A middle tier of slow requests, say anything over the p99 latency threshold, also gets logged in full, because latency incidents are often invisible in error logs and only show up as a cluster of slow-but-successful requests.
The log line that answers the first question
There is a way to test whether logging is traceable, and it does not require an incident. Take a recent failure from production and hand its alert, and only its alert, to an engineer who did not work on that service. Give them log search and nothing else. If they can reconstruct what happened, in what order, and why, within fifteen minutes, the logging is doing its job. If they spend that time asking which service owns which log line, it is not.
The test fails most often at the same point: the engineer finds a log line with an error, and then cannot find the request that caused it, because the correlation ID was never threaded or the line was sampled away. That is the gap to close first. A team that logs three fields well, with a correlation ID on every line, a version on every line, and failures never sampled, is far ahead of a team that logs thirty fields that no one can join.
Traceability is a property of the whole log corpus, not of any single line. The corpus either lets a stranger walk from symptom to cause or it does not. The work of building it is mostly a series of small, boring decisions made before the incident: which identifier travels, which facts become fields, which failures escape sampling. Made well, those decisions turn a 2:14 a.m. page from a mystery into a lookup. Made badly, they turn a forty-second dip into a four-hour outage while engineers read prose and guess.

