← Work

From "grep every pod and hope" to one trace ID across a 7-service chain

A record disappears somewhere inside a seven-service pipeline, and nobody can say where. The story of the baggage-tag trick that gave the system a memory — even the part running on a tool that can't read the tag.

Stack
OpenTelemetry Collector, Grafana, Tempo, Loki, Prometheus, Spring Boot, Kubernetes
Result
From guessing which of seven services (plus NiFi, twice) swallowed a record, to pasting one ID and watching its whole journey unfold.

The mystery of the vanishing record

A record goes in. Sometimes it comes out wrong. Sometimes it doesn’t come out at all. And on the day I joined, nobody could say why — not because nobody was smart enough, but because the system had no memory. It couldn’t tell its own story.

Think of every service the record passes through as a room in a building. The record walks in the front door, and gets handed from room to room until it’s done:

Request chain from NiFi through seven services

Seven rooms, one front door. Simple enough — except it isn’t quite that simple. A few of the doors between rooms are unusual, and each one turns out to matter later in this story.

Door #1: a conveyor belt, not a handshake. Most rooms hand the record directly to the next one. Rooms 1 and 2 don’t — they use RabbitMQ, a queue, sitting between them like a conveyor belt instead of a handoff:

Service 1 publishes to RabbitMQ, delivered to Service 2

Door #2: an outside courier, mid-building. Somewhere between Room 3 and the rest, the record leaves the building entirely and goes back through NiFi — the same tool that walked it in the front door — before coming back inside to keep going:

Service 3 hands off through NiFi mid-chain to the rest of the services

Door #3: someone doubles back. Room 7 doesn’t just finish the job — sometimes it needs something from Room 3 and calls it directly, out of order:

Service 7 calls back to Service 3

A conveyor belt, a courier who leaves and comes back, and a room that calls backwards. Keep those three in your pocket — they’re the whole reason this was hard, and they’re where the story is going.

Nobody could tell the same story twice

Here’s how you’d investigate a vanished record, on the day I joined: pick a room, open its logbook, read every line by hand, and guess. Nothing in any room’s logbook said which entries belonged to which record. Nothing connected Room 1’s story to Room 2’s story, let alone across a conveyor belt, an outside courier, and a room that calls backwards.

That’s not investigation. That’s guesswork with extra steps.

What a good detective actually needs

Three questions come up in every incident, every time:

The problem was never a missing tool. It was that logs, traces, and metrics spoke no common language. Fixing that mattered more than which product did the fixing.

Don’t shout into every room — install one intercom

The tempting shortcut: wire every service straight to its own logging tool, tracing tool, and metrics tool. Rejected on purpose, for reasons bigger than convenience:

Service chain feeding an OpenTelemetry Collector fanning out to Tempo, Loki, Prometheus, and Grafana

One intercom, fed by every room, repeating to three purpose-built backends — plus whatever a given deployment needs on top — and one Grafana screen in front of all of it, so nobody has to remember which tool answers which question.

Teach the trick once, not seven times

None of this works if instrumenting a service is a manual chore developers forget, skip, or get slightly wrong seven different ways. Luckily every room already shared one thing: a common internal library everyone depended on for cross-cutting concerns. That’s where the fix landed — extended to own the entire OpenTelemetry setup on its own: wiring the SDK, propagating the trace on every outbound call, and stamping every log line with the same ID, self-configuring from one standard YAML block.

Onboarding a new room now takes two steps, full stop:

  1. Add the dependency to the build file.
  2. Add or update the YAML config block (collector address, service name, sampling rate).

No developer writes a span. No developer wires an SDK. No developer touches a correlation ID by hand. The library does it the same way, every time — so the quality of the trace never depends on who happened to write that particular room.

The baggage-tag trick

Here’s the idea underneath everything above, in one sentence: attach an ID to the record the moment it arrives, and make sure every room passes that same ID to the next one — like a baggage tag at an airport. Nobody needs to know what’s inside the suitcase. They just need to keep the tag attached when they hand it off.

That “tag” is a real, standard thing: a W3C traceparent header, riding along on every call, plus the same value pushed into every room’s log context (MDC) so every log line carries it too. Simple, as long as every room speaks the language that reads and re-attaches tags.

The problem: two rooms in this building don’t.

NiFi, at the front door, has no tag to relay. Nothing arrives before the front door — no upstream caller carries a tag to begin with, because this is where the record first exists. So the tag gets created at Room 1, the first room that actually understands tags. The short walk from the front door to Room 1 stays untagged — a known, accepted gap, not an oversight.

NiFi, in the middle, is holding a record that already has a tag. By the time it comes back through NiFi, Room 3 already tagged it. NiFi doesn’t need to read the tag or understand it — it just can’t be the room that loses it. So NiFi does the one thing it’s good at: carrying things without opening them. A NiFi processor lifts the incoming traceparent value off and sticks it onto a FlowFile attribute; the outbound call is configured (via InvokeHTTP’s “Attributes to Send” setting) to stick that same value back on as a header going out the other side. No OpenTelemetry SDK ever runs inside NiFi. It’s a courier, not a reader — and that’s exactly enough.

The conveyor belt gets the same trick. When Service 1 publishes to RabbitMQ instead of calling Service 2 directly, the shared library rides the tag along as a message header on the way in, and reads it back off the same way on the way out. The belt doesn’t know what a trace is either. It doesn’t need to.

Put every room, both doors, and the backwards call from Room 7 into one picture, and the tag’s journey looks like this:

Sequence diagram of a trace ID propagating across NiFi, the service chain, and the callback

How the story ends now

Paste the tag into Grafana. Get the entire journey — every room, the conveyor belt, the trip back out through NiFi and in again, the call backwards — as one waterfall, in order, with every log line one click away from the exact moment it was written. No guessing which pod. No eyeballing timestamps. No archaeology.

Nothing here is one dramatic number. It’s a category change in what debugging feels like: