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
- 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. - 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.
- Now turn on
cardinality_limit. The bill stops growing and the warning appears: the excess attribute sets fold into oneotel.metric.overflowstream. 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. - Set
k8s.pod.nameto 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.
-
In
labs/three-pillars/compose.yaml, leavePILLARS_USER_ATTRIBUTEasfalseand rundocker compose up --build. In the Collector's output, count the distinct attribute sets onhttp.server.request.duration— route times status. -
Set it to
trueand 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.