The Shared ID - One Lookup Instead of Five Disconnected Logs

I built a five-step code review pipeline to see how bad log correlation actually gets once an agent chain has a revision loop in it. A planner agent breaks down the task, a retriever pulls the relevant files, a code-generator writes the diff, a test-runner checks it, and a reviewer either approves it or sends it back to the generator for another pass. Fifteen scripted tasks went through it, including a simple one: fix an off-by-one bug in a pagination function. One of the generated pull requests passed review, merged, and got reverted three days later in CI because it broke a downstream job.

I handed that incident to an engineer and asked the obvious question. Which step caused the bad diff.

Every one of the five services had logged its own work correctly. Each wrote a timestamp and a self-generated request ID to its own local log file, exactly as I'd built it. None of them passed anything to the next service that would let a person tie those five entries back into one causal chain. The revision loop made it worse. A rejected diff bounces from the reviewer back to the generator and comes around again, and retries and async handoffs desynchronize the timestamps enough that lining up five files by clock time still doesn't reliably show which pass produced the merged diff. My engineer ended up cross-referencing five files by hand against approximate timestamps, for an incident whose actual cause was one specific generator pass on one specific retry.

I built a dashboard to make that gap impossible to wave off. Across a fifteen-incident set, each one carrying a documented ground-truth cause, trace completeness, meaning the share of incidents I could reconstruct from the logs alone, sat under 40 percent. Five complete, correctly written logs, and I still couldn't solve half the incidents from them.

The problem

A trace proves execution. It doesn't prove causation, and those are two separate claims computed by two separate mechanisms. What I had after the revert was five perfectly formed records of five services doing exactly what they were built to do, with no shared thread connecting any of them. The retriever's log knew nothing about the planner's request. The reviewer's log, especially across a revision loop, might contain two or three entries for the same underlying task with no way to tell which one belonged to which attempt.

The failure isn't sloppiness. Every service in that pipeline was logging carefully. The failure is architectural: nobody decided, at design time, that a single request crossing five independent services needed a single identifier that survives every hop, every retry, and every loop back. Without that decision made in advance, an incident review becomes an archaeology project, and the revision loop is exactly where it gets worst, because that's where the same task legitimately produces multiple log entries that look like they could belong to different requests entirely.

The pattern

The fix is close to the simplest thing I've built in this space, and that simplicity is the point. Generate one trace_id, a UUID, exactly once, at the moment the request enters the system, when the planner first receives the task. Inject it into the shared state that gets passed through the graph. Forward it on every inter-service call as a header, and write it into every single log line, including the ones produced inside the reviewer's revision loop.

Then build one endpoint: a GET /trace/{trace_id} that pulls every service's log entries for that ID and returns them in causal order. Reconstructing an incident stops being a five-file archaeology project and becomes a single lookup.

On the same fifteen-incident set, with this change and nothing else, trace completeness moved from under 40 percent to at or near 100 percent. I also track a propagation_gaps counter, the number of handoffs where the ID failed to carry through, and I want that sitting at zero. I verify it with a test that specifically checks the revision loop, because that's the one place in this architecture where an async handoff is most likely to drop the header silently. A propagation mechanism that works everywhere except the one place retries happen isn't actually fixed.

The industry's own instrumentation standards confirm this is still something most teams have to build for themselves rather than get for free. OpenTelemetry's GenAI working group has been standardizing span attributes for individual LLM calls since 2024, and by 2026 the core chat and embedding conventions are stable enough to build a dashboard on. The conventions for multi-agent, multi-step orchestration, the part that would need to exist before a trace could represent a chain like mine uniformly across vendors, are still experimental. The pattern I'm describing here is plumbing I built myself, on top of LangGraph state, because the standard for doing it uniformly across tools hasn't finished forming yet.

Design considerations

The honest limit of this pattern is baked into what it actually answers. A propagated trace ID tells me, completely and reliably, which five services touched a given request and in what order. It says nothing about which of those five services' outputs actually caused the reverted diff. I can have a perfect trace ID sitting on top of a perfectly reconstructed five-step timeline and still be staring at five clean, causally uninterpreted log entries. Propagation makes the timeline legible. It doesn't compute a verdict about which entry in that timeline was at fault. Anyone building this and expecting it to answer "why" is going to be disappointed the first time they use it on a real incident.

The place this pattern earns its cost is exactly where I found it: chains with retries, branches, or revision loops, where the same logical request produces more than one physical execution path through the system. A single-agent, single-call system doesn't need this. There's nothing to correlate. The value shows up the moment a request can legitimately touch a service more than once, because that's when timestamp-based correlation stops working even in principle, not just in practice.

I also don't treat this as free once it's built. The propagation has to survive every future change to the pipeline, every new service someone bolts on, every new async pattern an engineer introduces without realizing it needs to carry the header forward. I check for gaps with an explicit test rather than trusting that the discipline holds by convention, because in my experience it doesn't. Somebody eventually adds a sixth service, forgets the header, and the trace completeness number quietly degrades until an incident review turns up the gap the hard way. Building the propagation is a day's work. Keeping it complete is an ongoing one, and I budget for that the same way I budget for keeping any other invariant true across a codebase that keeps changing under it.