Filter concepts by levelShowing all levels.

System Design · Observability

Observability

Logs, metrics and traces, the concrete signals to attach to every request, and tracing across sync and async hops

  • Three pillarslogs, metrics, traces — complementary, not substitutes
  • Correlation IDsrequest ID per hop, trace ID across every hop
  • Saturationthe earliest warning sign, before errors even rise
  • Cross-service tracingsync via headers, async via the message payload itself

1.Three pillars: logs, metrics and traces

Logs, metrics and traces are three different shapes of data about the same running system, and each answers a question the other two cannot answer well. A log is a discrete, timestamped record of one event — rich in detail about exactly what happened, but expensive to store and slow to query across a whole fleet at scale. A metric is a numeric measurement aggregated over time (a count, a rate, a gauge, a histogram bucket) — cheap to store and fast to query, but it tells you something is wrong without telling you which request or which user was affected. A trace follows one request as it moves across multiple services, recording how long each hop took and how the hops relate to each other — it answers "where did this specific request spend its time" in a way neither logs nor metrics alone can. None of the three replaces the other two; a mature observability setup carries all three and correlates them together.

Key features

  • Metrics are aggregated numeric time series (counters, gauges, histograms) — cheapest to store and query, but describe the system in general, not any one request
  • Logs are discrete timestamped event records — richest detail per event, but the most expensive to store and slowest to search at high volume
  • Traces follow a single request across service boundaries as a tree of timed spans, showing where that request spent its time
  • The three pillars are complementary, not substitutable — metrics tell you something is wrong, traces show you where, logs show you exactly what happened there
  • A request ID or trace ID is what lets you pivot from a metric spike to the specific traces and logs for the affected requests

What each pillar is actually good and bad at

What each pillar is actually good and bad at
PillarBest forWeak for
LogsExact detail of one event, ad hoc investigationAggregating patterns across millions of events cheaply
MetricsTrends, dashboards, alerting thresholds, cheap high-cardinality-free queriesExplaining why one specific request failed
TracesSeeing where time went across services for one requestAnswering "how often does this happen system-wide"
How the three pillars connect during one investigation
pivot viarequest/trace IDpivot to theslow span's logs

Metric dashboard

p99 latency spikes — something is wrong

Trace for a slow request

which service and span ate the time

Logs for that span

