4.5.22 · D4Software Engineering

Exercises — Logging and monitoring — structured logging, metrics, alerting

3,592 words16 min readBack to topic

Everything below builds only on the parent note. Where a symbol appears, we re-earn it.


Level 1 — Recognition

Goal: can you name the pillar, the metric type, the level?

L1.1 — Which pillar?

A customer complains "my payment at 14:03 failed." You want to reconstruct the exact single event — which IP, which amount, which error code. Which of the three pillars (logging / metrics / alerting) do you reach for, and why?

Recall Solution

Answer: Logging.

  • A log is a discrete record of one event with high detail (per-event fields).
  • A metric throws away per-event detail and keeps only aggregate numbers over time — it literally cannot tell you this one payment's IP.
  • Alerting is a decision layer ("wake a human"), not a store of event detail.

The tell-tale words are "exact single event." Logs are the only pillar that preserves individual events.

L1.2 — Counter, Gauge, Histogram, or Summary?

Classify each: (a) total HTTP requests served since boot (b) current number of connections open right now (c) the spread of request durations so we can read p95 on the server, aggregated across many machines (d) the spread of request durations where each single instance must report its own p95 with no central math

Recall Solution
  • (a) Counter — it only ever goes up (or resets to 0 on restart). "Total ... since boot" is the signature.
  • (b) Gauge — "right now" and it can rise and fall as connections open/close.
  • (c) Histogram — it ships bucket counts; percentiles are computed later, on the server, and — crucially — buckets from many instances can be summed before you read a percentile. Use this when you aggregate.
  • (d) Summary — a fourth type (e.g. a Prometheus Summary) where each process computes its own quantiles (p50/p95/…) locally and exports them as ready-made numbers. Cheap to read, but you cannot meaningfully average quantiles across instances — so it's for per-instance views only.

Rule of thumb: only up → counter; up and down → gauge; shape you'll aggregate later → histogram; quantiles pre-computed per instance → summary.

Histogram vs Summary, in one line: histogram = "here are my raw buckets, you do the percentile math (and you can add my buckets to others')"; summary = "here is my p95, already cooked (but don't average it with someone else's)."


Level 2 — Application

Goal: plug numbers into the parent's formulas correctly.

L2.1 — Rate from a counter (with resets)

A request counter reads at and at . What is the requests-per-second rate over that window? Then: at the next scrape the process restarted and the counter reads at . What rate should the query report, and why can't you use the raw difference?

Recall Solution

Normal case. is the counter value; is the elapsed seconds. Each increment is one event, so the difference is exactly the number of events; dividing by seconds gives events per second. The huge absolute value cancels — only the change matters.

Reset case. Naively — a negative rate, which is nonsense (a counter can never truly go down). The negative sign is the signal that a reset happened (process restart, or the counter wrapped past its storage limit). Two standard fixes:

  • Reset handling (what Prometheus does): if , assume it dropped to and climbed back, so the events since the reset are just itself:
  • Wrap-around (fixed-width counters): if the counter is a -bit integer that overflowed at , recover the true delta with modulo arithmetic: .

The robust rule: compute ; if , treat it as a reset (use ) or as a wrap (add ) rather than reporting a negative rate. A monitoring system that skips this shows scary downward spikes on every deploy.

L2.2 — Compute p95 (and which convention?)

Sorted latencies (ms): [8, 11, 14, 19, 23, 27, 40, 66, 95, 210], so . Find (median) and using the nearest-rank rule.

Recall Solution

The nearest-rank rule: sort ascending, then the -th percentile sits at rank , and (the -th smallest). means "round up."

p50: → the 5th value = 23 ms. p95: → the 10th value = 210 ms.

Figure — Logging and monitoring — structured logging, metrics, alerting
What this picture teaches: the bars are the sorted samples left-to-right; the highlighted mint bar is the p50 rank and the coral bar is the p95 rank. The only thing a percentile does is point at a rank in this sorted row — read the label above the highlighted bar to get the answer. There is no averaging or curve-fitting in the nearest-rank rule.

