OpenTelemetry · beginner

OpenTelemetry trace-continuity checklist

A grounded OpenTelemetry trace-continuity checklist for verifying that a single logical transaction stays correlated across HTTP boundaries, async/queue hops, and process boundaries. Use this guide when distributed traces arrive in the backend as multiple unrelated trace IDs, gaps appear in the trace timeline, or propagation headers are missing or stripped by an intermediary.

The symptoms

  • Single user request renders in the backend as two or more independent trace IDs with no shared parent, even though logs show the same request id flowing across services.
  • Trace UI shows a short root span and then a "discontinuity" or unrelated subtree joined by manual link annotations only.
  • Span B lists span A in its parent field but no edge is drawn because the referenced trace_id differs from B's trace_id.
  • Async work (queue consumer, scheduled job, callback) is correlated in logs via an explicit job id but never appears as a child of the originating trace span.
  • Sampling decisions differ across services: the root span is kept while downstream spans appear as "dropped by sampler" placeholders.
  • Propagation is correct in tests but breaks only behind a specific proxy, ingress, or message envelope observed in environment-specific evidence.

Likely causes

  • A service creates a brand new tracer and forgets to extract the W3C traceparent header on the inbound side, so it starts a new root span instead of continuing the parent.
  • The W3C tracecontext (or compatible) propagation header is set on the outbound request but stripped, rewritten, or downgraded by an edge proxy, service mesh sidecar, or CDN before the next service reads it.
  • For background or queue work the producer injects context into the message envelope but the consumer uses a different propagator and never extracts it, starting a fresh root span.
  • Manual context propagation passes only a correlation id string while trace_id and span_id are replaced, severing lineage even when logs look consistent.
  • Mixed propagators coexist across polyglot services: one side writes W3C traceparent, the other reads B3 or a custom header and silently falls back to a new root.
  • Sampler is configured per-service with inconsistent rules so the parent is recorded but children are not sampled, producing visible root spans with empty subtrees in the trace UI.

First ten minutes

  1. 01Capture one concrete end-to-end request: note the user-visible request id, the entry service, and every downstream service that logs the same id. Build a service-by-service map before touching configuration.
  2. 02On the entry service, inspect the outbound HTTP client or message producer for an explicit propagation step using the configured propagators (commonly W3C tracecontext plus baggage); confirm the header name and format match what the receiver expects.
  3. 03On the downstream receiver, confirm the inbound server or consumer registers a propagator that matches the inbound header; missing registration usually defaults to a new root span with no warning.
  4. 04Diff the propagator registration across each hop in the request path; mismatched or partially registered propagators are the most common root cause and are visible without any code changes.
  5. 05Sample one real trace and record the exact trace_id at every hop in code-aware logs or in an export of the collector, to confirm where the lineage first breaks before changing any configuration.

Evidence to collect

  • Propagation headers (commonly traceparent, tracestate, baggage, or vendor-specific equivalents) as written on the wire at each hop, captured via a proxy log or a controlled test fixture.
  • Active span context at process entry and exit for each service: trace_id, span_id, parent_span_id, and trace_flags, taken from the SDK's span processor or a diagnostic exporter.
  • Sampling decision per service in the trace_id lineage: whether the span was recorded, the sampler that produced the decision, and any parent-based override that may have been skipped.
  • For queue or async hops, the message envelope or job payload fields that were expected to carry context, plus the fields the consumer actually read.
  • Backend trace UI rendering for the same logical request: number of root spans, presence of parent edges, and any sampler-skipped placeholders linked to the original trace_id.

Where to look

  • SDK initialization in each service: the registration order of tracer providers, propagators, and resource detectors, where a missing propagator silently falls back to a new context.
  • Outbound client instrumentation middleware: where headers are injected, and whether explicit context injection overrides the automatic instrumentation in a way that drops the active span.
  • Inbound server or consumer instrumentation: where the incoming context is extracted and how the parent is linked, including any custom middleware that reads headers before instrumentation runs.
  • Edge boundaries between services: ingress controllers, API gateways, service mesh sidecars, and load balancers that may rewrite or strip request headers according to allowlists.
  • Message broker boundaries: producer instrumentation that injects context into message properties or headers, and consumer instrumentation that extracts context before processing the payload.
  • Sampler configuration in each service's SDK initialization, especially mixed parent-based and probability-based samplers that can produce divergent decisions across services.