exact error message and context

  • Metric dashboard — p99 latency spikes — something is wrong
    • leads to Trace for a slow request (pivot via request/trace ID)
  • Trace for a slow request — which service and span ate the time
    • leads to Logs for that span (pivot to the slow span's logs)
  • Logs for that span — exact error message and context

Command

text
// The same incident, seen through each pillar

metric:  http_request_duration_seconds{route="/checkout"} p99 spikes to 4.2s
log:     2026-08-25T10:03:11Z ERROR checkout-svc req_id=abc123 "payment gateway timeout after 4000ms"
trace:   req_id=abc123
           checkout-svc     [====================] 4210ms
             payment-svc    [==================]   4180ms  <- almost all the time
               db-query     [==]                     220ms

Metrics detect that something is wrong system-wide and are cheap to watch continuously; traces localize the problem to a specific service and span for one request; logs explain the specific cause once localized. The three are complementary — a request/trace ID is the thread that lets you pivot from one pillar to the next during a real investigation.

2.Core signals: IDs, service names, latency, errors, saturation and dependencies

Turning raw logs and metrics into something actually useful for debugging means consistently attaching a specific set of signals to every request, everywhere. A request ID uniquely identifies one request as it is handled within a single service (or one hop), while a trace ID identifies that same logical request as it crosses multiple services — both must be generated (or accepted from an inbound header) at the edge and propagated on every downstream call, or the correlation breaks. The service name on every log line and metric is what lets you tell which of possibly hundreds of services actually emitted a given signal. Latency and error counts are the two most basic health signals for any single service. Saturation (how full a resource is — CPU, memory, connection pool, queue depth) is the leading indicator that a service is about to start failing, often before error counts rise at all. Dependency metrics — the latency and error rate of the services or datastores a given service calls — are what let you tell whether a service's own problems are self-inflicted or caused by something it depends on.

Key features

  • A request ID identifies one request within a single service/hop; a trace ID identifies the same logical request across every service it crosses — they solve different scopes of correlation
  • IDs only work as correlation keys if propagated on every downstream call — a service that generates its own ID instead of forwarding the inbound one breaks the chain
  • Saturation (resource fullness — CPU, memory, connection pool, queue depth) is often the earliest warning sign of trouble, rising before error counts or latency visibly degrade
  • Latency and error counts are per-service; dependency metrics are what a service records about the things it calls, distinguishing "I am slow" from "something I depend on is slow"
  • Service name on every log line and metric is what makes any of the other signals attributable to a specific piece of a multi-service system

The seven signals and what each one is for

The seven signals and what each one is for
SignalScopeWhat it answers
Request IDOne request, one service/hop"Which log lines belong to this exact request here?"
Trace IDOne request, across all services"Which spans/logs across the whole system belong to this request?"
Service nameEvery signal emitted"Which service produced this signal?"
LatencyPer service (or per endpoint)"How long is this service taking to respond?"
Error countsPer service (or per endpoint)"How often is this service failing?"
SaturationPer resource (CPU, memory, pool, queue)"How close is this service to running out of capacity?"
Dependency metricsPer downstream call"Is this service's problem its own, or a dependency's?"
How one trace ID threads through service names and their dependency calls
trace_idpropagatedtrace_id propagated; recordedas a dependency metric

API gateway

mints trace_id=abc123

checkout-svc

latency 4210ms, error

payment-svc

dependency: 4180ms, timeout

  • API gateway — mints trace_id=abc123
    • leads to checkout-svc (trace_id propagated)
  • checkout-svc — latency 4210ms, error
    • leads to payment-svc (trace_id propagated; recorded as a dependency metric)
  • payment-svc — dependency: 4180ms, timeout

Command

text
// Structured log line carrying every signal at once
{
  "ts": "2026-08-25T10:03:11Z",
  "service": "checkout-svc",
  "trace_id": "abc123",
  "request_id": "req-9f2e",
  "latency_ms": 4210,
  "status": "error",
  "dependency": {
    "name": "payment-svc",
    "latency_ms": 4180,
    "status": "timeout"
  }
}
// Saturation is usually a separate metric, not a log line:
pool_connections_in_use / pool_connections_max  // e.g. 98/100 -> near saturation

Request IDs correlate within one hop, trace IDs correlate across every hop — both only work if propagated on every downstream call rather than regenerated per service. Latency and error counts describe a service itself; saturation is the earliest warning sign of trouble, often rising well before either does; dependency metrics tell you whether a problem is a service's own or something it calls.

3.Tracing cross-service requests and asynchronous workflows

A distributed trace represents one request as a tree of spans, where each span is one unit of work (an HTTP call, a database query, a function) with a start time, a duration, and a parent-child relationship to the span that caused it. For synchronous cross-service requests, the trace context (trace ID plus the current span ID, so the callee knows whose child it is) rides along as HTTP headers on the call itself — this is what lets a single trace stitch together spans from many different services into one coherent tree. Asynchronous workflows break that mechanism: when a message is published to a queue and processed later by a consumer, there is no in-flight HTTP call to carry headers on, so the trace context has to be explicitly serialized into the message itself (as message attributes or a header field within the message payload) and read back out by the consumer to continue the same trace rather than start a disconnected new one.

Key features

  • A trace is a tree of spans; each span records one unit of work's duration and which span caused it (its parent), and cross-service spans are stitched together by a shared trace ID
  • Synchronous calls propagate trace context via request headers (e.g. the W3C Trace Context standard's traceparent header) riding along on the call itself
  • Asynchronous workflows have no in-flight call to carry headers on — the trace context must be explicitly serialized into the message/event payload and read back out by the consumer
  • A consumer that does not extract trace context from the message starts a disconnected new trace, breaking the ability to see the full workflow as one thing
  • Fan-out (one message triggering several consumers, or a scatter-gather across services) produces multiple child spans under one parent, all still part of a single trace

How trace context travels, by call style

How trace context travels, by call style
Call styleHow context travelsCommon failure mode
Synchronous HTTP/RPCHeaders on the request itself (e.g. traceparent)A hop that strips unknown headers silently breaks the chain
Message queue / eventSerialized into message attributes or payloadConsumer ignores the field and starts a fresh, disconnected trace
Scheduled/batch jobOften none — job runs independently of any requestBatch work is invisible in tracing entirely unless deliberately given its own root span
One trace spanning a synchronous call and an asynchronous handoff
publish (contextin payload)consume (contextextracted)

order-api

root span; trace_id=abc123

message queue

trace_context serialized into message

fulfillment-worker

extracts trace_context; child span of order-api

  • order-api — root span; trace_id=abc123
    • leads to message queue (publish (context in payload))
  • message queue — trace_context serialized into message
    • leads to fulfillment-worker (consume (context extracted))
  • fulfillment-worker — extracts trace_context; child span of order-api

Command

text
// Synchronous: trace context rides HTTP headers
GET /charge HTTP/1.1
traceparent: 00-abc123...-span456...-01

// Asynchronous: trace context is carried inside
// the message itself, since there is no live call
{
  "event": "order.placed",
  "trace_context": {
    "trace_id": "abc123...",
    "span_id": "span789..."
  },
  "payload": { "order_id": 42 }
}
// consumer extracts trace_context and starts its
// span as a CHILD of span789, not a new root

Synchronous cross-service calls propagate trace context via request headers; asynchronous workflows have no live call to ride on, so the context must be explicitly serialized into the message and extracted by the consumer as a child span, not a fresh trace. A trace spans wall-clock time and queue hops just as validly as it spans a single synchronous call chain — skipping context propagation on any hop, sync or async, silently breaks that one trace into disconnected pieces.

Remember

  • Metrics detect that something is wrong system-wide; traces localize it to a specific service and span; logs explain the specific cause once localized.
  • A trace ID only correlates a request across services if every hop propagates the inbound one instead of minting its own.
  • Saturation (resource fullness) is usually the earliest warning sign of trouble — often rising well before error counts or latency visibly degrade.
  • Asynchronous workflows have no live call to carry trace context on — it must be serialized into the message and extracted by the consumer, or the workflow becomes an invisible, disconnected trace.

Pro tip

  • Record dependency metrics (latency and error rate of what a service calls) on the calling service's own side — it is what separates "I am slow" from "something I depend on is slow" without extra guesswork.
  • Instrument asynchronous consumers deliberately — trace context propagation across HTTP is close to automatic, but across a queue it always requires explicit serialize/extract code on both ends.

Next up

  • Monitoring and alerting
  • Incident response
  • Chaos engineering

Sources

  • OpenTelemetry signals docs — opentelemetry.io/docs
  • Google SRE book, Monitoring Distributed Systems — sre.google
  • W3C Trace Context — w3.org/TR/trace-context
  • Checked 2026-08-25

Spotted a mistake on this page? Tell us.