The first two pieces in this thread were about individual lines: write them for a reader with no context, and don’t write so many that the reader drowns. Both are necessary and neither is sufficient, because debugging almost never involves reading one line. It involves reconstructing what happened to one request — which service received it, what it called, where the time went, and which step produced the wrong answer. That reconstruction is an assembly job, and whether it’s possible at all is decided long before the incident, by whether the lines carry something that ties them together.

That something is usually a single identifier, generated at the edge and passed along through every call the request makes, logged by every service that touches it. With it, a search returns the request’s whole story in order across every system involved. Without it, you have a timestamp and a guess: you filter by a rough window, get everything from every concurrent request, and try to pick out which lines belong to yours by squinting at user IDs and plausibility. In a low-traffic system that sort of works. Under real load it does not, and the failure is worst exactly when traffic is highest — which is when incidents happen.

Propagation is where this usually breaks, and it breaks quietly. The identifier gets attached at the front door, flows through two services, and then hits a boundary that drops it: a queue whose message format has no field for it, a third-party client that doesn’t forward custom headers, a background job kicked off without inheriting its parent’s context. Everything before the gap is traceable, everything after is anonymous, and nobody notices until an investigation runs into the wall. The property worth verifying deliberately — not assuming — is that the identifier survives every hop in the system, especially the asynchronous ones, where the connection between cause and effect is already hardest to see.

It’s also worth being clear about what this buys, since tracing tools do a version of the same thing. Traces are better at showing structure and timing — the shape of the call graph, where the latency accumulated. Logs are better at recording what a step actually decided and why, in terms specific to the domain. The correlation identifier is what lets them be one picture rather than two: the trace tells you which step was slow or failed, and the logs for that step, found by the same identifier, tell you what it was working with when it did. Neither replaces the other, and the identifier is the cheap piece of infrastructure that makes both worth more than they are alone.

So the thread’s conclusion: the value of a logging system isn’t the lines it holds, it’s the questions it can answer, and nearly all the important questions are about one specific request. Write each line so it makes sense to a stranger, keep few enough that they can be found, and thread them all onto an identifier so they can be assembled into a story. That last part costs almost nothing to add at the start and is painful to retrofit — which is why it’s worth being the thing you check first, rather than the thing you discover missing at the worst possible moment.