Why round up: we need at least of samples at or below the answer. With and , exactly 9 samples is only — not enough — so we step to rank 10.

L2.3 — Error budget as a count

An SLO says of requests must succeed. In a day you serve requests. How many failed requests does the budget allow?

Recall Solution

The error budget is the fraction allowed to fail, , times the total. Why: SLO is the fraction that must succeed; whatever's left, , is your spendable failure allowance.


Level 3 — Analysis

Goal: reason about why an approach fails, and read numbers critically.

L3.1 — The average lies

Out of 1000 requests, 990 take ms and 10 take ms. Compute the mean latency and the p99. Then compute p99.1 and p99.9 to see where the pain finally appears. Which number would you show on a dashboard, and why?

Recall Solution

Non-integer percentiles use the same rule. The rank formula works for any — integer or not. You just plug the decimal in:

Mean = total time ÷ count: Percentiles. The sorted list is 990 copies of then 10 copies of (ranks 991–1000 are the slow ones):

  • : rank → the 990th value = 10 ms (last fast sample).
  • : rank → the 991st value = 5000 ms (first slow sample).
  • : rank 5000 ms.

Figure — Logging and monitoring — structured logging, metrics, alerting
What this picture teaches: there is no "typical" request — the population is two spikes, a tall mint bar (990 fast requests) and a tiny coral bar (10 slow ones). The lavender arrow shows the mean being dragged away from both spikes into empty space at ms: it names a latency no user ever experienced. The yellow box lists where each percentile lands, so you can see that p99 still sits on the fast spike and only p99.9 reaches the coral tail.

Reading it: the mean is a fiction. The p99 sits right on the cliff edge — one rank later everything changes. To see the pain you need p99.9 ( ms). General rule: your percentile must be deeper than your bad-request fraction. Here are slow, so anything up to p99 can look clean. See Percentiles and Distributions.

L3.2 — Why "for 5 minutes"?

A metric is scraped every s. Random noise makes it cross the alert threshold on about of individual scrapes even when nothing is wrong. If we page on a single crossing, roughly how many false pages per day? If we require the condition to hold for 10 consecutive scrapes (assume independence), how many?

Recall Solution

Scrapes per day: scrapes.

Single crossing: expected false pages per day. Unusable — pure alert fatigue.

10 consecutive (independence): probability a given window of 10 is all-noise . That's about per window — effectively zero false pages ever.

Why the "for" clause is magic: independent noise crossings almost never line up 10 in a row, but a real sustained problem trivially does. You trade minutes of detection delay for wiping out ~144 false pages/day. This is exactly the parent's "for 5 minutes" idea. See Incident Response.


Level 4 — Synthesis

Goal: combine formulas and design decisions.

L4.1 — Downtime budget across nines

For a 30-day month ( minutes), compute the allowed downtime for SLOs of , , and . Comment on the pattern.

Recall Solution

Allowed downtime .

  • : min h
  • : min
  • : min

