System designCore12 min
Observability: metrics, logs & traces
When something breaks in production, you can't attach a debugger to a thousand servers. You have only what the system recorded about itself: numbers over time, a line for each event, and the path each request took. Recorded well, they turn a vague complaint into the one line of evidence that explains it.
1 · The problem
An average can look healthy while 1 in 50 bookings stalls
A hotel-booking site, Friday evening. Support starts getting messages: the Book button spins for several seconds. The dashboard looks fine. CPU is at 45%, the error rate is 0.2% as always, and the average response time is about 250 ms, well under its 500 ms alert. Here is what was really happening to booking requests:
- Slowest 1% (p99)
- Average
Modeled: most bookings take about 150 ms (log-normal, σ = 0.5, mean about 170 ms). From 21:30 a growing share, 2% by 21:50, take 4,340 ms. Average = 0.98 × 170 + 0.02 × 4,340 ≈ 253 ms. The p99 jumps to 4.3 seconds as soon as more than 1% of bookings are slow.
The payment provider had stopped answering from one of its endpoints. The payment service waited 2 seconds, retried, waited again, and succeeded on the third try. Nothing failed, so the error rate stayed flat; one booking in fifty took 4.3 seconds, which the average barely shows. Retries had turned an outage into slowness (Timeouts, retries, backoff & jitter).
Monitoring watches for problems you predicted: CPU too high, errors above 1%. Observability is the ability to answer questions you didn't predict, from the outside, using what the system records about itself. Both rest on three kinds of data, called signals:
Signal | What it is | What it answers | Cost |
|---|---|---|---|
Metrics | Numbers counted or measured over time | Is something wrong? How much, since when? | Low: fixed per series, whatever the traffic |
Logs | A timestamped record of one event | What exactly happened to this request? | Grows with traffic |
Traces | One request's path across services, with timings | Where did the time go? | Grows with traffic, so usually sampled |
2 · Metrics
Metrics are cheap because they throw the details away
A metric is a number recorded at regular intervals, such as every 15 seconds. It keeps no detail about individual requests, which is why it stays cheap at any traffic: a counter of a million bookings takes the same space as a counter of ten. Three types cover almost everything:
Type | What it records | Example on the booking site |
|---|---|---|
Counter | A total that only goes up (or resets to zero on restart); you graph its rate | Bookings made, payment calls failed |
Gauge | A value that goes up and down | Requests in progress, free disk space, queue length |
Histogram | How many observations fell in each range (bucket), plus their sum | Booking time: how many under 100 ms, under 250 ms, under 1 s… |
Histograms matter because percentiles can't be averaged. Averaging the p99s of 40 servers doesn't give the p99 of all 40, but bucket counts can be added up across servers and the p99 read off the total (Latency). Had the booking site kept a histogram, its p99 would have shown the problem at 21:40.
Which numbers to keep? Three short checklists:
- RED, for every service: rate of requests, errors, and duration (as a histogram).
- USE, for every resource such as a CPU, a disk or a connection pool: utilization, saturation (work waiting), and errors.
- The four golden signals combine them: latency, traffic, errors and saturation.
Each metric can carry labels, such as endpoint="/book" or status="500", and every distinct combination of label values is a separate time series stored on its own. That count is the metric's cardinality:
- 20 endpoints × 5 status classes × 100 servers = 10,000 series: fine.
- Add a
user_idlabel with 2 million users, and even if each user touched a single endpoint on a single server, that is 2 million more series, every one stored every 15 seconds.
Keep labels to small, fixed sets. Detail about individual users and requests belongs in logs and traces.
3 · Logs
Logs keep every detail, so they cost the most
A log is a timestamped record of one event: a booking accepted, a payment retried, an exception thrown. Written as free text, it is easy for a person to read and hard for a machine to search. A structured log writes the same event as named fields, so you can ask for every line where provider_endpoint is eu-2:
One structured log line from the payment service
{
"time": "2026-10-02T21:43:07.412Z",
"level": "WARN",
"service": "payments",
"message": "provider timeout, retrying",
"attempt": 1,
"timeout_ms": 2000,
"provider_endpoint": "eu-2",
"booking_id": "B-88213",
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736"
}The last field, trace_id, is what lets you find this line from a slow request later (see below).
Logs grow with traffic, and the bill grows with them. At 2,000 requests a second, five lines per request and 400 bytes per line:
Window | Volume |
|---|---|
Per second | 2,000 × 5 × 400 bytes = 4 MB |
Per day | 4 MB × 86,400 ≈ 346 GB |
Kept for 30 days | About 10.4 TB, all of it indexed for search |
So teams choose what to keep:
- Levels. Mark each line DEBUG, INFO, WARN or ERROR, and switch DEBUG off in production unless you are chasing a bug.
- Sample the routine, keep the unusual. One success line in a hundred is plenty; every error and every retry is worth keeping.
- Never log secrets. Passwords, tokens and card numbers end up in a log store that many people can read and that lives for months. Personal data needs a short, stated retention.
4 · Traces
A trace shows where one request's time went
A booking passes through the web tier, the booking service, the availability service, the payment service and the database. Each one only knows its own part. A trace records the whole path. It is made of spans: one span per unit of work, each with a name, a start time, a duration and the ID of the span that called it (its parent). The first span, with no parent, is the root span. Here is the trace of one slow booking:
The child spans run one after another: 40 + 2,000 + 2,000 + 200 + 60 = 4,300 ms, and the booking service's own work makes up the last 40 ms. Two payment attempts that each waited the full 2-second timeout are the whole story.
No metric would have said this so directly. The payment service's average call time was a little high; the trace shows that the slow bookings are exactly the ones that hit two timeouts in a row.
Tracing every request costs as much as logging it, so most systems sample:
- Head sampling decides at the start of the request, say keep 1 in 100. It is simple and cheap, but it decides before anyone knows whether the request will be interesting.
- Tail sampling waits until the trace is complete, then keeps every error, every slow request, and a small share of the rest. It needs a collector that holds all spans for a few seconds, which costs memory and effort.
Head sampling's blind spot is rare failures. A bug that hits 1 in 10,000 requests at 2,000 requests a second fails 0.2 times a second; at 1% sampling you keep one example every 500 seconds, about every 8 minutes. Tail sampling keeps all of them.
5 · Connecting them
One ID passed along with the request ties the signals together
How does the payment service know its spans belong to the same trace as the web tier's? The trace ID travels with the request. Each service reads it from the incoming call, records its own spans under it, and passes it on. This is context propagation. Over HTTP, the standard header is traceparent, for example 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01: a version, a 32-character trace ID, the 16-character ID of the calling span, and flags (the final 01 means this trace is being sampled).
Every span and every log line along the way carries trace 4bf9…, so the backend can assemble them into one picture. Try 2 is left out to keep the figure short.
If one service in the chain drops the header, say a queue consumer or an old library, the trace breaks into two unrelated halves. Propagation has to work at every hop, including through message queues. With the ID in place, the Friday incident took five steps:
- An alert fires
Bookings slower than 1 second are burning the latency objective's error budget about 20 times too fast (see the next section).
- Metrics narrow it
The p99 is up only on /book, and only on the payment service's outgoing calls.
- A trace shows where
One slow booking's trace: two charge attempts of exactly 2,000 ms each.
- Logs say why
Searching the logs for that trace ID finds "provider timeout, retrying" with provider_endpoint eu-2. Every timeout that evening names eu-2.
- Fix and confirm
Payments switch to another endpoint. The p99 falls back to about 500 ms, and the alert clears.
6 · Alerting
Page people for what users feel, not for every cause
A symptom is what users experience: bookings failing or slow. A cause is why: a full disk, a busy CPU, a slow provider. There are endless possible causes and only a few symptoms, so alerts that wake someone up should watch symptoms. Every page should be urgent, visible to users, and something a person can act on now.
What you see | Page someone? | Why |
|---|---|---|
Bookings failing or slow fast enough to threaten the objective | Yes | Users are affected now |
CPU at 90% on one server | No; dashboard or ticket | Users may not notice; the autoscaler may already be adding servers |
One of three database replicas down | Ticket | Redundancy is doing its job; fix it during working hours |
The symptom alert is best written against the service level objective (Availability & reliability). With an objective of 99.9% good bookings over 30 days, the error budget is 0.1% of bookings. The burn rate is how fast you spend it: an error rate of 0.1% is a burn rate of 1, which uses up exactly the budget in 30 days. A burn rate of 14.4 for one hour spends 14.4 × 1 ÷ 720 hours = 2% of the month's budget. A common starting point:
Burn rate | Sustained over | Budget spent | Action |
|---|---|---|---|
14.4 | 1 hour (and still in the last 5 minutes) | 2% | Page |
6 | 6 hours (and still in the last 30 minutes) | 5% | Page |
1 | 3 days (and still in the last 6 hours) | 10% | Ticket |
The short second window makes the alert stop soon after the problem does, instead of an hour later. On Friday, 2% slow bookings against a 0.1% budget is a burn rate of 20: the first rule pages within minutes, while a threshold on the average would never have fired.
7 · In practice
Instrument once, at every boundary, before you need it
Most teams use OpenTelemetry, an open, vendor-neutral standard and set of libraries for producing traces, metrics and logs. Its libraries instrument common web frameworks, database drivers and HTTP clients automatically, propagate trace context, and send the data to whichever backend you choose. You add your own spans and attributes where the business logic needs them.
Question during an incident | Signal to reach for |
|---|---|
Is something wrong, and since when? | Metrics: RED per service, against the objective |
Which service or dependency? | Metrics split by endpoint and dependency, then traces |
Where did this request's time go? | A trace |
Why did this request fail? | Logs found by its trace ID |
A checklist:
- Record RED for every service and USE for every resource, with latency as a histogram, never an average.
- Keep metric labels small and fixed; put user and request detail in logs and traces.
- Write structured logs with the trace ID in every line, sample the routine, keep every error, and never log secrets.
- Propagate trace context through every hop, including queues, and prefer tail sampling if you can afford it.
- Alert on symptoms, with burn-rate alerts on your objectives, and send cause-level warnings to dashboards and tickets.
- Instrument before the incident: the data you didn't record is the data you will need.
Check yourself
5 questions
Takeaways
Remember this
- Metrics say something is wrong, traces say where, and logs say why; metrics stay cheap at any traffic, while logs and traces grow with it.
- Record latency as a histogram: an average can stay flat while 1 in 50 requests stalls.
- Every distinct label combination is a separate time series, so keep metric labels small and put per-user detail in logs and traces.
- A trace ID propagated through every hop, and written into every log line, ties the three signals together.
- Page on symptoms users feel, with burn-rate alerts on your objectives; send causes to dashboards and tickets.