Lab 03 — Observability, SLOs & Alerting: Metrics, PromQL-lite, Error Budgets
Phase 28 · Lab 03 · Phase README · Warmup
The problem
Every Kubernetes platform ships the same observability stack — Prometheus scraping metrics, Grafana rendering dashboards, Alertmanager routing pages — and most engineers use it for years without ever being able to answer the three questions that separate an operator from an SRE:
- What does
rate()actually compute, and why is a raw counter useless without it? - How does
histogram_quantile()turn bucket counts into a p99 — and why is that number an estimate whose accuracy you chose when you chose your buckets? - Why does a good paging alert need a
for:duration and two burn-rate windows — and what exactly is an error budget, arithmetically?
This lab makes you build all three mechanisms, plus the alert state machine between them
(inactive → pending → firing → resolved), so that when a Grafana panel shows
histogram_quantile(0.99, sum by (le) (rate(http_request_duration_seconds_bucket[5m]))) you can
read it the way you read your own code — because you wrote each function in it.
What you build
| Piece | What it does | The lesson |
|---|---|---|
MetricsStore — counter/gauge/histogram | append-only series keyed by (name, labels) | a "metric" is a family of time series, one per label combination — and that's also how cardinality explodes |
rate() / increase() | counter delta over a trailing window | counters are cumulative precisely so scrapes can be missed; rate() recovers the signal |
histogram_quantile() | p50/p95/p99 by linear interpolation over buckets | quantiles are bucket-resolution-limited estimates, not measurements |
sum_by() | aggregation by label | the sum by (service) in every dashboard, demystified |
AlertRule / AlertManager | breach → pending → (for: held) → firing → resolved | the for: debounce is why a 10-second blip doesn't page a human |
sli / error_budget / burn_rate | good/total, 1 − target, error-rate ÷ budget | reliability as arithmetic instead of argument |
SLOMonitor.fast_burn_alert | multi-window burn-rate paging | long window = "is it real?", short window = "is it still happening?" — both, or you page wrong |
File map
| File | Role |
|---|---|
lab.py | your implementation (fill the # TODOs) |
solution.py | reference implementation + worked example (python solution.py) |
test_lab.py | 32 tests: store semantics, rate/quantile/aggregation, alert state machine, SLO math, burn-rate alerts, determinism |
requirements.txt | pytest only |
Run it
pytest test_lab.py -v # your lab.py (red until you implement)
LAB_MODULE=solution pytest test_lab.py -v # the reference (green)
python solution.py # worked example
Success criteria
- Counters are monotonic (negative increment = structured error) and store cumulative values; gauges take the latest write.
-
rate()over a known steady series returns the exact per-tick rate, and uses only the trailing window (a past burst outside the window doesn't leak in). -
histogram_quantile()interpolates linearly within the correct bucket, walks cumulative counts across buckets, and returns the largest finite bound for the +Inf overflow bucket. -
sum_by()groups series by a label's value and sums the latest samples. -
An alert breaching its threshold goes pending, fires only after the breach is
sustained for
for_ticks, and a transient spike shorter than that never fires. - A firing alert resolves when the expression recovers, and every transition is recorded in order.
-
sli/error_budget/burn_ratecompute the SRE arithmetic exactly (99.9% target + 99.5% SLI = 5× burn). - The fast-burn alert triggers when both windows exceed the factor, and stays quiet when the short window has recovered even though the long window still remembers the incident.
-
All 32 tests pass under both
labandsolution.
How this maps to the real stack
- The metric types are Prometheus' metric types. A counter is cumulative and monotonic so a
missed scrape loses resolution, not data —
rate()reconstructs per-second behavior from any two samples. A gauge is instantaneous. A histogram is_bucket{le="..."}counters plus_sumand_count; our per-series{"counts", "inf", "sum", "count"}dict is exactly that data layout. And a series being keyed by (name, sorted label pairs) is Prometheus' identity model — which is also why a high-cardinality label (user ID, pod UID, request ID) multiplies your series count and takes the TSDB down. Real Prometheus adds the TSDB (chunked, compressed storage), scraping over HTTP/metrics, and staleness handling; the query semantics are what you built. rate(),histogram_quantile(),sum by()are the three PromQL constructs that appear in virtually every production dashboard, usually composed:histogram_quantile(0.99, sum by (le) (rate(..._bucket[5m]))). Realrate()also handles counter resets (a restarted pod's counter starts back at 0 — Prometheus detects the drop and compensates); ours doesn't, which is a stated limit below. Realhistogram_quantileruns over rated buckets (quantile of the last 5m, not of all time); ours runs over the snapshot, same interpolation.- The alert state machine is Prometheus' alerting-rule lifecycle verbatim:
inactive→pending(expr true,for:not yet elapsed) →firing(sent to Alertmanager, which owns grouping, silences, inhibition, and routing to PagerDuty/Slack/Opsgenie) → resolved. Thefor:duration is the single most important field in a paging rule — without it you page on every scrape blip. - SLI/SLO/error budget/burn rate are Google SRE's definitions: SLI = good events / valid events; SLO = the target over a window (say 99.9% over 30 days); error budget = 1 − SLO; burn rate = how many times faster than "exactly spend the budget by window's end" you're erring. The multi-window, multi-burn-rate alert (long window ≥ factor AND short window ≥ factor, e.g. 14.4× over 1h AND 5m for a 30-day SLO) is the SRE Workbook's recommended paging setup: the long window gives confidence it's real, the short window guarantees it's current so you don't get paged for an incident that ended an hour ago — exactly the behavior the test suite proves.
- Grafana is the query client on top: every panel is a PromQL expression like the ones you built, rendered over time. The RED method (Rate, Errors, Duration — for services) and USE method (Utilization, Saturation, Errors — for resources) from the Warmup are dashboard design disciplines over these exact primitives.
Limits of the miniature. No scraping, no TSDB, no retention/compaction — the store is an
in-memory append-only list. rate() doesn't handle counter resets. histogram_quantile runs on
snapshot counts, not rated buckets, and there are no native/exponential histograms. The
AlertManager here evaluates and tracks state but doesn't group, silence, inhibit, or route
notifications (real Alertmanager's whole job). And logs and traces — the other two observability
legs — are covered by Phase 14's tracer and
the Warmup, not this lab.
Extensions (your own machine)
- Add counter-reset handling to
rate(): when a sample is lower than its predecessor, treat the drop as a reset (add the post-reset value instead of the negative delta) — then write the test that a pod restart mid-window doesn't produce a negative rate. - Add recording rules: a rule that evaluates an expression each tick and writes the result
back into the store as a new series (
job:http_errors:rate5m), then alert off the recorded series — the standard trick for making dashboards and alerts cheap. - Implement Alertmanager grouping: batch firing alerts by a label set within a time window so one incident = one notification, not fifty.
- Run the real thing:
prometheus --config.file=...+node_exporterlocally, write yourHighErrorRaterule with afor:clause, and watch the pending→firing transition in the UI. - Compute the full SRE Workbook alert table: for a 30-day 99.9% SLO, derive the burn-rate factors and window pairs for page-fast, page-slow, ticket-fast, ticket-slow.
Interview / resume signal
"Built the Prometheus mechanism set from scratch: a label-keyed time-series store with counter/gauge/histogram semantics, PromQL-style
rate()/histogram_quantile()(cumulative bucket walk with linear interpolation)/sum by()queries, the alerting-rule state machine withfor:-duration debounce and resolution, and Google-SRE SLO arithmetic — SLI, error budget, burn rate, and the multi-window fast-burn paging alert, all deterministic and test-proven."