system-design

The Three Pillars: Logs, Metrics, Traces

You can’t operate, debug, or secure what you can’t see. These three signals are how you see inside a running distributed system — and each answers a different question.

Prerequisites: Performance Metrics, The 8 Fallacies Time to read: ~20 minutes


The problem

Your system is a hundred services across a thousand machines. A user reports “it’s slow.” You have no direct way to look inside — you can’t SSH into a thousand boxes, and the problem is already over by the time you’d get there.

🚨 Observability is the property of being able to understand a system’s internal state from its external outputs. The distinction from monitoring is worth knowing:

You need observability because at scale, the failures you didn’t predict dominate — and you can’t predict every question you’ll need to ask.


The three pillars, and the question each answers

🚨 The key framing: each pillar answers a different question, and you need all three.

Pillar Question Shape
Metrics Is something wrong, and how bad? Numbers over time (aggregated)
Logs What exactly happened? Discrete events (detailed)
Traces Where, across services, did it happen? One request’s path through the system

The workflow ties them together: a metric alert tells you something’s wrong (error rate spiked); a trace shows you which service in the request path is failing; a log in that service tells you exactly why (the specific error, the stack trace, the parameters).

🎙️ “Metrics tell me something is wrong, traces tell me where, logs tell me why. That’s the debugging workflow, and it’s why you need all three, not one.”


Metrics

Numbers measured over time, aggregated. Cheap to store (a metric is a few numbers per interval), efficient to query, and ideal for dashboards and alerts.

http_requests_total{status="500", endpoint="/checkout"}  = 4,213
request_duration_seconds{quantile="0.99"}                = 0.340
db_connections_active                                    = 87

What to measure — the frameworks:

The Four Golden Signals (Google SRE) — if you instrument nothing else:

RED (for services): Rate, Errors, Duration. USE (for resources): Utilization, Saturation, Errors.

🚨 Metrics are aggregates, which is their strength and their limit. They tell you the error rate is 5% but not which requests failed or why — you can’t drill into an individual event. And 🚨 high-cardinality labels kill metrics systems (user_id as a label creates millions of series, as in time-series databases). Metrics are for bounded dimensions; individual events go in logs and traces.

Store percentiles correctly. 🚨 You can’t average percentiles across hosts — metrics systems store histograms and compute percentiles from the merged distribution. → Performance Metrics

Tools: Prometheus (pull-based, the standard), Grafana (dashboards), Datadog, CloudWatch.


Logs

Discrete, timestamped records of events. Detailed — the full context of what happened.

🚨 Structured logging is non-negotiable at scale. Unstructured text is unqueryable:

# ❌ Unstructured — you can't query this
[2026-07-22 10:00:03] Error processing order 4821 for user 42

# ✅ Structured (JSON) — queryable, filterable, aggregatable
{"ts": "2026-07-22T10:00:03Z", "level": "error", "msg": "order processing failed",
 "order_id": 4821, "user_id": 42, "trace_id": "7f3a...", "error": "payment_declined"}

With structured logs you can query “all errors for user 42” or “all payment_declined events in the last hour.” With text logs you’re grepping and hoping.

🚨 Every log line must carry the trace/correlation ID (trace_id above). This is what lets you find every log across every service for one request — the single most valuable thing about structured logging in a distributed system. → Distributed Tracing

The costs, which are real:

Tools: ELK/Elasticsearch (the inverted index again → Search), Loki, Splunk, CloudWatch Logs.


Traces

🚨 The pillar that makes distributed systems debuggable, and the one people underuse.

A trace follows a single request across every service it touches, showing where the time went.

Trace: checkout request (total: 340ms)
├─ api-gateway          [██] 12ms
├─ auth-service         [█] 8ms
├─ order-service        [████████████████████] 280ms   ← the problem is here
│  ├─ inventory-check   [███] 40ms
│  └─ payment-service   [████████████████] 230ms        ← specifically here
│     └─ stripe-api     [███████████████] 220ms          ← waiting on Stripe
└─ notification-service [█] 5ms (async)

A trace is made of spans — each span is one unit of work (one service call), with a start time, duration, and metadata. Spans link into a tree via a shared trace ID propagated through the whole request, and each span records its parent span.

🚨 Why this is transformative: without tracing, “checkout is slow” means reading logs from six services and manually correlating by time — hours of work. With tracing, you look at one waterfall and see that 220ms is spent waiting on Stripe. It turns distributed debugging from archaeology into reading a picture.

How trace context propagates: the trace ID travels in request headers (the W3C Trace Context standard, traceparent), so every service adds its span to the same trace. Every service must propagate it, or the trace breaks. → Distributed Tracing

Tools: Jaeger, Zipkin, Tempo, Datadog APM, and OpenTelemetry as the vendor-neutral standard for instrumentation.


How the pillars work together

The unified debugging workflow, which is the point of the chapter:

flowchart LR
    M[Metric alert:<br/>error rate 5%] --> T[Trace:<br/>which service?]
    T --> L[Log:<br/>exact error + context]
    L --> F[Fix]
  1. Metric fires an alert: checkout error rate jumped to 5%.
  2. Trace the failing requests: they’re all failing in payment-service, waiting on Stripe.
  3. Log in payment-service: stripe_timeout, Stripe is returning 503s.
  4. Root cause: Stripe outage. Fix: circuit-break Stripe, degrade gracefully.

🚨 The trace/correlation ID is the thread through all three. A metric points at a symptom, you find example traces, each trace carries an ID, and that ID pulls up every relevant log. Without the shared ID, the three pillars are three disconnected islands.


