Problem
A dashboard shows p99 latency doubled, but aggregated metrics give no way to see which requests were slow. Searching a trace store by time window finds thousands of irrelevant traces.
Also known as: metric-exemplars, metrics-to-traces
Attach sample trace identifiers to metric data points so an engineer can jump from a latency spike straight to a representative slow trace.
A dashboard shows p99 latency doubled, but aggregated metrics give no way to see which requests were slow. Searching a trace store by time window finds thousands of irrelevant traces.
Metrics tell you that something is wrong; traces tell you why. Exemplars connect them. An exemplar is a small piece of metadata attached to a metric sample, normally a trace ID, that points to one concrete request that contributed to the value.
On each observation the instrumentation library looks at the active span. If sampled, it stores the trace ID with the histogram bucket the value falls into.
# OpenMetrics exposition
http_request_duration_seconds_bucket{le="1.0"} 2847 # {trace_id="4bf92f35"} 0.87 1700000000.1
http_request_duration_seconds_bucket{le="2.5"} 2851 # {trace_id="a1c3e8d2"} 2.31 1700000012.4
The metrics backend keeps the latest exemplar per bucket per series and returns them with queries. The dashboard overlays dots on the latency graph; clicking one opens the trace. The workflow becomes: notice the spike, click an exemplar from the high bucket, read the trace to find the slow downstream call.
Why not use trace ID as a label? A label creates a new time series for every value, which is unbounded cardinality. An exemplar sits beside the sample and does not multiply series.
Low - small extra storage
Medium - integration across tools
Medium - needs both pillars mature
Exemplar points to a trace dropped by sampling, giving a dead link
Trace ID added as a label instead, exploding cardinality
Clock or unit mismatch makes exemplars appear in the wrong bucket