← Back to the library
OperationsApplied · 8 min

You can't follow one request across services

“A user reports a failed checkout. Each service has logs and maybe spans, but nothing connects them, and the trace stops at the gateway or the message queue.”

THE PRINCIPLE

One trace id from the edge to the last consumer: propagate W3C traceparent on every hop, including messages, and stamp it on every log line.

FIRST MOVES

  1. Pick one failing request and check each hop for an incoming traceparent header. The first service that starts a new trace id sits just past the break.
  2. Enable OpenTelemetry auto-instrumentation (the zero-code agents or the language SDK's instrumentations) in every service, using the default W3C TraceContext propagator everywhere.
  3. For queues, cron jobs, and custom protocols, inject the context into message headers when producing and extract it when consuming. Without that, the consumer's work becomes a separate root trace.
  4. Make sure gateways, meshes, and CDNs forward traceparent and tracestate, and that a legacy B3 or vendor format is configured as an extra propagator rather than replacing W3C.
  5. Put trace_id and span_id on every log line through the OTel log bridge or your logger's integration, and return the trace id to clients in error responses.

TOOLS: THEN → NOW

Per-service request ids and grep across log storesW3C Trace Context propagated by OpenTelemetry, with logs carrying trace_id
Vendor-specific agents and header formatsOpenTelemetry SDKs and the Collector exporting OTLP to any backend

PATTERN SNAPSHOT

import { context, propagation, trace } from "@opentelemetry/api";
const tracer = trace.getTracer("orders");

// Producer: carry the current trace in the message headers.
const headers: Record<string, string> = {};
propagation.inject(context.active(), headers); // adds traceparent
await queue.publish("order.created", payload, { headers });

// Consumer: continue the same trace instead of starting a new one.
queue.subscribe("order.created", (msg) => {
  const parent = propagation.extract(context.active(), msg.headers);
  return context.with(parent, () =>
    tracer.startActiveSpan("process order.created", (span) =>
      handle(msg).finally(() => span.end())));
});

CLOSE THE AI. EXPLAIN THIS.

A trace shows the HTTP request but the worker's spans appear as a separate trace. Where was the context lost, and what two calls fix it?

HOW IT WORKS UNDERNEATH

SOURCES

Guide reviewed