Warmup Guide — Observability, Azure Monitor & KQL

Zero-to-principal primer for Phase 13: what the Azure Monitor umbrella actually is (and which store answers which question), the KQL read pipeline operator by operator and the mechanism under each one, how percentiles and distinct-counts are really computed at scale (and why they're approximate), how distributed tracing stitches a request back together with operation_Id, the cost/fidelity math of sampling, and how an alert rule turns an aggregation over a window into a 2 a.m. page. Read it slowly; build the lab as you go.

Table of Contents


Chapter 1: What "Observability" Actually Means

From zero. Monitoring answers questions you knew to ask in advance — "is CPU above 80%?" Observability is the property that lets you answer questions you didn't anticipate — "why are exactly the users in the EU West region who hit the checkout API after the 14:02 deploy getting 503s?" — without shipping new code, purely by interrogating the telemetry you already emit. The distinction matters because production never fails the way you planned for; the failures that page you are the ones no pre-built dashboard anticipated.

The classic frame is the three pillars:

PillarWhat it isThe question it answersAzure store
Metricsnumeric time series (a number sampled over time)"is it up? how loaded? trending where?"Azure Monitor Metrics
Logstimestamped structured records (rows with fields)"what exactly happened to this request/resource?"Log Analytics workspace (queried with KQL)
Tracescausally-linked spans across services"which hop in the distributed call was slow/failed?"Application Insights (requests/dependencies)

A principal's first move in any incident is to pick the right pillar: a metric tells you that something is wrong and roughly how bad; a log tells you what happened to a specific entity; a trace tells you where in a multi-service path it happened. Reaching for the wrong one — grepping logs to answer "is the service up" — is how you lose the first fifteen minutes of an incident.

Chapter 2: The Azure Monitor Umbrella — Four Stores, Four Questions

"Azure Monitor" is a brand over several genuinely different data planes. Conflating them is the single most common source of both wrong answers and surprise bills.

                         ┌──────────────────── Azure Monitor ───────────────────┐
   resources, apps  ──►  │  METRICS        LOGS            ALERTS    APP INSIGHTS │
   (emit telemetry)      │  (numeric TS)   (Log Analytics  (rules → (APM: req/dep │
                         │  pre-aggregated  workspace,      action    /trace/exc, │
                         │  ~1-min, cheap)  KQL, flexible)   group)   operation_Id)│
                         └───────────────────────────────────────────────────────┘
                                │              │              │            │
                          near-real-time   rich queries   page someone  trace trees
  • Metrics — a numeric value (CPU%, request count, queue depth) sampled at a fixed cadence (typically 1 minute), pre-aggregated at ingestion, retained cheaply, queried with a lightweight metrics API (and charted instantly). Near-real-time; low cardinality by design.
  • Logs — a Log Analytics workspace: many tables (requests, traces, AzureDiagnostics, Heartbeat, SigninLogs, …), each a stream of typed rows. You query them with KQL. Flexible and powerful; billed by GB ingested and retention.
  • Alerts — rules evaluated on a schedule over either a metric or a log query; breaching fires an action group (email/SMS/push/webhook/Logic App/runbook).
  • Application Insights — an APM layer that writes into a Log Analytics workspace (workspace-based AI) the tables requests, dependencies, traces, exceptions, customEvents, pageViews — all stamped with operation_Id so a trace can be reassembled (Chapter 9).

