Chapter 1
Why Observability — The Problem Three Pillars Solve
The problem
A service is running in production. A user says "it's slow." You have SSH access and top. Now what?
Without instrumentation, you're guessing:
- Requests/sec right now — is that normal?
- Slow for everyone, or just the unlucky P99?
- All endpoints, or one?
- Started 5 minutes ago, or degrading for a day?
- CPU, memory, a downstream dependency, or lock contention?
Observability is the property of a system that lets you answer these from the outside, without shipping a new build to add a printf. This series builds that property in Go, from zero, against go-observability-lab — a lab stack you can run locally with docker compose up.
Three pillars, three different questions
| Pillar | What it is | Answers | Tool in this series |
|---|---|---|---|
| Metrics | Numeric time series, cheap, aggregatable | "How much / how fast" — rates, percentiles, trends, alerts | Prometheus + Grafana |
| Logs | Timestamped structured events | "What exactly happened, for this one request" | Loki |
| Traces | One request's path across services, timed per hop | "Where did the 800ms go" | Tempo |
They're complementary, not redundant:
metric → "P95 on /orders spiked at 14:32" (something's wrong, roughly where)
log → "order_id=4471 failed: product not found" (what exactly happened)
trace → db.orders.insert took 780ms of that 800ms (which hop, exactly)
A metric alone tells you to look. A log or trace tells you what you'll find.
RED and USE — what to actually watch
For anything request-driven (an HTTP API, a queue consumer), watch three signals — RED:
- Rate — requests/sec
- Errors — failed requests/sec, or as a ratio
- Duration — the latency distribution (P50/P95/P99), never the average
For a resource (CPU, a connection pool, disk) — its sibling, USE:
- Utilization — % time busy
- Saturation — how much work is queued beyond capacity
- Errors — resource-level errors (rejected connections, OOM kills)
RED is the "if you only build one dashboard" dashboard. USE is what you add once you're chasing a saturation problem RED can only hint at (Chapter 2 covers why an average latency hides exactly the request you care about).
Pull vs push — same app, opposite collection model
Metrics, logs, and traces don't just answer different questions — they get collected in opposite directions, and your application code doesn't know either one exists beyond emitting its own output:
| Pillar | Model | Mechanism |
|---|---|---|
| Metrics (Prometheus) | Pull | Prometheus scrapes your app's /metrics on an interval. The app never pushes anywhere. |
| Logs (Loki) | Push | Your app writes plain JSON to stdout. A collector (Grafana Alloy) tails it and pushes to Loki. |
| Traces (Tempo) | Push | Your app's OpenTelemetry SDK batches spans and pushes them over OTLP. |
This distinction matters the first time a dashboard shows up == 0 (Prometheus can't reach your app) versus an empty Loki panel (your app is fine, the collector isn't running) — same symptom, opposite side of the wire failed.
What this series builds
Seven chapters, each grounded in a real phase of the lab repo, with real bugs and real before/after numbers instead of toy examples:
- Why observability — this chapter
- Metrics — Counter/Gauge/Histogram, PromQL correctness, the cardinality trap
- Alerting — recording rules,
for:windows, Alertmanager routing - Logs — the canonical log line, Loki's label model, a live cardinality blowup
- Traces — spans, the goroutine trap, propagation across Kafka
- Correlation — exemplars, derived fields, one incident click-through
- Production reality — what changes at scale, and where the "three pillars" model is being challenged
Next: Chapter 2 — the four metric types, the one PromQL mistake almost everyone makes once, and the cardinality trap that recurs in every pillar from here on.