OpenTelemetry Visual Handbook · lab

One attribute is not one more column

An account manager asks which customer is affected. You have a latency histogram with route, status and method on it, so you add enduser.id and ship it. The metric had 288 streams. It now has 2 304 000 — and nothing warned you, because nothing was wrong: a stream is identified by its attribute set, so attributes multiply.

Below is that arithmetic, with your numbers. Turn attributes on and off, and correct the value counts to your own service — the default numbers are a mid-sized shop and the only ones that matter are yours. Watch the product written out, not just the total: it is the whole argument.

Break it

  1. Turn on enduser.id. Four orders of magnitude, from one checkbox. This is the whole lab, and it is why the answer to “can we break this down by customer?” is never yes or no — it is a multiplication you do before answering.
  2. Switch the instrument from Histogram to Counter. The stream count does not move and the volume collapses, because a histogram publishes a point per bucket plus the sum and the count. A histogram is the expensive instrument before any attribute is added, and most teams reach for it by default.
  3. Now turn on cardinality_limit. The bill stops growing and the warning appears: the excess attribute sets fold into one otel.metric.overflow stream. Read that number carefully — it is how many breakdowns you are paying to produce and cannot read. The limit protects the invoice, not the question.
  4. Set k8s.pod.name to 40 and think about deploys. The product is over values actually seen, so each rollout retires 40 pod names and mints 40 more. The old streams stay until retention rolls them off — which is why cardinality incidents outlive the change that caused them by days.

Predict before you look

Answer first, then compute it above. Predicting and being wrong is what makes the answer stick.

A histogram has http.route (12 values), http.response.status_code (6) and http.request.method (4). You add enduser.id, which has 8 000 values in production. How many streams does the metric have afterwards?

Commit to an answer first. You cannot change it afterwards.

What people get wrong

Every row below is a belief worth naming, because the arithmetic above is arranged to take it apart. The right column is the specification, with the link.

What people believe What the spec says
One more attribute is one more column. A metric stream is identified by its attribute set, so the number of streams is the PRODUCT of the distinct values, not the sum. Adding an attribute with 8 000 values to a metric that had 288 streams does not give you 8 288 — it gives you 2 304 000. source
A histogram costs about the same as a counter. A counter publishes one point per series. A histogram publishes one per bucket, plus the sum and the count — so with the default bucket set it is over ten times the volume before a single attribute is added. source
The cardinality limit protects me, so high-cardinality attributes are safe. It caps the number of streams, and that is all it does. The excess folds into a single overflow stream: nothing is lost and nothing is double-counted, and the breakdown you added the attribute FOR is gone. You pay the instrumentation cost and get no answer. source
I will add it temporarily, just to debug this incident. The product is over values actually SEEN, so a series exists from the first measurement that carries it until retention rolls it off. A pod name that lived for one deploy keeps its streams for the whole window — which is why cardinality incidents outlive the change that caused them. source

Check it against a real SDK

This lab is arithmetic, so it owes you a way to confirm the arithmetic is the SDK's and not ours. It is — and the counterpart already exists: labs/three-pillars/ runs a real SDK against a real Collector, and PILLARS_USER_ATTRIBUTE is exactly the move this lab charges you for.

  1. In labs/three-pillars/compose.yaml, leave PILLARS_USER_ATTRIBUTE as false and run docker compose up --build. In the Collector's output, count the distinct attribute sets on http.server.request.duration — route times status.
  2. Set it to true and run again. The count is multiplied by the number of distinct users the app emits, not increased by it.

That is the one claim this lab makes about the world, and it is checkable. What it deliberately does not claim is a price: every vendor bills on a different unit, so the lab computes volume — which is mechanical — and leaves the price field for you to fill from your own invoice. A number we invented would be the most expensive kind of mistake this project can make.

Read the theory

What an attribute costs is the prose version of this arithmetic, and it carries the part a calculator cannot: why the decision is taken in instrumentation code by one developer and paid for by everyone a month later. It starts from What a signal is — a metric throws the individual away, and an attribute is how you take part of that back.

Check your understanding

The assessment for this lab has not been authored yet.

Continue to the handbook assessment →