The seam failures live exactly here: putting a high-cardinality dimension into a metric (metrics aren't built for it → silent split or rejection), or trying to alert on a log with a too-short window (no data → flaps), or assuming Application Insights data is a metric when it's a log table with its own cost.

Chapter 3: Metrics vs Logs — The Pre-Aggregation Tradeoff

Why two stores at all? Because there is a fundamental tradeoff between pre-aggregation (cheap, fast, but you must choose the dimensions in advance) and raw retention (flexible, queryable any way later, but expensive).

MetricLog (Log Analytics)
stored aspre-aggregated time seriesraw typed rows
granularity~1 minute, fixedper-event
cardinalitylow (a few dimensions)high (any field)
queryfast, limitedKQL, arbitrary
latency to queryseconds (near-real-time)seconds–minutes (ingestion lag)
costcheap$/GB ingested + retention
use it for"is it up / how loaded / trending""what happened to this one"

The principal rule: alert on metrics, investigate in logs. A metric alert is cheap to evaluate every minute and catches the that; once it fires, you pivot to a KQL query for the what and why. Building both pipelines (the lab does) makes the asymmetry concrete — the alert evaluator aggregates over a window like a metric rule; the operators run a full KQL query like a log investigation.

Chapter 4: KQL — The Read Pipeline, Operator by Operator

From zero. A KQL query is a table piped left-to-right through tabular operators, each of which takes a table and returns a table:

requests
| where Success == false                          // filter rows
| extend Bucket = bin(TimeGenerated, 5m)          // add a computed column
| summarize n = count() by Service, Bucket        // group-and-reduce
| where n > 100                                    // filter the aggregate
| top 10 by n desc                                 // the 10 worst
| project Service, Bucket, n                       // pick columns

Read it as a Unix pipe: the table flows down, each | is one transform. Order matters and is a performance decision — a where early shrinks the row set every later stage touches. In the lab, each operator is a pure function f(rows) -> rows that returns a new list and never mutates its input, which is precisely why a KQL query is safe to re-run, cache, and reason about.

OperatorWhat it doesLab function
wherekeep rows matching a predicatewhere(rows, predicate)
projectkeep / rename a subset of columnsproject(rows, columns)
extendadd computed columnsextend(rows, **computed)
order by / sort bystable sort by a columnorder_by(rows, key, desc)
top n bythe n largest/smallest by a columntop(rows, n, by, desc)
take / limitthe first n rowstake(rows, n)
countnumber of rows (a scalar)count(rows)
summarize ... bygroup-and-reducesummarize(rows, aggs, by)
joincorrelate two tables on a keyjoin(left, right, on, kind)

A subtlety the lab encodes faithfully: in extend NewCol = expr, every expr sees the original row — computed columns within one extend do not see each other. If you need column B to depend on column A you just added, you chain a second extend. That's Kusto's real semantics, and it's why the lab's extend callables each receive the un-extended row.

Chapter 5: summarize and bin — Group-and-Reduce Over Time

summarize is the heart of KQL — it's GROUP BY with the aggregation built in.

requests
| summarize p95 = percentile(DurationMs, 95), n = count()
          by Service, bin(TimeGenerated, 5m)

Mechanism, exactly as the lab implements it:

  1. Compute the group key for each row — the tuple of the by columns. With by Service, bin(TimeGenerated, 5m) the key is (Service, 5-min-bucket).
  2. Bucket rows by that key (a hash map: key → list of rows).
  3. Reduce each bucket: apply every aggregation to that bucket's column values.
  4. Emit one row per group: the by-columns + the aggregates.

Real Kusto does not promise group order; the lab sorts groups by key so output is deterministic and testable — a small, deliberate divergence from production for the sake of assertable tests.

bin(value, width) is the function that makes time-series rollups work:

$$\text{bin}(t, w) = \left\lfloor \frac{t}{w} \right\rfloor \cdot w$$

It floors t onto an absolute grid of w-sized buckets — not relative to your query's start time. That absoluteness is the whole point: two independent queries, two services, two days apart, with the same bin(TimeGenerated, 5m) land on the same bucket boundaries, so their series line up and can be joined or overlaid. A consequence the lab tests: with epoch base t0 = 1_700_000_000 and w = 300, the first bucket floors to t0 - 200sbelow t0 — because the grid doesn't care where your data starts. Negative inputs floor toward $-\infty$ (so bin(-1, 300) = -300), matching math.floor and Kusto. Internally, that's why the lab's bin_time is math.floor(value / width) * width.

Chapter 6: Aggregations, Percentiles & the Math

The fold functions a principal recites: count, sum, avg, min, max, dcount (distinct count), and percentile. The first five are obvious. The interesting two are percentile (subtle) and dcount (expensive — Chapter 7).

Why percentiles, not averages. The average hides the tail, and the tail is the user experience. A service with avg latency 80 ms can still have a p99 of 4 s — and that p99 is a real cohort of real users having a bad time. "Real-time" means a number and a percentile (Phase 00's banned-word lesson). So you summarize on percentile(Duration, 95) and percentile(Duration, 99), not avg.

How a percentile is computed. There are several definitions; the lab uses nearest-rank because it returns an observed value and is therefore exactly assertable. For $n$ sorted values and percentile $p$:

$$\text{rank} = \lceil \tfrac{p}{100} \cdot n \rceil \quad (\text{1-based}), \qquad P_p = \text{value at that rank}$$

Worked example — the values $1, 2, \dots, 100$ (so $n = 100$):

$$\text{rank}{95} = \lceil 0.95 \cdot 100 \rceil = 95 ;\Rightarrow; P{95} = 95$$

That is why the lab asserts percentile([1..100], 95) == 95 exactly — no interpolation, no ambiguity. The contrast is linear interpolation (the "percentile.inc" / R-7 method), which returns a value between two observations when the rank isn't an integer; it's smoother but never an observed point. Both are legitimate; the lab documents and tests nearest-rank so the answer is unambiguous.

What Kusto actually does, and why it's approximate. At billions of rows you cannot sort the whole column to find a percentile — that's $O(n \log n)$ over petabytes. So Kusto's percentile() uses a T-Digest: a compact, mergeable sketch that keeps fine-grained centroids near the tails (where percentiles matter) and coarse ones in the dense middle, giving a bounded-memory, distributed-friendly approximation with a small, bounded error. The principal point for an interview: percentile at scale is approximate by design, and that's the right trade — an exact p95 that takes 40 s and 8 GB of RAM is worse than a 0.1%-off p95 in 200 ms. The lab uses exact nearest-rank only so its tests have known answers.

Chapter 7: Cardinality — Why dcount Blows Up the Bill

Cardinality is the number of distinct values a column takes. It is the single most important cost lever in observability, and the one juniors most consistently miss.

dcount(X) answers "how many distinct X?" — distinct failing users, distinct affected operations. It's the blast-radius gauge: 3 distinct users hitting an error is a bug report; 30,000 is a Sev-1. But computing exact distinctness requires remembering every value seen, which at scale is impossible — so Kusto's dcount uses HyperLogLog, a probabilistic sketch that estimates cardinality in kilobytes regardless of how many billions of values pass through, with a tunable accuracy (~1.6% standard error by default). Approximate, by necessity, just like percentile.

The cost trap is using a high-cardinality column as a by dimension:

// CHEAP: a handful of groups
requests | summarize count() by Service, ResultCode

// EXPENSIVE: one group per distinct user → millions of groups
requests | summarize count() by user_Id

Grouping by user_Id creates one output row per distinct user — millions of groups, huge intermediate state, slow query, big result. The same cardinality explosion hits metrics: a metric dimension with high cardinality multiplies the stored time series (cost) or gets rejected/split. The principal habits:

  • Aggregate to a low-cardinality dimension first (by Service, bin(...)), then dcount the high-cardinality thing inside the group as a measure, not a key.
  • Never put requestId / user_Id / full URLs (with query strings) into a metric dimension or a summarize ... by.
  • Use sampling (Chapter 9) to cap the raw volume before it's even ingested.

This is the "$/TB scanned, layout is a cost feature" law (Phase 00) reappearing in the observability layer: cardinality is the layout that costs money.

Chapter 8: join — Correlating Tables

A single table rarely tells the whole story. join correlates two on a shared key — most importantly requests ⋈ dependencies on operation_Id to attribute a request's latency to its downstream calls.

requests
| join kind=inner (dependencies) on operation_Id
| summarize req_p95 = percentile(DurationMs, 95),
            dep_time = sum(dep_DurationMs) by operation_Id

The two kinds the lab implements (Kusto has more):

  • inner — emit a merged row for every (left, right) pair sharing the key. Rows with no match on either side vanish. Use when you only care about correlated pairs.
  • leftouter — keep every left row at least once; when a left row has no right match, the right columns are null. Use when the gaps are the signal — "which requests had no downstream dependency call?" An inner join would silently hide exactly those rows.

Mechanism (the lab's, and the real engine's default strategy): hash join — index the right side by key into a hash map, then stream the left side looking each key up. $O(n + m)$ rather than $O(n \cdot m)$. The lab emits in left order then matching-right order so the result is deterministic, and right-side columns win on a name collision (documented; real Kusto keeps both with a suffix).

The inner-vs-leftouter choice is a classic correctness trap: investigating "why are some requests slow" with an inner join to dependencies will drop the requests that made no dependency call — which might be exactly the ones stuck somewhere else. Default to leftouter when you're hunting for what's missing.

Chapter 9: Distributed Tracing — Spans, operation_Id & Sampling

From zero. In a microservice system one user action fans out across services: the API gateway calls a Function, which reads Service Bus, which calls Key Vault, which calls a database. A trace is the tree of all that work; each unit of work is a span (an operation with a start, a duration, a status, and a parent). Application Insights records them in tables and ties them together with two fields:

  • operation_Id — the trace id: the same value on every span of one user action, across every service. This is the join key that reassembles the request.
  • operation_ParentId — the parent span id: which span caused this one, so the flat list of spans becomes a tree.
operation_Id = abc123                         (one user request, all services)
  request  span (gateway)        parent=∅
   └─ dependency span (→ func)   parent=gateway
        └─ dependency (→ bus)    parent=func
        └─ dependency (→ vault)  parent=func

A query that joins requests to dependencies on operation_Id and walks operation_ParentId rebuilds that tree and shows you which hop spent the time — ending the "it's not my service" standoff with data. That correlation id is propagated between services via W3C Trace Context headers (traceparent), which is why the same operation_Id shows up in five different services' telemetry.

Sampling — the cost/fidelity dial. Tracing every request at high volume is expensive (every span is a billed log row). Sampling keeps a fraction:

  • Head-based sampling decides at ingress, before the request runs, to keep or drop the whole trace — typically with a fixed probability $r$ (the "sampling rate"). Cheap and simple; the cost is predictable ($\approx r \cdot$ volume). The flaw: it's a coin flip that doesn't know the outcome, so it drops error and slow traces at the same rate as boring ones — exactly the traces you wanted.
  • Tail-based sampling decides after the trace completes, when you know it errored or was slow, and keeps the interesting ones (all errors, all p99-slow, a sample of the rest). Far higher fidelity for the same budget; the cost is operational complexity (you must buffer spans until the trace finishes).

The math you'd recite: if you head-sample at rate $r$ and a true population statistic over the kept traces is $\hat{x}$, your estimate of the population total is $\hat{x}/r$ — but the variance of that estimate grows as $r$ shrinks, and rare events (errors at rate $\varepsilon$) survive at expected count $\varepsilon \cdot r \cdot N$, so at $r = 0.01$ you keep only 1% of an already-rare error class and your error dashboards go blind. Application Insights' default adaptive sampling is a head sampler that varies $r$ to hit a target items-per-second, which controls cost but inherits the "drops errors too" flaw — which is why serious shops add tail sampling (often via an OpenTelemetry Collector) for the error/slow paths. Naming this trade out loud is a principal-level signal.

Chapter 10: Alert Rules — Aggregation, Window, Threshold, Action

An alert is the automation that turns telemetry into a human being woken up. Its anatomy, which the lab's AlertRule + evaluate_alert implement exactly:

fire iff   AGG( metric over [now − window, now] )   OP   threshold      →  action group
           └──── aggregation ────┘ └── lookback ──┘  └ gt/lt/ge/le ┘
  1. Signal — the metric or the result column of a log query.
  2. Aggregationavg / min / max / count / sum / percentile over the window.
  3. Lookback window — only data in [now - window, now] is considered. Too short → noisy and gappy; too long → slow to detect.
  4. Operator + thresholdgt / lt / ge / le against the threshold. The boundary is a real bug surface: a value of exactly 300 against threshold 300 fires under ge but not under gt. Pick wrong and you either miss the breach or page on the boundary forever (the lab tests this).
  5. Action group — who/what gets notified: email/SMS/push, a webhook (PagerDuty/ITSM), a Logic App, an Automation runbook.

Two production refinements the lab's structure points at:

  • No-data handling. An empty window has no metric to breach, so the lab returns False (no false page). In Azure you separately decide whether "no data" is itself an alert — for a heartbeat it absolutely is (silence means the thing died); for a low-traffic endpoint it isn't.
  • Debounce (for / number-of-violations). A single noisy sample shouldn't page. Real rules require the threshold be breached for N consecutive evaluations (the for window) before firing. This is the single most effective noise-reduction knob, and the extension the lab README suggests.

Metric alert vs log (scheduled-query) alert: a metric alert evaluates a cheap pre-aggregated series every minute (low latency, low cost, low flexibility); a log alert runs a full KQL query on a schedule (any logic you can express, higher latency and cost). Use a metric alert for "CPU/availability/latency from the platform metric"; a log alert for "p95 of my custom field by my dimension" — exactly the metric-vs-log split from Chapter 3.

Chapter 11: SLO, SLI & the Error Budget

The JD's "ensure high availability" is, done rigorously, the Google-SRE error-budget loop — and observability is what makes it measurable:

  • SLI (Indicator) — a query: e.g. availability $= \dfrac{\text{good requests}}{\text{total requests}}$, expressed as a KQL ratio over a window.
  • SLO (Objective) — a target on the SLI: "99.9% of requests succeed over 28 days."
  • Error budget — the allowed failure: $1 - \text{SLO}$. At 99.9% that is 0.1% of requests — about 43 minutes of full outage per 30 days, or the equivalent spread as errors.

The budget is the point. It converts reliability from a vibe ("be more careful") into a number you spend: while you have budget left, ship fast; when you're burning it too quickly, freeze risky changes. A burn-rate alert fires when you're consuming budget faster than the window allows (e.g. spending an hour's worth of budget in five minutes) — far better than a raw threshold because it scales with traffic and ties the page to business impact. This is the bridge into Phase 14 (Reliability), and the reason this phase comes right before it: you cannot manage an error budget you cannot query.

Lab Walkthrough Guidance

Lab 01 — KQL Query Engine + Alert Evaluator, suggested order (matches lab.py top to bottom):

  1. Row-shapingwhere(rows, predicate) (validate callable; return copies), project(rows, columns) (list or {new: old}; raise on a missing column), extend(rows, **computed) (each callable sees the original row). These are Ch. 4.
  2. Ordering/windowingorder_by (stable sort), top (= order_by[:n], default desc), take, count. Validate n >= 0.
  3. bin_time(value, width)math.floor(value/width)*width; validate width > 0. Test the grid (299 → 0, 300 → 300) and the negative case (Ch. 5).
  4. percentile(values, p) — nearest-rank: rank = ceil(p/100·n), clamp to [1, n], return the value at that rank; p=0 → min, p=100 → max. Validate non-empty and 0 ≤ p ≤ 100 (Ch. 6).
  5. summarize(rows, aggregations, by) — bucket by the by-tuple, reduce each group, emit in sorted key order. Support count/sum/avg/min/max/dcount/percentile; by=[] → one global row. Validate aggregations non-empty, funcs known, by-columns exist (Ch. 5–6).
  6. join(left, right, on, kind) — hash-index the right by key; for each left row emit merged rows for matches (inner), or a null-padded row if leftouter and no match. Validate kind and key presence (Ch. 8).
  7. AlertRule + evaluate_alert(rows, rule, *, now) — validate operator/window/agg in __post_init__; window-filter to [now - window, now], aggregate the metric, compare with the operator. Empty window → False (Ch. 10).
  8. Make Table construct (defensive copy, reject non-dicts) so the fluent tests pass.

Run red, make green: pytest test_lab.py -v, then LAB_MODULE=solution pytest test_lab.py -v, then python solution.py for the worked example.

Success Criteria

You are ready for Phase 14 when you can, from memory:

  1. Name the three pillars (metrics/logs/traces), the Azure store for each, and the question each answers — and state when you reach for which.
  2. Write a KQL query that computes p95 latency + count by 5-minute bin and service, and explain each operator's effect on the row set.
  3. Compute a nearest-rank percentile by hand, contrast it with interpolation, and explain why Kusto uses an approximate T-Digest at scale.
  4. Explain why dcount and high-cardinality by dimensions blow up cost, and what you do instead.
  5. Explain inner vs leftouter join and the correctness trap of using inner when you're hunting for what's missing.
  6. Draw the operation_Id / operation_ParentId correlation and contrast head vs tail sampling with the cost/fidelity argument.
  7. State the alert anatomy (agg(window) <op> threshold → action group), the gt-vs-ge boundary trap, and the for/debounce knob.
  8. Define SLI/SLO/error-budget and explain a burn-rate alert.

Interview Q&A

Q: "The checkout API got slow after a deploy. Walk me through finding the cause." First I pick the pillar: a metric confirms that p95 latency rose and when (correlate with the deploy time). Then I move to logs for the what: `requests | where TimeGenerated

ago(30m) | summarize p95=percentile(DurationMs, 95), n=count() by Service, bin(TimeGenerated, 5m)andrender timechartto see which service and which minute. Then **traces** for the *where*:join requeststodependenciesonoperation_Idandsummarize sum(dependency duration)` to attribute the latency to the exact downstream hop — is it the database, Key Vault, Service Bus? The query names the culprit in three steps; I never guess.

Q: How does percentile scale to billions of rows, and what does that cost you? It can't sort the column — that's $O(n \log n)$ over petabytes. Kusto computes percentiles from a T-Digest sketch: mergeable, bounded-memory, fine-grained near the tails where percentiles live, coarse in the dense middle. So the answer is approximate by design, with a small bounded error — which is the right trade, because an exact p95 that takes 40 s is worse than a 0.1%-off p95 in 200 ms. dcount is the same story with HyperLogLog (~1.6% standard error). If I genuinely need exact, I aggregate a small filtered subset; at scale I accept the sketch.

Q: A team wants to summarize ... by user_Id. What do you tell them? That user_Id is high-cardinality, so it produces one output row per distinct user — millions of groups, a slow query, a huge result, and if it's a metric dimension, a multiplied bill or a rejected metric. What they almost always want is dcount(user_Id) as a measure inside a low-cardinality group (by Service, bin(...)) — "how many distinct users were affected per service per 5 minutes," which is the blast-radius number. Cardinality is the layout that costs money; you aggregate the high-cardinality thing, you don't group by it.

Q: Metric alert vs log alert — when each, and how do you stop it flapping? A metric alert evaluates a cheap pre-aggregated series every minute — use it for platform-emitted CPU/availability/latency; low cost, low latency, low flexibility. A log (scheduled-query) alert runs a full KQL query on a schedule — use it when the condition needs your fields, your dimensions, or a join; more flexible, more expensive, slightly higher latency. To stop flapping: a debounce (for window / "fire after N consecutive violations"), a sensible lookback window (not so short it's gappy), and explicit no-data handling. And I watch the boundary: gt vs ge against the threshold decides whether an exactly-at-the- line value pages — I pick deliberately, because the off-by-one there is a real false-page bug.

Q: Explain head vs tail sampling and when you'd pay for tail. Head sampling decides at ingress with a fixed probability — cheap and predictable, but it's a coin flip that doesn't know the outcome, so it drops error and slow traces at the same rate as boring ones, blinding your error dashboards exactly when you need them. Tail sampling decides after the trace completes, keeping all errors and slow traces plus a sample of the rest — far higher fidelity per dollar, at the cost of buffering spans until the trace finishes (usually an OpenTelemetry Collector). I'd run adaptive head sampling for baseline volume and add tail sampling for the error/slow paths once the trace volume — or the cost of missing a rare failure — justifies the operational complexity.

Q: What's an error budget and why does it change behavior? It's $1 - \text{SLO}$: at a 99.9% SLO, 0.1% of requests (≈43 minutes of full outage per 30 days) is allowed to fail. It turns reliability into a number you spend — ship fast while budget remains, freeze risky changes when you're burning it. A burn-rate alert (you're spending budget faster than the window permits) beats a raw threshold because it scales with traffic and ties the page to actual user impact instead of an arbitrary number.

References

  • KQL language reference — Microsoft Learn, Kusto Query Language (KQL) overview and the per-operator pages (where, project, extend, summarize, join, bin, top): https://learn.microsoft.com/azure/data-explorer/kusto/query/
  • Aggregation functionspercentile/percentiles, dcount, count, avg: https://learn.microsoft.com/azure/data-explorer/kusto/query/aggregation-functions
  • Azure Monitor overview (metrics vs logs vs alerts, the data platform): https://learn.microsoft.com/azure/azure-monitor/overview
  • Log Analytics workspace & data structure: https://learn.microsoft.com/azure/azure-monitor/logs/log-analytics-workspace-overview
  • Azure Monitor alerts (metric vs log alerts, action groups, the for/debounce): https://learn.microsoft.com/azure/azure-monitor/alerts/alerts-overview
  • Application Insights distributed tracing & correlation (operation_Id, operation_ParentId, W3C Trace Context): https://learn.microsoft.com/azure/azure-monitor/app/distributed-tracing-telemetry-correlation
  • Application Insights sampling (adaptive/fixed-rate head sampling and the tradeoff): https://learn.microsoft.com/azure/azure-monitor/app/sampling
  • T-Digest — Dunning & Ertl, Computing Extremely Accurate Quantiles Using t-Digests (the algorithm behind percentile at scale).
  • HyperLogLog — Flajolet et al., HyperLogLog: the analysis of a near-optimal cardinality estimation algorithm (behind dcount).
  • Google SRE BookService Level Objectives and Monitoring Distributed Systems chapters (SLI/SLO/error-budget, the four golden signals).
  • W3C Trace Context — https://www.w3.org/TR/trace-context/ (the traceparent propagation standard).
  • The track's own CHEATSHEET.md and GLOSSARY.md.