Module O-1·28 min read

Metrics vs logs vs traces and what each one costs, cardinality as a budget, distributed tracing, and correlation IDs that actually correlate.

JJS
Written by Jatin Jain Saraf · Senior Software Engineer

Module O-1 — Observability

What this module covers: The difference between monitoring and observability, stated as a practical test: can you answer a question you did not anticipate without shipping code? The three signals and what each one genuinely cannot do, plus the wide structured event that increasingly replaces the separation. Cardinality as the budget that governs your metrics bill, and the single label that has taken down more metrics backends than any other cause. Why you cannot average percentiles, and why histogram buckets must be chosen before you need them. Correlation IDs propagated through every hop including queues and background jobs, which is the gap most systems have. Traces, and why tail-based sampling is what you actually want. Then the synthesis section: every mechanism built in the previous three phases has one metric that proves it is working, and this is where they are listed together — because a circuit breaker, a retry budget, a replication slot and a reconciliation job are all silent until someone measures them.


Monitoring Answers Known Questions; Observability Answers New Ones

Monitoring is the set of dashboards and alerts you built for failures you predicted: CPU, error rate, disk space, queue depth. It works well, and it only covers what you thought of.

Observability is the property that lets you answer a question nobody anticipated. "Why are requests from Android clients in Sydney on the new checkout flow slow, but only for tenants on the enterprise plan, and only since Tuesday?" — a question with five dimensions nobody predicted, and which must be answerable now, from data you already have.

The practical test:

If answering a new question requires deploying code, you have monitoring. If you can slice the data you already collect along a dimension you did not plan for, you have observability.

That test explains why the shape of the data matters as much as its volume. A pre-aggregated counter of requests by status code cannot be sliced by tenant afterwards — the information was discarded at write time. A wide event carrying tenant, plan, region, client version, endpoint and duration can be sliced by any of them, forever.

Analogy: a car's dashboard versus an engine diagnostic port. The dashboard shows the six things the designer decided you would need — speed, fuel, temperature — and it is perfect for those. When the car makes an unfamiliar noise under load in third gear, the dashboard has nothing, and no amount of staring at it helps; you plug in a tool that reads everything the engine records and go looking. The dashboard is monitoring. The diagnostic port is observability. Both exist because they answer different questions, and building only the dashboard is what makes an unfamiliar noise a two-day investigation.


Three Signals, Three Different Bills

MetricsLogsTraces
Shapenumeric time seriesdiscrete events with textcausal spans across services
Answers"how many, how fast, trend""what exactly happened here""where did the time go"
Cannot answer"why was this request slow"aggregate questions cheaplyanything not sampled
High cardinalitycatastrophicfinefine
Cost driverunique label combinationsvolume × retentionsampled volume
Best foralerting, SLOs, dashboardsdebugging one case, auditdistributed latency, dependency graphs

Each has a hard limit that the other two cover:

  • Metrics are pre-aggregated, so per-request detail is gone. You can know that 3% of requests failed and not which ones or why.
  • Logs are per-event and therefore expensive to aggregate — computing a p99 from raw logs means scanning them, which is why log-based dashboards get slow and costly as traffic grows.
  • Traces show causality across services, which nothing else does — and they are sampled, so the one request a customer is complaining about may not exist in your trace store.

The convergence: wide structured events

The modern position is that the three-pillar split is partly an artefact of tooling, and that the better primitive is one wide, structured event per unit of work:

json

One such event replaces a great many metrics, because metrics can be derived from events while events cannot be derived from metrics. The information is kept, so a new dimension is a new query rather than a new deploy. That is the observability test satisfied by construction.

The costs are real: storage and a backend that can aggregate over high-cardinality fields at query time, plus discipline about what goes in the event. And metrics do not go away — they remain the right substrate for alerting, because an alert must evaluate cheaply, frequently, and over a long window.


Cardinality Is the Budget, and One Label Has Ruined More Backends Than Anything Else

A metric's cost is not the number of data points. It is the number of unique label combinations, each of which is a separate time series that must be stored, indexed and held in memory.

text

That is not a gradual cost increase; it is an out-of-memory kill on the metrics backend, or a bill that rises by an order of magnitude overnight. The labels that cause it are always the same: user_id, request_id, session_id, email, a full URL path with IDs in it, an error message string, or a timestamp.

The rule, and it is close to absolute:

Metrics take bounded dimensions only. Every unbounded or high-cardinality dimension belongs in a log line, a trace, or a wide event.

Which makes path normalisation mandatory: /orders/8891/items/42 must be recorded as /orders/:id/items/:id, or you have one series per order. Most frameworks' auto-instrumentation does this; hand-rolled instrumentation frequently does not, and the discovery is usually a billing alert.

Two related traps:

Histogram buckets multiply cardinality. A latency histogram with 12 buckets creates 12 series per label combination — so a histogram is roughly an order of magnitude more expensive than a counter, and adding a label to a histogram costs proportionally more.

You must choose buckets before you need them. Percentiles are computed from bucket boundaries, so if your buckets stop at 1 second, you cannot learn anything about a 4-second p99.9 after the fact. Choose bucket boundaries that straddle your SLO thresholds, and accept that this is a decision made in advance — which is precisely the limitation wide events do not have, since a raw duration field supports any percentile computed later.

You cannot average percentiles

This error is extremely common and produces confidently wrong dashboards:

text

A percentile is not a linear quantity, so it cannot be averaged, summed or meaningfully compared across differently-loaded instances. The correct aggregation is over the histograms: merge bucket counts across instances, then compute the quantile once. Prometheus's histogram_quantile(0.99, sum(rate(bucket[5m])) by (le)) does exactly this, and the sum … by (le) is the part people omit.

Two further honesty requirements. Measure client-perceived latency, not only server-side handler time — the server's p99 excludes DNS, connection setup, queueing at the load balancer, and the network, which is where a meaningful share of user-perceived slowness lives (Module F-5). And look at the distribution, not just the percentile: a bimodal latency distribution (cache hit versus miss) has a p99 that describes neither mode.


Correlation IDs, and the Boundary Everyone Forgets

A single ID that identifies one logical operation, present on every log line, every span, and every downstream call, is the cheapest and highest-value thing in this module. Without it, "what happened to this request" is a manual join across services by timestamp, which does not work under load.

Sign in to keep reading

The rest of this module is free — sign in with Google to unlock it and track your progress.

Sign in & Register

Discussion

0

Join the discussion

Loading comments...

© 2026 Jatin Jain Saraf (JJS). All rights reserved.