OpenTelemetry: the unifying standard

🚨 Worth knowing as the current direction. OpenTelemetry (OTel) is a vendor-neutral standard for generating and collecting all three signals with one instrumentation library and one wire format.

Historically you instrumented separately for metrics (Prometheus), logs (some logger), and traces (Jaeger) — and were locked into each vendor. OTel unifies instrumentation: instrument once, export to any backend (Datadog, Grafana, Honeycomb, whatever), and switch backends without re-instrumenting.

🎙️ “I’d instrument with OpenTelemetry so we’re not locked into a vendor — one instrumentation layer for metrics, logs, and traces, exportable to any backend.” Mentioning OTel is a good currency signal.


The costs of observability

⚖️ Observability isn’t free, and a mature answer acknowledges it:

🚨 The trade-off in one line: observability data can cost as much as the system it observes if unmanaged. Sample intelligently — full fidelity for errors and a sample of the rest.


In the real world


🚨 Interview traps


🎙️ Soundbites


🛠️ Try it

1. Instrument a service with all three. Add Prometheus metrics (request count, latency histogram), structured JSON logs with a trace ID, and OpenTelemetry tracing across two services. Then trigger a slow request and debug it using the three pillars in sequence — metric shows the latency, trace shows which service, log shows why. Doing the workflow once makes the “three pillars” framing concrete.

2. Generate and read a trace. Two services calling each other, instrumented with OTel + Jaeger. Make one slow, then look at the trace waterfall. Seeing the time-spent-per-service picture is the “aha” for why tracing matters.

3. Break your logs, then fix them. Log an error as plain text and try to answer “how many errors for user 42 today?” You can’t easily. Switch to structured JSON and answer it with a query.

4. Cause a cardinality explosion. Add user_id as a Prometheus label and generate traffic from many users. Watch the series count and memory climb. That’s why high cardinality belongs in logs, not metrics.


Check yourself

1. What question does each of the three pillars answer, and why do you need all three? **Metrics** answer "*is* something wrong, and how bad?" — aggregated numbers over time (error rate, latency, saturation) that are cheap to store and ideal for dashboards and alerts, but can't tell you about an individual event. **Logs** answer "*what exactly* happened?" — detailed, discrete event records with full context (the specific error, parameters, stack trace), but they're expensive and you can't easily see cross-service flow. **Traces** answer "*where*, across services, did it happen?" — following one request's path through the whole system to show where the time or failure occurred. You need all three because they operate at different resolutions: a metric alerts you that something's wrong, a trace localizes it to a service, and a log explains why. Any one alone leaves a gap — metrics without traces can't localize, traces without logs can't explain, logs without metrics can't alert.
2. Why is structured logging essential at scale? Because unstructured text logs are effectively unqueryable at scale. When you have terabytes of logs across hundreds of services and need to answer "show me all payment-declined errors for user 42 in the last hour," grepping free-text lines is slow, fragile (a format change breaks your patterns), and can't aggregate. Structured logs (JSON with typed fields) let you query, filter, and aggregate like a database: filter by `user_id`, count by `error` type, group by `service`. Critically, structured logs carry the trace/correlation ID as a field, which is what lets you retrieve every log across every service for a single request — impossible to do reliably by correlating timestamps in text. Structure turns logs from a haystack into a queryable dataset.
3. Why does distributed tracing make microservices debuggable in a way logs alone don't? Because it reconstructs the causal, cross-service path of a single request and shows where time was spent — automatically. Without tracing, debugging "checkout is slow" means pulling logs from every service the request touched and manually correlating them by timestamp and IDs to reconstruct the sequence and find the slow step, which takes hours and is error-prone. A trace does this for you: each service adds a span (its unit of work, with duration) to a shared trace identified by a propagated trace ID, and the spans form a waterfall you can read at a glance — you *see* that 220ms of a 340ms request was spent waiting on Stripe inside payment-service. It turns distributed debugging from manual log archaeology into reading a picture, which is why it's the pillar that specifically addresses the microservices "where did it go wrong?" problem.
4. Why is high cardinality a problem for metrics but fine for logs and traces? Because of how each is stored. Metrics systems store a separate time series for every unique combination of label values, and query performance and memory depend on the total series count. A high-cardinality label like `user_id` (millions of values) multiplies the series count into the millions or billions, exhausting memory and grinding queries to a halt — it's the most common way to take down a metrics backend. Logs and traces, by contrast, store individual events, so a unique `user_id` per event is just a field value, not a new series — exactly what they're designed for. So the rule is: metrics carry *bounded, low-cardinality* dimensions (status code, endpoint, region) for aggregation; high-cardinality identifiers (user ID, request ID, trace ID) go in logs and traces where you drill into individual events.
5. What ties the three pillars together into a debugging workflow? The trace/correlation ID, propagated through the entire request and recorded in all three signals. The workflow: a **metric** alert fires (error rate spiked) — telling you something is wrong but not what. You find example failing requests and pull their **traces**, which show which service in the request path is failing and where the time went. Each trace carries a trace ID, and because every **log** line in every service also records that same trace ID, you use it to retrieve exactly the logs for that request across all services — which contain the specific error and context that explain the root cause. Without the shared ID, the three pillars are disconnected: you'd have an alert, some traces, and a sea of logs with no reliable way to connect a specific metric anomaly to the specific traces and logs that explain it. The ID is the thread that makes them a system rather than three separate tools.

Further reading