Observability
0-day streakReview

Your progress

Level 1

0 XP100 XP to level 2
0day streak
0best streak
0topics started
0cards reviewed
    Review cards

    Progress is stored only in this browser. No account is required.

    Layer 4 · Full breakdown · 19 min · 5 figures

    Observability: logs, metrics and traces

    What logs, metrics and traces are each good and bad at, what they cost, how OpenTelemetry joins them with one trace ID, and how to alert on error-budget burn instead of causes, with a worked p99 investigation.

    Observability is how quickly you can answer a new question about production using data you already collect. That data arrives as three signals: metrics tell you something is wrong and by how much, traces show where one request spent its time, and logs record exactly what happened. A shared trace ID turns three separate tools into one investigation.

    Picture it first: a cricket match, three ways

    A T20 match is recorded three ways at once.

    • The scoreboard shows runs, wickets and the required rate. Tiny, always on, readable from the back row. It tells you the chase is in trouble, not why.
    • Ball-by-ball commentary records every delivery: “short and wide, edged to third man”. All the detail, 120-odd entries an innings, and nobody reads it for the score.
    • The ball-tracking replay follows one delivery from hand to bat, with speed and timing at each stage. It explains that ball perfectly and says nothing about the rest.

    They work together because of the ball number: “over 14.3” appears in the fall of wickets, the commentary and the replay.

    Map it back. The scoreboard is metrics, cheap aggregates for dashboards and alerts. The commentary is logs, one detailed record per event. The replay is a trace, one request’s path through every service. The ball number is the trace ID.

    Monitoring vs observability

    Monitoring watches for failure modes you already know about. Someone decided checkout’s p99 matters, built the dashboard and wrote the alert. It covers known unknowns: you know the question, not today’s answer.

    Observability is a property of the system and its telemetry: how well you can work out what is happening inside from what it emits. When something new breaks, can you ask “are the slow checkouts all on app version 8.2, paying through one bank?” and answer it from data you already have, without shipping new code? That is the bar the OpenTelemetry observability primer sets, and how you handle unknown unknowns.

    Two things make it possible: context on every record (route, region, version, customer tier), so you can slice a symptom along dimensions nobody pre-aggregated; and correlation, so you move from a metric to its traces to their logs instead of lining up timestamps by eye.

    Monitoring doesn’t go away: black-box probes from outside tell you users are hurting now; white-box telemetry from inside tells you why.

    One request, three signals

    One checkout request leaves three records: a log line from the payment service, a sample in checkout’s latency histogram, and a span in the trace. All three carry the same trace ID.Notice The metric bucket keeps the trace ID as an exemplar. That one field is what lets you jump from a graph to a real request.

    Each signal answers a different question about the same request. The metric says “one more checkout landed in the 2.5-second bucket”, among millions of others. The span says “2.01 of its 2.15 seconds were spent in payment”. The log line says why: payment waited 1.79 seconds for a database connection because its pool was full.

    Logs: the detailed record

    A log is a timestamped record of one event. Logs are best at detail: the error, the inputs, the branch taken, who asked for what. They are worst at aggregates (counting means scanning) and at cost, since volume grows with traffic times verbosity.

    Structured, with a trace ID on every line

    Payment failed for order 8812 after 3 retries is easy to write and painful to query. Emit structured records with a stable schema: the same field names and types in every service.

    import json, logging
    from opentelemetry import trace
    
    log = logging.getLogger("payment")
    
    def log_event(level: int, msg: str, **fields) -> None:
        ctx = trace.get_current_span().get_span_context()
        log.log(level, json.dumps({
            "level": logging.getLevelName(level),
            "service": "payment",
            "msg": msg,
            "trace_id": format(ctx.trace_id, "032x"),   # same ID as the trace
            "span_id": format(ctx.span_id, "016x"),
            **fields,
        }))
    
    log_event(logging.WARNING, "pool wait", wait_ms=1790, pool_max=5, in_use=5)

    With an OpenTelemetry SDK or auto-instrumentation active, your existing logging library’s records get the IDs stamped for you. The habits that matter:

    • One wide entry per unit of work. Amazon’s Builders’ Library describes one structured log entry per request or queue message, carrying its timers, counters and metadata. It is cheaper than five chatty lines, and it is all you need to read when that request fails.
    • Levels with meaning. ERROR: a request failed. WARN: degraded but handled (a retry, a fallback). INFO: lifecycle events. DEBUG: off in production, with a runtime switch to raise verbosity for one host or customer while you investigate.
    • IDs everywhere, including request IDs returned by services you called. Nothing sensitive: no card numbers, OTPs or tokens, and truncate caller input.
    • Rate-limit error spam, so a bug that throws on every request doesn’t write 5,000 stack traces a second.

    Volume is the bill

    A service at 5,000 requests a second writing five 500-byte lines per request produces 5,000 × 5 × 500 B = 12.5 MB/s, and 12.5 MB/s × 86,400 s ≈ 1.08 TB a day, about 32 TB a month before replication and indexing. The cost section shows how to cut that by more than 20×.

    Metrics: cheap numbers over time

    A metric is a number sampled over time, identified by a name and labels; each label combination is a separate time series. Metrics are best at alerts, dashboards, trends and SLOs: tiny, fast, and priced by series count, not traffic. They are worst at explaining one request and at high-cardinality dimensions like user IDs.

    Counters, gauges and histograms

    Prometheus, the widely used open-source metrics system, defines four types:

    • Counter: only goes up, resetting to zero on restart. Requests, errors, bytes sent. You read its rate() per second, never the raw value.
    • Gauge: goes up and down. In-flight requests, queue depth, pool connections in use. If it can go down, it’s a gauge.
    • Histogram: counts observations into buckets (at most 50 ms, at most 100 ms …) plus a sum and a count. You compute any percentile later, over any window, across any set of instances.
    • Summary: computes fixed quantiles such as p99 inside each process. Accurate per instance, but chosen at instrumentation time and impossible to combine across instances.

    Space buckets roughly exponentially (the SRE book suggests steps of about 3×: 10, 30, 100, 300 ms), and put a boundary exactly at your SLO threshold so “requests under 300 ms” is a count, not an interpolation. Name metrics with base units: http_request_duration_seconds, http_requests_total.

    Labels and the cardinality explosion

    Every label combination is its own series, with its own memory, index entries and storage, so series count is a product.

    Checkout’s latency histogram has 12 buckets. A classic Prometheus histogram stores 12 bucket series (including +Inf) plus _sum and _count: 14 series per label set. Label it by route (20 values), status class (5) and pod (50): 20 × 5 × 50 = 5,000 label sets × 14 = 70,000 series. Fine.

    Now someone adds user_id “to debug one customer”. If 2 million users check out in a day and each touched just one route on one pod, that is 2,000,000 × 14 ≈ 28 million series from one metric, 400 times more, most written once and never again. Prometheus’s guidance: keep most metrics below a cardinality of 10, investigate anything that could pass 100, and never label with unbounded values like user IDs or emails.

    Cardinality hides in innocent labels

    Raw URL paths (/orders/8812 instead of the route /orders/:id), error messages as label values, and pod names that change on every deploy all explode series counts just like user_id. Per-user questions belong on spans and log fields, which are built for high cardinality.

    Why you can’t average percentiles

    A percentile is a rank, not a sum, so it does not survive averaging. Ten pods each serve 100 requests a second. Nine have a p99 near 100 ms; pod 7 sits on a bad host and every request it serves takes about 1 second.

    Ten pods with equal traffic. Nine have a p99 near 100 ms; pod 7 answers every request in about 1 second. Averaging the ten p99 values reports 190 ms.Notice The gap between the two dashed lines: the averaged number is five times better than what one in ten users actually gets.

    The average of the ten p99s is (9 × 100 + 1,000) ÷ 10 = 190 ms. But pod 7 carries 10% of all requests, so the fleet’s slowest 1% are all on pod 7 and the true p99 is about 1 second. Averaging hid the incident. It can mislead the other way too: a canary taking 1% of traffic with a terrible p99 drags the average up while barely touching users.

    Histogram buckets add, so sum them across pods and compute the percentile once:

    # Wrong: averages each pod's precomputed p99 (a summary)
    avg(checkout_latency_seconds{quantile="0.99"})
    
    # Right: add every pod's bucket rates, then take the 99th percentile
    histogram_quantile(0.99,
      sum by (le) (rate(checkout_duration_seconds_bucket[5m])))

    The Prometheus docs call the first form statistically nonsensical. The same trap catches dashboards that average p99 across regions, or roll one-minute p99 points into an hourly figure. And the mean is no rescue: as the SRE book notes, a service averaging 100 ms can easily have 1% of requests taking 5 seconds.

    Traces: one request’s journey

    A trace is the tree of work done for one request. Each node is a span: one operation with a name, start time, duration, attributes, status and parent span ID. Traces are best at showing where time went across services, and worst at aggregate questions; keeping them all is expensive, so most are sampled away.

    Reading a waterfall

    The slow checkout from Fig. 1 as a trace waterfall. Each row is a span; children are indented under their parent and drawn on a shared time axis.Notice Payment’s real work is fast: 1.79 of its 2.01 seconds go to borrowing a database connection, while the query itself takes 9 ms.

    The root span covers the whole request; children nest inside parents. Shapes worth learning:

    • One long child: a slow dependency, or, as here, waiting for a scarce resource before the real work starts.
    • A staircase of many short, sequential children: chatty I/O, such as 200 queries in a loop.
    • The same call repeated: retries, often hiding a timeout.
    • A gap inside a parent with no child: the service’s own CPU work or uninstrumented code. Profiles, covered below, explain those.

    Context propagation: the traceparent header

    For spans from different services to join one trace, every hop passes the context along in the header defined by W3C Trace Context (a Recommendation since February 2020, and OpenTelemetry’s default):

    traceparent: 00-5a1e3c9b7f2d4e60a8c4b2f1d9e07c3a-c4f2a8e61b9d3075-01
    #            │  │                                │                └ trace-flags: 01 = sampled
    #            │  │                                └ parent-id: 8 bytes, 16 hex, the caller's span ID
    #            │  └ trace-id: 16 bytes, 32 hex, same for every span in the trace
    #            └ version: 00

    Fields are lowercase hex and all-zero IDs are invalid. Each service keeps the trace-id, parents its span on the incoming parent-id, and sends its own span ID onward. The only defined flag, sampled, is a recommendation to the receiver. A companion tracestate header carries vendor data, and neither may hold personal data.

    Libraries handle HTTP and gRPC automatically. Traces break at async hops (thread pools, cron jobs, message queues), where you must copy the context into message headers yourself or get orphan traces.

    Head vs tail sampling

    At 5,000 requests a second with 25 spans each, full tracing means 125,000 spans a second, nearly all boring. You sample.

    Head sampling decides at the root, usually by hashing the trace ID against a rate so every service agrees, and passes the verdict on in the sampled flag. Cheap and blind: it can’t know this request will fail. Google’s Dapper sampled 1 in 1,024 traces by default and found that enough for high-volume services; tracing every request added 16.3% to a web-search cluster’s average latency.

    Tail sampling decides after the trace finishes, in a collector: keep every error, every trace over 1 second, and 1% of the rest. You keep exactly the traces you want, but the collector buffers every span until it decides (30 seconds by default in OpenTelemetry’s tail-sampling processor), and every span of a trace must reach the same instance.

    Head sampling Tail sampling
    Decides At the root, before the work After the trace completes
    Can keep all errors and slow traces No, only by luck Yes, by rule
    Cost Nearly free; unsampled spans are cheap Buffer every span; route by trace ID; stateful tier
    Failure mode Rare bugs vanish at low rates Collector overload drops data
    Default use Protect the pipeline at high volume Choose which traces are worth storing
    Head vs tail sampling

    Many teams combine them: a generous head rate protects the pipeline, then tail rules choose. Anything dropped at the head never reaches the tail.

    Logs vs metrics vs traces

    Logs Metrics Traces
    Answers What exactly happened in this event? How much, how often, how fast, overall? Where did this request spend its time?
    Cost grows with Traffic × lines × bytes per line Number of series (label combinations) Traffic × spans per request × sample rate
    High-cardinality fields (user, order) Fine Dangerous: each value is a new series Fine, as span attributes
    Usual sampling Keep errors; sample routine successes None; already aggregated Head rate plus tail rules
    Typical retention Days hot, longer in cheap archive Months, downsampled Days to a couple of weeks
    Best at The “why”, audit, edge cases Alerts, dashboards, SLOs, trends Latency breakdown across services
    Worst at Aggregates; the bill Explaining one request Rare events that were sampled away
    The three signals side by side

    What to measure: golden signals, RED and USE

    Three checklists, each aimed at a different thing:

    Four golden signals RED USE
    From Google SRE book Tom Wilkie, 2015 Brendan Gregg
    Apply to Each user-facing system Every request-driven service Every resource, hardware or software
    Measure Latency, traffic, errors, saturation Rate, errors, duration Utilisation, saturation, errors
    Payment example p99 authorize time, payments/s, failure ratio, pool fullness Calls/s, 5xx ratio, duration histogram DB pool in use ÷ max, threads waiting, acquire timeouts

    RED says whether a service is healthy for its callers; USE says which resource is the bottleneck, and pools and queues count as resources. Three details catch teams out:

    • Measure failed-request latency separately. A 500 from a dead database can return in 2 ms, so mixing errors in makes latency look better while users fail. And a slow error is worse than a fast one.
    • Short windows for saturation. Gregg describes five-minute averages that never showed CPU above 80% while it hit 100% for seconds at a time.
    • Measure both ends of every call. If the client sees 2 s and the server 20 ms, the time went to the network, a queue or the client’s own pool.

    OpenTelemetry: instrument once, send anywhere

    OpenTelemetry (OTel), a CNCF project formed from OpenTracing and OpenCensus, standardises how telemetry is produced and shipped. It is not a backend.

    • API: what code calls to start a span or record a metric. Without an SDK the calls are no-ops, so libraries can instrument without picking a vendor for you.
    • SDK: installed by the application owner: samplers, batching, exporters, and resource attributes such as service.name, version and region.
    • Auto-instrumentation: wraps HTTP, gRPC, database and messaging clients, producing spans, metrics and propagation without code changes.
    • OTLP: one protocol for all signals, over gRPC (port 4317) or HTTP (4318).
    • Semantic conventions: shared names (http.route, http.response.status_code) so one query works across services and languages.
    • Collector: a vendor-neutral proxy that receives, processes and exports telemetry.
    A typical OpenTelemetry pipeline. Services speak OTLP to a per-node agent, which forwards to a pool of gateway collectors that sample, clean and route data to one store per signal.Notice The backends sit behind the collectors. Changing a store or a sampling rule means editing collector config, not redeploying every service.

    The agent runs beside the app, so the app offloads data locally and never blocks on a slow backend. The gateway tier does the heavy work, and because tail sampling needs a whole trace on one instance, agents route spans to gateways by trace ID.

    Joining the signals

    • Exemplars attach a sample measurement, with its trace and span ID, to a metric data point, so a heatmap’s 2.5-second bucket points at a real slow request.
    • Trace IDs in logs let you open a span’s logs, or a log line’s trace.
    • Shared resource attributes (service.name, version, region) make one filter work on all three stores.

    Alerting on symptoms, not causes

    The SRE book frames monitoring as what is broken (the symptom) and why (the cause). Page on symptoms users feel: checkout failing or slow. Causes such as CPU at 90% or a replica down belong on dashboards, unless definite and imminent, like “the disk fills in 4 hours”. Every page should be urgent, actionable and need judgement; robotic responses should be automated.

    Burn rate

    The cleanest symptom alert comes from your SLO (the theory lives in SLIs, SLOs and error budgets). With a 99.9% SLO over 30 days, the error budget is 0.1% of requests. Burn rate is the observed bad-event ratio divided by that budget: at 1 the budget lasts exactly 30 days, at 14.4 it lasts 720 h ÷ 14.4 = 50 h.

    Decide how much budget is worth a page in a given window, then solve: burn rate = budget fraction × 720 h ÷ window.

    Budget spent Long window Burn rate Bad-ratio threshold Short window Action
    2% 1 hour 0.02 × 720 ÷ 1 = 14.4 1.44% 5 minutes Page
    5% 6 hours 0.05 × 720 ÷ 6 = 6 0.6% 30 minutes Page
    10% 3 days 0.10 × 720 ÷ 72 = 1 0.1% 6 hours Ticket

    These are the starting values the SRE Workbook recommends. For a total outage, the 1-hour ratio crosses 1.44% after 0.0144 × 60 min ≈ 52 s.

    Multi-window, multi-burn-rate

    A long window alone resets slowly: after the fix, a 1-hour average stays high for up to an hour. So each condition also requires a short window, 1/12 of the long one, to be burning now.

    A 99.9% SLO and a burst of 6% errors from minute 10 to minute 30. The page condition needs both the 1-hour and the 5-minute bad ratios above 14.4 × 0.1% = 1.44%.Notice The two bars at the bottom: with the short window the page clears 3.8 minutes after errors stop; on the long window alone it would keep firing until minute 75.6.

    The arithmetic: at 6% errors the 1-hour ratio grows 0.1 points a minute and crosses 1.44% at 10 + 14.4 = 24.4 minutes. By then 6% × 14.4 min ≈ 0.86 minutes of full outage are spent, out of a monthly budget of 0.1% × 43,200 min = 43.2: exactly 2%. As Prometheus rules (the ratios are recording rules you define):

    - alert: CheckoutBudgetFastBurn
      expr: |
        (checkout:bad_ratio:rate1h > 14.4 * 0.001 and checkout:bad_ratio:rate5m > 14.4 * 0.001)
        or
        (checkout:bad_ratio:rate6h > 6 * 0.001 and checkout:bad_ratio:rate30m > 6 * 0.001)
      labels: { severity: page }
    - alert: CheckoutBudgetSlowBurn
      expr: checkout:bad_ratio:rate3d > 0.001 and checkout:bad_ratio:rate6h > 0.001
      labels: { severity: ticket }

    A latency SLO works identically: a request slower than the threshold counts as a bad event.

    Low-traffic services

    At 10 requests an hour, a 30-day month holds about 7,200 requests and a budget of 7.2 failures, so one failed request spends 1 ÷ 7.2 ≈ 13.9% of it and pages at once. The Workbook suggests synthetic traffic, grouping related services, or a looser SLO.

    Fewer, better pages

    Pages that fire too often get skimmed until a real one is missed. Suppress duplicates (a fast burn also trips the slower windows), link each alert to a runbook, and remove rules nobody acts on.

    Give every service the same dashboard: RED against the SLO on top, client-side RED per dependency below, USE for CPU, memory and pools at the bottom, and deploy markers on every graph.

    A worked investigation: checkout p99 doubles

    Checkout’s SLO: 99.9% of requests within 1.5 seconds. Its p99 is normally about 1 s.

    1. The page. At 19:57 the fast-burn alert fires. About 4% of checkouts exceed 1.5 s, a burn rate of 4% ÷ 0.1% = 40; the 1-hour ratio needs 1.44 ÷ 4 × 60 ≈ 22 minutes to cross 1.44%, so trouble began around 19:35.
    2. RED for checkout. Rate is normal for the hour. Errors are flat (the 3-second timeout to payment isn’t being hit). p99 is 2.1 s while checkout’s CPU sits at 45%: it is waiting on something.
    3. Dependencies. Checkout’s client-side histograms show cart, pricing and inventory flat, and payment’s p99 up from 300 ms to 2 s. Payment’s own dependencies are flat (Postgres 12 ms, the bank near 200 ms), and a deploy marker sits at 19:35: payment v2.14.
    4. Exemplar to trace. A dot in the heatmap’s 2.5-second bucket opens trace 5a1e…7c3a (Fig. 3): db.pool.acquire takes 1.79 s, the query 9 ms.
    5. Trace to logs. Payment’s logs for that trace ID hold the WARNING line from Fig. 1: pool_max=5, in_use=5. The pool used to hold 50; v2.14’s config changed it to 5.
    6. Fix and follow-up. Roll back; the 5-minute ratio drops and the page clears. Half an hour at 4% slow spent 4% × 30 min ÷ 43.2 min ≈ 2.8% of the month’s budget. Add pool wait time to payment’s dashboard, and gate config changes behind a canary that watches p99.

    Nobody had a dashboard for “time waiting on the pool”. The span, the exemplar and the trace ID in the log answered an unplanned question, which is what observability means.

    In the interview

    Walk this funnel out loud: symptom on RED, split by dependency and deploy, exemplar trace from a slow bucket, longest span or gap, then that trace’s logs. Interviewers want a method, not a guess.

    Cost and scale

    Per-service telemetry budget at 5,000 requests a second:

    Signal Raw Lever After
    Logs 5 × 500 B lines per request: 1.08 TB/day One 1 KB wide line; keep every error and slow request (say 1%), 10% of the rest 5,000 × 0.109 × 1 KB × 86,400 ≈ 47 GB/day
    Traces 25 spans × 500 B: 62.5 MB/s ≈ 5.4 TB/day Tail-sample errors, slow and 1% of the rest, about 2% overall ≈ 108 GB/day, but the gateways buffer 62.5 MB/s × 30 s ≈ 1.9 GB
    Metrics 70,000 series every 15 s Prometheus stores 1–2 bytes per sample 4,667 samples/s × 86,400 × 1.5 B ≈ 0.6 GB/day, flat as traffic grows

    The 500-byte span is close to Dapper’s measured 426 bytes. Sampling logs has a price: you can’t look up an arbitrary successful request, and some audit rules forbid it. Record the rate on each kept entry so counts scale back up.

    Retention is a policy choice: often raw metrics for weeks (Prometheus’s local default is 15 days) and downsampled ones for a year, logs searchable for days then archived, traces for about a week. Keep high-cardinality questions (“which app version and bank hit the slow path?”) on events, traces and logs, and count unique users with HyperLogLog, not a user_id label.

    Profiles: the emerging fourth signal

    A profile samples stack traces to show which code burns CPU, allocates memory or blocks, explaining the “gap with no child span” in a waterfall. OpenTelemetry’s profiles signal is at Alpha status: an eBPF profiler for Linux needs no code changes, exports over OTLP, and can tag samples with trace IDs so a slow span links to the functions behind it.

    Going deeper: how observability fails

    • It dies with the system. Telemetry in the same cluster or region as the outage goes blank when you need it. Alert from separate infrastructure, alert on missing data, and page when a heartbeat alert stops firing.
    • A label melts the metrics store. One deploy labelling by raw URL or user ID can multiply series by thousands and exhaust the database’s memory. Cap series per metric at ingestion.
    • Log storms. Stack traces multiply volume exactly when you need the logs. Rate-limit per call site and load-test log shipping at peak.
    • Clock skew. Spans from drifting hosts can start before their parents. Synchronise clocks, and trust same-host durations over cross-host timestamps.

    Cheat sheet

    Idea Remember it as
    Monitoring vs observability Known questions vs new questions answered without new code
    Logs Structured JSON, one wide entry per request, trace_id on every line; cost = traffic × bytes
    Metrics Counter, gauge, histogram; cost = series count; no unbounded labels
    Percentiles Never average them; sum histogram buckets, then histogram_quantile
    Traces Spans with parents; traceparent = 00-trace(32 hex)-parent(16 hex)-flags
    Sampling Head is cheap and blind; tail keeps errors and slow traces but buffers everything
    What to measure Golden signals per system, RED per service, USE per resource; failed-request latency separately
    OpenTelemetry API, SDK, auto-instrumentation, OTLP (4317/4318), semantic conventions, Collector
    Correlation Exemplars link metric → trace; trace IDs link trace ↔ logs
    Alerting Page on symptoms: 14.4× over 1 h and 5 min, 6× over 6 h and 30 min; ticket at 1× over 3 days

    How to say it in the interview

    “I’d instrument every service with OpenTelemetry and ship through a collector tier. Metrics give RED per service and USE per resource, latency as histograms so percentiles aggregate, and no high-cardinality labels. Traces propagate W3C traceparent; I head-sample lightly and tail-sample every error and slow trace. Logs are structured JSON, one wide entry per request, with the trace ID on every line. I page only on SLO burn rate, and debug from metric to exemplar to trace to logs.”

    How would you find why p99 doubled? Confirm it on RED, split by dependency, endpoint, region, version and deploy. Open an exemplar trace from a slow bucket, find the longest span or gap, and read that trace’s logs.

    What’s wrong with averaging p99s? Percentiles are ranks, so their average means nothing. Nine pods at 100 ms and one at 1 s average 190 ms while the fleet p99 is 1 s. Sum histogram buckets, then compute once.

    Head or tail sampling? Head decides at the root: cheap, consistent, blind to errors that haven’t happened yet. Tail decides after the trace completes, so it keeps every error and slow trace, but buffers every span and needs each trace on one collector. At scale, use both.

    What do you alert on? Symptoms, as SLO burn rate: page at 14.4× over 1 hour (and 5 minutes) or 6× over 6 hours (and 30 minutes), ticket at 1× over 3 days. CPU and lag go on dashboards unless a cause is definite and imminent, like a disk filling within hours.

    Why not put user_id on a metric? Each value creates series: 2 million users × 14 histogram series is 28 million from one metric. Put user IDs on spans and logs, and use exemplars to hop from metric to request.

    Where to go next

    References and further reading

    1. Monitoring Distributed Systems (Google SRE book, chapter 6)sre.googleSymptoms versus causes, black-box and white-box monitoring, the four golden signals, separate latency for failed requests, and what makes a page worth sending.
    2. Alerting on SLOs (Google SRE Workbook, chapter 5)sre.googleBurn rate, the multiwindow multi-burn-rate recipe (14.4× over 1 hour, 6× over 6 hours, 1× over 3 days), short windows at 1/12 of the long one, and low-traffic caveats.
    3. Trace Context (W3C Recommendation)w3.orgThe exact traceparent format (version, 16-byte trace-id, 8-byte parent-id, trace-flags with the sampled bit), the tracestate header and the privacy rules.
    4. OpenTelemetry documentation: Samplingopentelemetry.ioHead versus tail sampling, consistent probability sampling on the trace ID, and the operational costs of a stateful tail-sampling tier.
    5. Prometheus: Histograms and summariesprometheus.ioWhy summary quantiles cannot be aggregated, why averaging them is statistically meaningless, and how histogram buckets are summed before histogram_quantile.
    6. Prometheus: Instrumentation best practicesprometheus.ioCounter versus gauge, a counter per log line, and the label cardinality guidance with the 10,000 users on 10,000 nodes example.
    7. The USE Method (Brendan Gregg)brendangregg.comUtilization, saturation and errors for every resource, including software resources, and how five-minute averages hid CPU saturation.
    8. The RED Method: How to instrument your services (Grafana Labs, 2018)grafana.comTom Wilkie’s rate, errors and duration for every service, and how RED and USE complement the golden signals.
    9. Dapper, a Large-Scale Distributed Systems Tracing Infrastructure (Sigelman et al., Google, 2010)static.googleusercontent.comSpans and trace trees, the 1-in-1,024 default sampling rate, measured tracing overhead by sampling rate, and about 426 bytes per span.
    10. Instrumenting distributed systems for operational visibility (Amazon Builders’ Library)aws.amazon.comOne structured request log per unit of work, separate timers for successful responses, trace IDs in every log, log sampling and rate-limiting error spam.