The observability pipeline: Collector, sampling, exemplars, wide events, profiling
What it is
Modern observability is not "metrics, logs, and traces" as three separate systems. It is a pipeline that collects signals, processes them (sampling, redaction, enrichment), and routes them to backends, with the signals correlated so an investigation moves between them.
apps (SDKs) COLLECTOR backends
│ ┌──────────────┐
metrics ─────────▶│ receive │──▶ metrics store (Prometheus, Mimir)
traces ─────────▶│ process: │──▶ trace store (Tempo, Jaeger)
logs ─────────▶│ - sample │──▶ log store (Loki, ELK)
profiles ────────▶│ - redact │──▶ profile store (Pyroscope)
│ - enrich │
│ - batch │
└──────────────┘
The OpenTelemetry Collector is the pipeline's centre: receivers ingest, processors transform, exporters route, and the whole thing is vendor-neutral, so the backend is a configuration choice rather than an instrumentation rewrite.
What this is confused with: "observability 1.0", three pillars queried separately. The argument of "observability 2.0" is that the useful primitive is not three signal types but one wide event per unit of work, carrying every dimension, from which metrics, traces and logs are all derived. The distinction is not academic: it determines whether you can ask a question you did not instrument for in advance, which is the entire promise of observability over monitoring.
The problem it solves
Monitoring answers questions you predicted. Observability answers questions you did not.
Monitoring: "alert when p99 latency > 300 ms" (predicted)
Observability: "why is p99 latency high, and is it
concentrated in one customer, one
region, one API version, one feature
flag cohort, on the write path?" (not predicted)
The pipeline exists because raw signals are too expensive to keep at full fidelity and too uncorrelated to investigate:
A service at 50,000 req/s, one trace per request:
full traces: 50,000 traces/s, ~150 GB/day per service
storage cost: dominant, and most traces are of successful,
unremarkable requests
You cannot keep every trace, and the ones you throw away must not be the interesting ones, which is the sampling problem. And a metric spike with no way to jump to an example trace is a dead end, which is the correlation problem.
Mechanics
The Collector pipeline
receivers:
otlp:
protocols: {grpc: {endpoint: 0.0.0.0:4317}, http: {}}
processors:
memory_limiter: # BACKPRESSURE: drop rather than OOM
check_interval: 1s
limit_percentage: 80
batch: # amortise export overhead
timeout: 5s
send_batch_size: 8192
attributes/redact: # strip PII BEFORE it leaves your network
actions:
- {key: user.email, action: delete}
- {key: http.request.header.authorization, action: delete}
tail_sampling: # see below
policies:
- {name: errors, type: status_code, status_code: {status_codes: [ERROR]}}
- {name: slow, type: latency, latency: {threshold_ms: 500}}
- {name: sample-rest, type: probabilistic, probabilistic: {sampling_percentage: 1}}
exporters:
otlphttp/traces: {endpoint: https://tempo:4318}
prometheusremotewrite: {endpoint: https://mimir/api/v1/push}
service:
pipelines:
traces: {receivers: [otlp], processors: [memory_limiter, tail_sampling, batch], exporters: [otlphttp/traces]}
metrics: {receivers: [otlp], processors: [memory_limiter, batch], exporters: [prometheusremotewrite]}
memory_limiter first is not optional: without it the Collector under a traffic spike
buffers until it OOMs, taking down the telemetry pipeline exactly when you need it. It applies
backpressure by refusing data, which is the correct failure mode for an observability system.
attributes/redact before the exporter is where PII leaves your control. A trace attribute
carrying an email or a token is a data-exposure incident once it reaches a third-party backend,
and redaction at the Collector is the single enforcement point.
Head sampling versus tail sampling
The distinction that decides whether you keep the interesting traces:
HEAD sampling: decide at the START of the trace, before you know anything.
"keep 1% of all traces."
-> cheap, stateless, and it throws away errors and slow
requests at the same 1% rate as everything else.
TAIL sampling: decide at the END, having seen the whole trace.
"keep 100% of errors, 100% of slow traces, 1% of the rest."
-> keeps what matters, and requires BUFFERING every trace
until it completes, which is memory and a stateful Collector.
Head sampling's flaw is that it is blind: it decides before it knows whether the request errored or was slow, so a 1 percent head sample keeps 1 percent of your errors, which is useless for debugging a rare failure.
Tail sampling keeps the interesting traces and costs a stateful buffer:
The tail-sampling trade:
buffer window: ~10-30 s (a trace must complete within it)
memory: proportional to in-flight traces x trace size
the catch: all spans of a trace must reach the SAME Collector
instance, so you need a load-balancing exporter that
routes by trace ID.
The trace-ID routing requirement is the operational cost of tail sampling and the reason it is deployed as a two-tier Collector (a first tier routing by trace ID to a second tier that buffers and samples). Head sampling is stateless and blind; tail sampling is stateful and smart, and for anything where errors are rare, tail is worth the complexity.
Exemplars: the metric-to-trace bridge
An exemplar attaches a trace ID to a metric data point, so a spike on a latency histogram links directly to an example trace of a slow request.
histogram bucket [500ms, 1s]: count 42, exemplar: trace_id=abc...
# The p99 is high. Click the exemplar on the graph -> the trace that
# was in that bucket -> the span that was slow -> the log lines for it.
histogram_quantile(0.99, sum(rate(http_duration_bucket[5m])) by (le))
Exemplars are what make the metric-first debugging workflow work: you alert on a metric, see the spike, and jump to a representative trace without knowing in advance which request to look at. Without exemplars, a metric spike is a signal with no example, and you are back to grepping. It is a small feature with a large effect on time-to-diagnosis.
Structured logs and trace correlation
{ "timestamp": "2026-08-04T09:12:44Z", "level": "error",
"msg": "payment declined", "trace_id": "abc123", "span_id": "def456",
"customer_id": "4471", "amount_cents": 4250, "decline_code": "insufficient_funds" }
The trace_id in every log line is the correlation key. A log without it is an island; a
log with it is a node in a trace, so an error log links to the trace that produced it and the
metric that counted it. Structured logs (key-value, not a formatted string) are the
prerequisite, because you cannot correlate or query a string.
Retention tiers, because full-fidelity logs are expensive:
Hot (7 days): everything, indexed, fast query
Warm (30 days): sampled, or indexed on fewer fields
Cold (1 year): object storage, queryable slowly, for compliance
Wide events and observability 2.0
The observability 2.0 argument: stop emitting three signal types and emit one wide event per unit of work.
Instead of:
a counter increment, a log line, and a span, each with a subset of context,
Emit ONE wide event per request:
{ trace_id, span_id, duration_ms, http_method, http_route, http_status,
customer_id, customer_tier, region, api_version, feature_flags: [...],
db_queries: 4, db_time_ms: 12, cache_hits: 2, cache_misses: 1,
upstream_calls: [...], error: null, ... 50+ dimensions }
The point is high cardinality on purpose. A wide event carries every dimension you might want to slice by, so "is the latency concentrated in one customer on one API version behind one feature flag" is a query, not a re-instrumentation. Metrics, traces and logs are then derived views of the same events rather than separately-instrumented signals.
The cost is cardinality, which is the metrics cost model (see cardinality):
a metric with customer_id as a label explodes into a series per customer. Wide events push
that cardinality into a columnar event store (Honeycomb, ClickHouse) rather than a
time-series database, because the time-series model cannot hold it. That is the architectural
commitment observability 2.0 asks for, and it is why it is a store choice, not a config flag.
The honest position: observability 2.0 is genuinely better for debugging novel problems and it requires a different storage engine. For a team on Prometheus and Grafana, adopting it is a migration, not a setting, and the argument is strongest for services whose failures are unpredictable and whose questions are not known in advance.
Continuous profiling: the fourth signal
Metrics, traces and logs tell you that a service is slow and where in the call graph. Profiling tells you which line of code, continuously, in production.
A service's p99 rose. The trace shows the time is in one span.
The profile shows that span is 60% in JSON serialisation, on one type.
Continuous profiling (Pyroscope, Parca, Polar Signals) runs at roughly 1 percent overhead, sampling stacks continuously, so a regression is a diff between two time windows' flame graphs rather than a reproduction. It is the same wall-clock-versus-CPU distinction as the JVM profiling page: CPU profiles for compute, and wall-clock for latency.
Its place in the pipeline is the last-mile drill-down: metric spike (exemplar) → trace (slow span) → profile (slow line). Each signal narrows the search by an order of magnitude, and profiling is the one that ends at code.
A worked example: a debugging workflow that took 3 hours and then 90 seconds
A payments platform. About 200 services, Prometheus plus Jaeger, head sampling at 1 percent, no exemplars, unstructured logs.
The incident, before the changes:
alert: checkout p99 latency > 800 ms
investigation:
1. see the metric spike (5 min)
2. no exemplar, so no example trace. Which request?
3. grep logs for slow checkouts (unstructured, no trace_id): 40 min
4. find a few slow request IDs; search Jaeger for them
5. head sampling kept 1% -> the slow requests were mostly not sampled
6. eventually find one slow trace, see the time is in a downstream call
7. that service's logs, again unstructured: another 40 min
total: ~3 hours to identify a slow downstream dependency
Head sampling had discarded 99 percent of the slow traces, so the specific requests that were slow were mostly not captured, and the unstructured logs could not be correlated to the few that were.
The changes:
1. Tail sampling, keeping errors and slow traces.
tail_sampling:
policies:
- {name: errors, type: status_code, status_code: {status_codes: [ERROR]}}
- {name: slow, type: latency, latency: {threshold_ms: 500}}
- {name: baseline, type: probabilistic, probabilistic: {sampling_percentage: 1}}
traces kept: 1% of all -> 100% of errors + 100% of slow + 1% baseline
storage: +8% (slow and error traces are a small fraction)
slow-trace capture: ~1% -> 100%
Storage went up 8 percent and slow-trace capture went from 1 percent to 100 percent, because slow and error traces are a small fraction of total volume. This is tail sampling's whole argument: you keep the interesting traces at full fidelity for a small storage increase.
2. Exemplars on the latency histograms.
alert -> click the exemplar on the p99 graph -> the exact slow trace,
no grepping.
3. Structured logs with trace_id.
every log line: JSON, with trace_id and span_id.
-> from a trace span, one click to its logs; from a log, one click to
its trace.
The same incident, after:
alert: checkout p99 > 800 ms
1. click the exemplar on the graph -> the slow trace (10 s)
2. the trace shows the time in the fraud-check span (5 s)
3. click the span's logs -> "fraud model cold, reloading" (10 s)
4. the fraud service had just deployed and was cold
total: ~90 seconds
Three hours to ninety seconds, because each signal linked to the next: metric spike → exemplar → trace → span → correlated logs. The individual improvements were modest; the correlation between them was the transformation.
4. Later: a wide-events pilot on the checkout service, because a recurring question was "is this latency concentrated in one customer segment."
Prometheus with customer_id as a label: cardinality explosion, refused.
Wide events in ClickHouse: the query is
SELECT quantile(0.99)(duration_ms) FROM events
WHERE service = 'checkout' GROUP BY customer_tier, api_version
-> answered in one query, sliceable by ANY of 50 dimensions
The question that Prometheus could not hold as a metric was a routine query over wide events, which is the observability 2.0 argument in one example: cardinality that breaks a time-series database is the normal case for an event store.
5. Continuous profiling on the three highest-traffic services.
a p99 regression after a deploy:
before: reproduce locally, profile, guess. Hours to a day.
after: diff the flame graph between the two deploys. The regression
was a regex compiled per request. ~10 minutes.
Final:
before after
mean time to identify a
slow component ~3 hours ~90 s
slow-trace capture rate ~1% 100%
trace storage baseline +8%
"which customer segment" not askable a query (wide events pilot)
p99 regression diagnosis hours-day ~10 min (profiling)
The transferable finding: observability value is in the correlation, not the signals. The team had metrics, traces and logs before, and each was an island. Exemplars, trace IDs in logs, and tail sampling to keep the interesting traces are what turned three separate systems into one investigation, and that was worth more than any individual backend.
Production evidence
The OpenTelemetry Collector is the CNCF-standard pipeline, and its receiver-processor- exporter model is what makes the backend a configuration choice. Its adoption as the vendor- neutral standard is why "instrument once, route anywhere" is now realistic.
Tail sampling with trace-ID-aware load balancing is documented in the Collector's
tailsamplingprocessor and loadbalancingexporter, and the two-tier deployment is the
reference pattern for it. The requirement that all spans of a trace reach the same instance is
the operational cost the docs are explicit about.
Exemplars are part of OpenMetrics and Prometheus (native histograms carry them), and Grafana's exemplar support is what makes the click-through-to-trace workflow real. The feature is small and its effect on time-to-diagnosis is the reason it exists.
Charity Majors and Honeycomb's "observability 2.0" framing (one wide event per request, derive everything from it, store in a columnar event database) is the reference argument, and Honeycomb, ClickHouse-based stacks, and Grafana's exploration of the model are the production expressions. The cardinality argument is the crux and it is why the store differs from a TSDB.
Continuous profiling (Google's Google-Wide Profiling paper, then Pyroscope, Parca and Polar Signals) established profiling as an always-on production signal at roughly 1 percent overhead, and the flame-graph-diff workflow for regressions is its defining use.
Grafana's LGTM stack (Loki, Grafana, Tempo, Mimir) and the exemplar-and-trace-ID correlation between them is the clearest production instance of the "correlation over signals" argument, because it is built around jumping between the three.
The debate
Head or tail sampling? Tail, for anything where errors and slow requests are rare and are what you debug, because head sampling keeps them at the same low rate as everything else. The cost is a stateful, trace-ID-routing Collector tier, and head sampling remains right where volume is so high that even buffering is infeasible or where all traces are equally interesting (rare). The default should be tail with error-and-latency policies.
Is observability 2.0 worth adopting? For a team whose failures are novel and whose questions are not predictable in advance, the wide-event model is genuinely better, and it requires a columnar event store rather than a TSDB. It is a migration, not a config change, so the honest answer is to pilot it on the service where "slice by an arbitrary dimension" is a recurring need, rather than rearchitecting everything. The cardinality argument is the deciding one: if your important questions have high-cardinality dimensions, the TSDB cannot hold them.
Are three pillars obsolete? Not obsolete, but the framing is limiting. The three signals are still what you store; the shift is treating them as derived views of events rather than separately-instrumented systems, and instrumenting the wide event once. The practical middle ground most teams occupy is three correlated signals (exemplars, trace IDs in logs), which is most of the value of 2.0 without the store migration.
Is continuous profiling worth the overhead? At roughly 1 percent, yes, for high-traffic
services, because it turns a p99-regression investigation from a reproduction into a flame-graph
diff. The constraint is the same as any profiling in a container: it needs perf_events
access, which is a node-level decision. For low-traffic services the value is smaller and
on-demand profiling suffices.
What is the single highest-value change for a team with three uncorrelated signals? Trace IDs in every log line and exemplars on the key metrics, because they turn separate systems into one investigation for very little effort. In the worked example that was the three-hours-to- ninety-seconds change, and it required no new backend.
Follow-up Q&A
"Head sampling or tail sampling?"
Tail, when errors and slow requests are rare and are what you debug, because head sampling decides before it knows whether a request errored, so a 1 percent head sample keeps 1 percent of your errors, which is useless for a rare failure. Tail decides after seeing the whole trace, so you keep 100 percent of errors and slow traces and 1 percent of the rest. The cost is a stateful Collector that buffers until traces complete and routes all spans of a trace to the same instance by trace ID, which is a two-tier deployment.
"What is an exemplar and why does it matter?"
A trace ID attached to a metric data point, so a spike on a latency histogram links to an example trace of a slow request. It matters because it enables the metric-first debugging workflow: alert on the metric, see the spike, click through to a representative trace without knowing in advance which request to look at. Without it, a metric spike is a signal with no example and you are back to grepping logs for slow request IDs, which in one case was 40 minutes of a three-hour investigation.
"What is the observability 2.0 argument?"
Stop instrumenting three signal types and emit one wide event per unit of work, carrying every dimension you might slice by (customer, tier, region, API version, feature flags, query counts), and derive metrics, traces and logs from it. The point is high cardinality on purpose, so "is the latency concentrated in one customer on one API version" is a query rather than a re-instrumentation. The cost is that this cardinality breaks a time-series database, so it needs a columnar event store, which makes it a migration rather than a config change.
"How do metrics, traces, logs and profiles fit together?"
As a drill-down where each narrows the search by an order of magnitude. A metric spike (with an exemplar) links to a trace; the trace shows which span is slow; the span's logs (correlated by trace ID) show the error; and a profile shows which line of code. In one case that chain took a three-hour investigation to ninety seconds, because the value was the correlation between the signals rather than any one of them.
"Is continuous profiling worth it?"
For high-traffic services, yes, at roughly 1 percent overhead. It turns a p99-regression
investigation from a local reproduction into a diff between two time windows' flame graphs: in
one case a regression was a regex compiled per request, found in ten minutes by diffing flame
graphs across two deploys rather than hours of reproduction. It needs perf_events access,
which is a node-level decision, and for low-traffic services on-demand profiling is enough.
Common misconceptions
"1 percent sampling is fine." Head sampling at 1 percent keeps 1 percent of your errors and slow requests, which are exactly what you debug. Tail sampling keeps those at 100 percent for a small storage increase.
"Observability is metrics, logs, and traces." That framing treats them as separate systems. The value is their correlation, and observability 2.0 treats them as derived views of one wide event.
"More dimensions means a bigger metrics bill." In a time-series database, yes, because each label combination is a series. Wide events push that cardinality into a columnar store designed for it, which is the architectural point.
"Exemplars are a minor feature." They are the bridge from a metric spike to an example trace, which is the difference between a diagnosable spike and a dead end. Small feature, large effect.
"Profiling is for development." Continuous profiling runs in production at about 1 percent overhead and turns regression diagnosis into a flame-graph diff.
Interview delivery note
Say this verbatim: "The value of observability is in the correlation, not the signals. In one case an investigation went from three hours to ninety seconds, and the change was exemplars on the metrics, trace IDs in the logs, and tail sampling to keep the slow traces, so a metric spike linked to a trace linked to the exact log lines. Each signal narrowed the search by an order of magnitude." The drill-down chain and a concrete before-and-after.
The senior-versus-staff separator is head-versus-tail sampling with the blindness argument. A senior engineer knows sampling reduces cost. A staff engineer explains that head sampling decides before it knows whether a request errored, so it keeps errors at the same low rate as everything else and is useless for rare failures, that tail sampling keeps errors and slow traces at full fidelity for a small storage increase, and that the cost is a stateful trace-ID-routing Collector tier. Knowing why head sampling fails is the depth signal.
The second signal is the observability 2.0 cardinality argument stated as a storage decision. "High-cardinality dimensions break a time-series database, so wide events need a columnar event store, which makes 2.0 a migration rather than a config flag" shows you understand why the framing is a real architectural choice rather than a fashion.
Further reading
- The OpenTelemetry Collector documentation, particularly the tail-sampling processor and the trace-ID load-balancing exporter.
- Charity Majors, Liz Fong-Jones and George Miranda, Observability Engineering, for the wide- event / observability 2.0 argument.
- The OpenMetrics and Prometheus exemplar specifications, and Grafana's exemplar-to-trace workflow.
- The Google-Wide Profiling paper and Pyroscope/Parca documentation, for continuous profiling as a production signal.