Diagnostic steps

  1. 01Verify that the same propagator names are registered on every hop in the request path; a service registering only an outbound propagator but not an inbound one commonly produces a fresh root on the receiving side.
  2. 02Compare the trace_id emitted by the entry service against the trace_id of the first downstream span; if they differ, the break occurs at the first hop, which localizes the search to the network boundary.
  3. 03Check whether the receiving service logs an explicit "no parent context found" or similar diagnostic line from the propagator, which indicates a header was missing or the propagator is misconfigured rather than the client failing to send it.
  4. 04For queue hops, inspect one representative message envelope and confirm it carries the propagation fields the consumer expects, including field name and encoding; mismatched encoding frequently breaks continuity without producing errors.
  5. 05Distinguish sampling discontinuity from propagation discontinuity: if the trace_id is identical across hops but only some spans are recorded, the issue is a sampling decision rather than missing propagation.
  6. 06Reproduce in a minimal environment with the same two services and the same edge proxy in the path; if continuity returns when the proxy is bypassed, the proxy is rewriting or removing the header.
  7. 07Confirm the receiver creates a SpanKind.SERVER or consumer-type span as the parent of subsequent work, since a CLIENT-kind span on the receiving side indicates extraction did not run.
  8. 08For polyglot environments, verify the header set on one language matches the header set the other language's SDK reads; mismatched header names commonly present as identical trace_ids in logs but distinct trace_ids in the backend.

Common mistakes

  • Adding a custom request-id header alongside W3C tracecontext and assuming the correlation is sufficient; the trace_id is what builds the trace tree, not the application-level request id.
  • Configuring propagation in only some services while leaving others with default initialization that does not register the expected propagator at server entry.
  • Assuming a service mesh handles propagation for every protocol; HTTP/2 and gRPC are commonly covered, but specific message brokers and proprietary RPCs require explicit handling.
  • Manually building a span from a copied trace_id and span_id without using the configured propagator extraction, which can attach visually but breaks causality and sampling lineage.
  • Trusting a green unit test that mocks the network; tests that bypass the real header path commonly mask propagation bugs that only surface behind real intermediaries.
  • Diagnosing sampling gaps as propagation gaps, which leads to changes at the wrong boundary and may reduce trace coverage without restoring continuity.

Safe fixes

  • When the receiver shows a new root span and a missing-parent diagnostic, register the matching propagator on the inbound side before any custom middleware reads the request, then re-check the trace_id lineage end-to-end.
  • When an intermediary strips headers, adjust its allowlist to permit the propagation header names and encoding used by your SDKs, then verify with a captured request that the header survives the hop unmodified.
  • For queue or async hops, store propagation fields in message headers using the SDK's inject/extract helpers on both sides rather than copying the trace_id string by hand, then confirm the consumer's root span carries the producer's trace_id.
  • For mixed propagator environments, standardize the propagator registration order across services and add a startup log that lists active propagators, so misregistration is visible at boot rather than at first failure.
  • For divergent sampling decisions, adopt a parent-based sampler consistently across services and confirm that a sampled parent yields sampled children before adjusting probability-based rules elsewhere.
  • When fixing, change one boundary at a time and re-capture the same end-to-end trace; if continuity does not return, revert before modifying the next boundary to avoid stacking unrelated changes.

Prove the fix

  1. 01Submit one synthetic request through the full path and confirm a single trace_id appears for every span across entry, downstream, and background work, observable in the backend trace UI as one tree, not multiple roots.
  2. 02Inspect the receiving service's root span attributes: trace_id equals the producer's trace_id, parent_span_id matches the producer's last emitted span_id, and SpanKind matches the protocol (SERVER for HTTP, CONSUMER for queue work).
  3. 03Run the same request ten times under nominal load and observe that the trace tree structure, sampler outcome for the root, and hop count are stable; intermittent split traces or sampler-skip placeholders indicate the fix is partial.
  4. 04Capture the propagation header at the network boundary before and after the fix and confirm byte-for-byte equality of the trace_id and parent-id fields, proving the boundary no longer rewrites the context.
  5. 05Verify that an existing alerting or sampling rule that depends on parent span attributes still fires for traces that traverse the previously broken hop, since restored continuity changes the lineage that rules evaluate.

Prevention and next steps

  • Treat propagator registration as a service-startup invariant: log the active propagator list at boot and assert in CI that each service's registration matches the agreed polyglot standard.
  • Add a synthetic end-to-end probe that issues one request through the full path and asserts a single trace_id across hops; run it on a schedule so propagation regressions are caught early rather than at incident time.
  • Maintain a header allowlist contract for every intermediary in the request path, reviewed alongside SDK upgrades, since edge configuration is a common silent cause of lost context.
  • When introducing a new propagator or transport, extend the same end-to-end probe before deployment and keep the propagation header name visible in architecture documentation for each hop.
  • Standardize on parent-based sampling across services so that a sampled root always yields a sampled lineage, reducing the chance that an upstream sampling decision masks a downstream propagation issue.

Safe commands and checks

ps -eo pid,comm | grep -E '<service-binary-name>'  # capture the local process ids of the services under diagnosis; replace <service-binary-name> with the actual process name observed in the environment.
ss -ltnp  # list local listening sockets to identify ports for each service in the request path; map each service to a port without exposing internal addresses in shared output.
grep -RIn 'propagator' <service-source-root>  # locate propagator registration in each service source tree; replace <service-source-root> with the resolved source path before running.
grep -RInE 'traceparent|tracestate|b3|x-request-id' <service-source-root>  # locate header names read or written by each service to confirm they match across hops.
grep -RInE 'set_parent|extract_context|inject_context|otel.context' <service-source-root>  # surface manual propagation call sites that may bypass configured propagators and should be reviewed.