Observability · beginner
Observability-correlation checklist
A beginner-friendly checklist for verifying that logs, metrics, and traces emitted by a service share the same correlation identifiers and can be joined back to a single request or transaction. The guide frames correlation as the discipline of propagating identifiers end-to-end so an engineer can pivot from a trace span to its log lines and metric points without manual stitching.
The symptoms
- •Logs from a single user request do not share a common identifier with the corresponding trace spans, so the request cannot be reconstructed across signals.
- •Metric points emitted during a request carry labels that do not match the trace_id or request_id present in logs, so dashboards and traces cannot be cross-referenced.
- •Distributed trace spans across multiple services are broken at a hop, leaving orphan spans with no parent context and no downstream activity.
- •Log search by request_id returns hits from one service but misses identical requests handled by peer services in the same call graph.
- •Alerting fires on a metric, but the on-call engineer cannot locate the originating log lines or trace for the time window of the alert.
Likely causes
- •Propagation context (W3C Trace Context header or equivalent) is not being injected at the service ingress or is being stripped by an intermediate proxy or library.
- •Application code emits log lines or metric points with locally generated identifiers that are never bound to the request context.
- •Resource attributes on spans, logs, and metrics are set independently per process, so the join key (service.name, service.version, deployment.environment) drifts between signals.
- •Asynchronous work such as queue consumers, scheduled jobs, or background workers do not capture the parent context at enqueue time and therefore cannot resume correlation on consumption.
- •Sampling decisions are applied to traces only, so correlated logs and metrics are emitted for requests that have no retained trace span to anchor them.
First ten minutes
- 01Confirm the failure mode: pick one recent request and verify independently that its logs, its metric points, and its trace span exist in their respective backends.
- 02Record the identifiers actually present in each signal for that single request and list which fields overlap and which do not.
- 03Identify the boundary where correlation breaks by repeating the join for two or three requests and noting whether the breakage is per-request, per-service, or per-signal-type.
- 04Inspect the request ingress for the presence of a propagation header and note whether downstream services see the same value or a freshly minted one.
- 05Capture the observed join key fields (trace_id, span_id, request_id, session_id) and the resource attributes attached to each signal before forming any hypothesis.
Evidence to collect
- •One concrete request with its trace_id, the log entries that reference that trace_id, and the metric points that share the trace_id or a derived label.
- •The propagation header value at the ingress edge and the same header value observed at each downstream service boundary.
- •The list of resource attributes (service.name, service.version, deployment.environment, host.name) attached to a sampled span, a sampled log record, and a sampled metric data point.
- •A counter of requests that produced a trace span versus requests that produced log lines during the same time window, to surface sampling-induced gaps.
- •The list of asynchronous boundaries (queues, schedulers, thread pools) touched by a traced request and whether context was captured at each boundary.
Where to look
- •At the service ingress boundary: the HTTP or RPC handler that first receives a request, where a propagation header should be parsed and a context object created.
- •At each downstream service boundary: outgoing client calls and incoming server handlers, where context must be injected and extracted respectively.
- •At asynchronous boundaries: queue producers, queue consumers, scheduled task runners, and thread executor submission sites, where context typically has to be captured manually.
- •At the instrumentation layer: the OpenTelemetry SDK configuration for the log, metric, and trace exporters, where resource attributes and propagators are bound.
- •At the backend join surface: the tracing UI search field, the log search field, and the metric label explorer, to confirm each backend can accept the same identifier.
Diagnostic steps
- 01Form the join hypothesis: for one known request, list the fields each signal must share. If any field is absent from any signal, that signal is the suspect.
- 02Test propagation continuity by tracing one request across three hops and recording the propagation header value at each hop; mismatch indicates a strip or rewrite.
- 03Test resource attribute parity by exporting one span, one log, and one metric from the same process and comparing the attached attributes for drift in service.name or service.version.
- 04Test asynchronous correlation by enqueuing a job inside a traced request, consuming it, and checking whether the consumer's span is parented to the producer's context.
- 05Test sampling alignment by checking whether the sampled trace rate approximately matches the rate of requests that emit correlated logs and metrics; large divergence implies sampling is decoupled.
- 06Test log-to-trace linkage by querying the log backend with a trace_id and confirming the result set includes the expected span, then performing the inverse query from the trace UI.
Common mistakes
- •Assuming correlation works because traces appear healthy; traces can be intact while logs and metrics carry unrelated identifiers.
- •Searching the log backend with a field name (request_id) that the application does not emit, then concluding correlation is broken when the field was never set.
- •Confusing high-cardinality user identifiers (user_id, session_id) with propagation identifiers (trace_id, span_id); they serve different join purposes and must not be conflated.
- •Configuring resource attributes only on the trace exporter and forgetting the metric and log exporters, which causes drift across signals.
- •Reading a sampled trace as evidence that all requests are correlated; sampling hides the gaps where correlation actually fails.
Safe fixes
- •If a propagation header is missing at ingress, configure the inbound instrumentation to parse the W3C Trace Context header and bind it to the request context before any downstream call.
- •If resource attributes drift between signals, centralize the attribute set in the OpenTelemetry SDK so the trace, log, and metric exporters share the same resource definition.
- •If asynchronous work breaks correlation, capture the current context at enqueue time and re-activate it inside the consumer before any log or span is emitted.
- •If logs do not carry trace_id, configure the log appender or bridge to read the active span context and inject trace_id and span_id into each log record.
- •If metric points lack the join key, add exemplars that bind a metric bucket to a recent trace_id, so a dashboard data point can be drilled into its trace.
Prove the fix
- 01Replay or capture one request end-to-end and confirm that a single trace_id appears in the trace backend, in the log backend search results, and as an exemplar on at least one metric data point.
- 02Confirm propagation continuity: the propagation header value at the ingress equals the value observed at every downstream hop for the same request.
- 03Confirm resource parity: the attributes service.name and service.version attached to one span, one log, and one metric from the same process are byte-identical.
- 04Confirm async correlation: an enqueued job produces a child span whose parent span_id matches the span that enqueued it.
- 05Confirm regression coverage: the join succeeds for ten consecutive sampled requests, and a log-to-trace pivot from a real log line returns the expected trace within the expected latency.
Prevention and next steps
- •Treat correlation identifiers as part of the service contract: define the set of join fields (trace_id, request_id, service.name, deployment.environment) and enforce them in code review and SDK configuration.
- •Add a synthetic check that emits one correlated log, metric, and trace per build, and fail the build if any signal lacks the agreed join fields.
- •Audit every asynchronous boundary during onboarding of new queues, schedulers, or worker pools to confirm context capture is wired before traffic is enabled.
- •Keep the OpenTelemetry propagator list explicit and reviewed whenever a new client or server library is added, so propagation is not silently disabled by a default.
- •Review resource attributes on every exporter after each release, and verify parity across trace, log, and metric exporters rather than per signal in isolation.
Safe commands and checks
grep -RIn "traceparent" <service_source_path> | head -n 50
grep -RIn "trace_id\|span_id\|request_id" <log_config_path> | head -n 50
grep -RIn "Resource(service\|service.name\|service.version" <otel_config_path> | head -n 50
grep -RIn "set_exporter\|add_exporter\|OTLPLogExporter\|OTLPMetricExporter\|OTLPSpanExporter" <otel_config_path> | head -n 50
grep -RIn "exemplar\|Exemplar" <metric_emission_path> | head -n 50
awk '/trace_id/{print $1}' <sampled_log_file> | sort -u | head -n 20
awk '/span_id/{print $1}' <sampled_log_file> | sort -u | head -n 20