Pattern: each added "nine" divides allowed downtime by 10. "Two nines" = 7.2 hours of slack (easy); "four nines" = 4.32 minutes (you likely can't even deploy safely inside that). Each nine is roughly the engineering cost. See SLI SLO SLA.

L4.2 — Burn rate: is this incident page-worthy?

Monthly error budget failed requests (from L2.3). Right now you are failing at requests per hour. The month is 30 days hours. Compute the burn rate (how many times faster than "even" you are spending the budget) and decide whether to page.

Recall Solution

"Even" burn spends the whole budget across the window: failures/hour is the budgeted rate. Interpretation: you are burning the monthly budget times too fast. Time to exhaust it: hours. Page immediately — the whole month's tolerance is gone before lunch. Why burn rate, not raw count: it converts "700/hr" into "how long until we've spent everything," which is directly actionable and self-scaling to any window.


Level 5 — Mastery

Goal: design an end-to-end policy and defend every choice.

L5.1 — Design a two-tier burn-rate alert

You have a SLO over 30 days. You want:

  • a fast alert that pages when the budget would be exhausted in ~2 hours (real emergency), and
  • a slow alert that pages when it would be exhausted in ~3 days (creeping degradation).

Give the burn-rate multiplier for each tier and a concrete for duration for each. (A burn rate of exhausts the budget in exactly the full 30-day window.)

Recall Solution

If burn rate means "spending the even pace," then time-to-exhaust . Solve for : Window days h.

  • Fast (2 h): . for: 2m — a 2-minute confirmation window. At you're spending the whole month in 2 hours, so you cannot afford to wait long; 2 minutes still filters single noisy scrapes (see L3.2) without meaningful delay. Page the on-call now, high urgency.
  • Slow (3 days h): . for: 1h — a 1-hour confirmation window. A creep isn't an emergency, so we can wait an hour to be sure it's sustained and not a transient bump; this kills false pages from short spikes. Ticket / working-hours page, not a wake-up.

Design defense:

  • The two for durations are chosen from the response time each tier allows: the fast tier's budget dies in 2 h, so waiting more than a couple of minutes wastes precious response time; the slow tier's budget dies in 3 days, so a 1 h wait is negligible but hugely reduces noise.
  • Two tiers avoid the parent's alert fatigue: tiny blips never sustain for a full hour, so they page no one.
  • Both alert on a symptom (budget burn = user-visible failures), never on internal causes.
  • This is exactly a symptom-based, budget-driven policy from the parent, made concrete.

L5.2 — The logging half of the same incident

The fast alert fires. You have the burn-rate metric but must now find the cause. Explain, using structured logging concepts from the parent, the shortest path from "alert fired at 14:03" to "root cause," name one privacy pitfall, and one query-performance pitfall.

Recall Solution

First, two terms defined:

  • A correlation ID (a.k.a. request ID) is a unique string generated once per incoming request — e.g. a random UUID like a1b2-... — and then attached as a field to every log line that request produces, in every service it touches. In practice the first service mints it and passes it downstream in an HTTP header (e.g. X-Request-Id / W3C traceparent); each service reads that header and includes it in its structured logs.
  • A trace is the assembled story: all the log lines/spans that share one correlation ID, stitched in time order across services, so you see the request's full journey. Building traces automatically is Distributed Tracing.

Path:

  1. Metrics told you that it's bad (symptom) — they can't tell you why (they discard per-event detail).
  2. Filter structured logs for the failing events: event=payment_failed AND level=error around 14:03.
  3. Group by an error field to spot the dominant failure, then pull one failing event's correlation ID.
  4. Query all lines carrying that ID to reconstruct the trace — the one broken hop is your root cause.

Privacy pitfall: in step 2 you may be tempted to log full card numbers / passwords to "have everything." That logs PII into a searchable store — a compliance breach. Log a reference (last 4 digits, a hashed user id), never the secret. See PII and Data Privacy.

Query-performance pitfall (cardinality): cardinality is the number of distinct values a field can take. Indexing a high-cardinality field (a raw user_id, an email, a full URL with query string) makes the log store build an enormous index — queries and ingestion slow to a crawl or the index blows your memory budget. The correlation ID is itself high-cardinality, so index it as a lookup key but don't turn it into a metric label (labels multiply time series — one series per distinct value — and can crush a time-series database). Keep metric labels low-cardinality (status code, region); keep high-cardinality identifiers in logs, retrieved by exact-match lookup.


Recall Self-test: fill the gaps

A counter stores values that only go ::: up (rate is derived as its slope) If the counter delta is negative it means ::: a reset or wrap-around happened — handle it, don't report a negative rate The nearest-rank p-th percentile rank is ::: The metric type that pre-computes quantiles per instance is a ::: Summary Error budget equals ::: Burn rate to exhaust a window in time is ::: You alert on symptoms, not ::: causes The field that stitches one request across services is the ::: correlation / trace ID A field with many distinct values (bad as a metric label) is ::: high-cardinality