Designing a Unified Observability Platform

A system design interview guide to unifying the three pillars — metrics, logs, and traces — into one platform that ingests, correlates, and queries telemetry across a fleet, replacing a pile of fragmented legacy monitoring tools.

Student edition · start with the guarantee

Unify metrics, logs, and traces through shared context while budgeting storage, coverage, and query isolation.

Correctness contract: Every signal has an explicit loss/retention policy. Correlation is configured; sampling/redaction and access controls apply before any exposed or retained sensitive copy.

Basic HTTP, databases, and programming are assumed. Specialized terms are explained below. Named products motivate the problem; this is an educational design, not a claim about their current internals. Capacity numbers are illustrative decimal payload estimates, not benchmarks. Add indexes, protocol overhead, replicas, headroom, and failure capacity before provisioning.

Most organizations grow their monitoring by accretion: a metrics system here, a log aggregator there, a tracing tool bolted on later, each with its own agents, storage, query language, and dashboards. The result is that during an incident an engineer hops between three tools that cannot talk to each other, manually copying timestamps and IDs to correlate what happened. A unified observability platform fixes this by collecting all three signal types through one pipeline, storing each in a backend suited to its shape, and — the key move — stitching them together with shared identifiers so you can pivot from a metric spike to the exact trace to the exact logs in a couple of clicks. This guide builds that platform: the pillars and their data model, a single collection layer built on OpenTelemetry, per-signal storage, the correlation glue, and the hard problems of high cardinality, multi-tenancy, and cost.

Contents

  1. Requirements & Scope
  2. Back-of-the-Envelope Estimates
  3. The Three Pillars & Data Model
  4. API & Data Model
  5. High-Level Architecture
  6. Collection & OpenTelemetry Ingest
  7. Storage Per Signal
  8. Correlation via Trace IDs & Exemplars
  9. Cardinality, Multi-Tenancy & Cost
  10. Failure Modes & Tradeoffs
  11. 45-Minute Interview Walkthrough
  12. Common Mistakes
  13. Summary
  14. Check Yourself
  15. Key Terms
  16. Further Reading
  17. Concepts to understand first
  18. Worked example
  19. Runnable Python model

Concepts to understand first

Each definition includes a concrete example and the condition under which the mechanism is useful. You can read this guide without opening another guide.

Spans and trace context

A span records one operation with a trace ID, span ID, parent or links, timing, and attributes. Context propagation sends the current trace/span identity across HTTP, RPC, thread, and queue boundaries so related work can be assembled.

Example. A gateway starts a trace, injects W3C traceparent into a service call, and the service links its queue consumer work to the producer span.

Limit. Cross-host wall clocks can skew timelines. A parent-child graph is not always a tree because asynchronous work can have multiple links. Instrumentation must be configured; IDs are not automatically present in every log.

Head and tail sampling

Head sampling decides at trace start. Tail sampling buffers received spans and decides after a configured decision interval using observed errors/latency. Sampling reduces retained volume but introduces coverage and bias choices.

Example. A 1% head sampler forwards only one in 100 traces. A later tail sampler cannot recover an error trace that the head stage never sent.

Limit. No tail policy guarantees all errors when input spans are lost, dropped, late, or evicted. Route compatible trace state together, bound memory, and report coverage before drawing aggregate conclusions.

Cardinality

The number of distinct identities a store must track. A metric series is its metric name plus the full label combination. The product of label-value counts is an upper bound when not every combination occurs.

Example. 100 hosts × 20 routes × 5 status codes can produce up to 10,000 series. Adding a unique request_id can make every request create a new series.

Limit. Hashing every ID to a distinct digest does not reduce cardinality or guarantee anonymity. Use bounded categories, explicit limits, or intentional fixed-bucket aggregation.

Inverted indexes and columnar storage

An inverted index maps searchable terms to document postings. A columnar file groups values by column so analytical scans can read only selected fields. They optimize different access patterns.

Example. 'ERROR' → document IDs {2,7}; 'service=api' → {1,2,7}. Intersection returns {2,7}. A monthly count may instead scan only time, service, and status columns.

Limit. Tokenization affects meaning, indexes have storage/write cost, and incomplete shard responses must be marked. Search refresh is visibility, not the same as durable commit.

Rollups and weighted aggregation

A precomputed summary per time bucket. Sums and counts merge by addition, min/max by comparison. An average is derived as total sum divided by total count, not an unweighted average of bucket averages.

Example. One window has [10], another [20,20,20]. Merge sum=70 and count=4 to get 17.5, not the unweighted mean of window means (10+20)/2=15.

Limit. Do not sum cumulative counter snapshots to compute traffic, or average p99 values to get a global p99. Retain reset-aware counter increases or compatible histogram distributions.

Backpressure and load shedding

Backpressure tells an upstream producer that the downstream stage cannot accept more work. Buffering delays that signal; shedding deliberately rejects or drops lower-priority work when a bound is reached.

Example. A sink accepts 100 events/s while input is 150/s. A 1000-event queue fills in 20 seconds. The system must then block, reject, or apply its documented drop policy.

Limit. A finite queue cannot absorb an infinite outage. Prioritized messages still need hard quotas, deadlines, and bounded client queues; otherwise an exemption can exhaust the system.

SLIs, SLOs, and error budgets

An SLI measures a user-relevant outcome; an SLO sets its target over a window. For a success-ratio SLO, the allowed bad fraction is 1−SLO. An SLA is a contractual commitment that may use different definitions.

Example. 99.9% successful requests over 1 billion requests allows 1 million bad requests. A time-based 30-day availability target instead allows 43.2 unavailable minutes.

Limit. Request and time budgets are not interchangeable under variable traffic or partial failure. SLOs are not universally stricter than SLAs; choose both from their definitions and user needs.

1. Requirements & Scope

Intuition

Think of a hospital. Metrics are the heart-rate monitor: one number, always on, cheap. Logs are the nurse’s notes: rich detail, lots of paper. Traces are the patient’s path through every ward. A good hospital links all three with one patient ID. An observability platform does the same with a trace_id.

Clarifying questions to ask

The goal is one platform that answers the three questions an operator asks during an incident: is something wrong? (metrics), what exactly happened? (logs), and where in the request path? (traces). Scope it as functional and non-functional requirements.

Interview tip: The single sentence that shows seniority here is “the three pillars are not three products — they are three views of the same events, and the platform’s value is the correlation between them.” Anyone can list metrics, logs, and traces; the unification is the design.

2. Back-of-the-Envelope Estimates

Sizing tells you where the money and the engineering go. Assume a mid-large fleet of 10,000 hosts running 50,000 service instances, serving 1,000,000 requests/second at peak.

SignalEstimateVolume math
Metrics~20M active series; ~20M samples/s at 1s scrape50k instances × ~400 series each ≈ 20M series; each emitted per second.
Metrics storage~3.5 TB/day compressed (~230 GB/day at a 15 s scrape)20M samples/s × 86,400s × ~2 bytes/sample (Gorilla-style) ≈ 3.5×1012 B. Most fleets scrape every 15 s, which cuts this 15×; downsampled tiers keep years cheaply.
Logs~10M lines/s; ~430 TB/day raw1M req/s × ~10 log lines/request × ~500 bytes = 5 GB/s; × 86,400s ≈ 432 TB/day before compression (~45–90 TB/day after 5–10× compression).
Traces (spans)~10M spans/s raw; ~100k/s kept1M req/s × ~10 spans/request; sampled at ~1% → ~100k spans/s stored.
Trace storage~9–17 TB/day kept100k spans/s × 86,400s × ~1–2 KB/span ≈ 8.6–17 TB; unsampled it would be 100× more, so sampling is what makes this affordable.

Worked estimate, step by step

StepMathResult
Active series50,000 instances × 400 series20M series
Metric samples20M series ÷ 1 s scrape20M samples/s
Metric bytes/day20M × 86,400 s × 2 B≈ 3.5 TB
Log bytes/s1M req/s × 10 lines × 500 B5 GB/s
Log bytes/day (raw)5 GB/s × 86,400 s≈ 432 TB
Kept spans/day100k spans/s × 86,400 s × 1–2 KB≈ 8.6–17 TB
Raw ingest per day3.5 + 432 + ~13≈ 450 TB/day (logs ≈ 96%)
Read the table

Logs are almost all of the bytes. So log tiering and log compression are where the money goes. Metrics are tiny by comparison.

The estimate immediately explains three design decisions: metrics compress fantastically and are cheap to keep, so they are the always-on layer; logs are the bulk of raw bytes, so tiering and cheap object storage dominate their design; and traces are so voluminous raw that sampling is mandatory, not optional. Each pillar therefore gets a different storage strategy.

3. The Three Pillars & Data Model

Each pillar answers a different question and has a different data shape, which is precisely why one storage engine cannot serve all three well.

PillarShapeAnswersStorage fit
Metricsname + labels → (ts, value)Is it broken? How much?Time-series DB (TSDB).
Logstimestamp + structured fieldsWhat exactly happened?Inverted index and/or columnar.
Tracestree of spans by trace_idWhere in the path?Key-by-trace_id store on object storage.

The unifying thread is context: every signal carries a common set of resource attributes (service, host, region) and, wherever it originates inside a request, the trace_id and span_id. That shared context is what later lets the platform join a metric, a log, and a trace that describe the same moment.

4. API & Data Model

Apps never call the stores directly. They speak OTLP to a collector. Users speak a query API to one front end.

# write path (OTLP over gRPC or HTTP)
POST /v1/metrics   ResourceMetrics{ resource{service,host}, points[ {name, labels, ts, value, exemplar?} ] }
POST /v1/logs      ResourceLogs{ resource, records[ {ts, severity, body, attrs, trace_id?, span_id?} ] }
POST /v1/traces    ResourceSpans{ resource, spans[ {trace_id, span_id, parent_id, name, start, end, attrs, status} ] }

# read path (one query layer over three stores)
GET /api/metrics/query_range?q=<PromQL>&start&end&step
GET /api/logs/search?q=<LogQL/Lucene>&start&end&limit
GET /api/traces/{trace_id}
GET /api/traces/search?service&min_duration&status=error
POST /api/alerts/rules   { expr, for, labels, receivers }

The one field that ties it all together is trace_id. It rides on spans, on log records, and on metric exemplars.

5. High-Level Architecture

The platform is a pipeline: instrument once, collect through one layer, route each signal to its own store, and query and alert over all of them with a correlation layer on top.

Instrumented apps to OTel Collector, fanning out to metrics, log, and trace stores, then a query/correlate layer feeding dashboards and alerting
Applications are instrumented with the OpenTelemetry SDK and emit all three signals to a collector, which receives, enriches, and routes each signal to a purpose-built store — a TSDB for metrics, an inverted-index or columnar store for logs, and an object-storage-backed trace store. A query-and-correlate layer serves dashboards and alerting, and cross-cutting concerns (cardinality, tenancy, cost) apply at every stage.

The design deliberately splits collection from storage from query. Collection is a common front door so applications never know or care which backend stores their telemetry. Storage is specialized per signal because their access patterns differ so sharply. And the query layer sits above all three stores so a single UI can correlate them. This separation is exactly what a fragmented legacy setup lacks — there, each tool owns its own collection, storage, and query, and nothing joins across them.

6. Collection & OpenTelemetry Ingest

OTel SDKs instrument operations and export signals; context propagation and log injection must be configured in supported frameworks or manually. A collector pipeline has signal-specific receivers, processors, and exporters; a compact list here is conceptual, not valid copy-and-run YAML.

Run agents for local collection and gateways for tenant policy/routing and eligible tail sampling. Bound SDK/collector queues, memory, disk, retry, and export time. Decide drop versus backpressure per signal; audit data may need a different durable path. Redact sensitive fields before any retained transport/archive/DLQ required by policy. Tail sampling cannot restore head-discarded or lost traces.

7. Storage Per Signal

Each pillar gets a store matched to its shape. Forcing all three into one engine is the classic mistake that makes a platform either slow or ruinously expensive.

Metrics → time-series database

Metrics are stored in a TSDB optimized for append-heavy writes and range-scan reads over (timestamp, value) pairs. It uses an inverted index on labels to select series, delta-of-delta timestamp and XOR value compression to shrink the data dramatically, and downsampling into coarser resolutions as data ages so years of history stay cheap. (See the companion time-series store guide for the internals.)

Logs → inverted index and/or columnar

Logs need full-text and field search, so they go into an inverted index (Lucene-style, as in Elasticsearch/OpenSearch) for recent data, and increasingly into a columnar format (Parquet on object storage) for older data and analytical scans. Indexing every field is expensive, so a cost-conscious design indexes a few high-value fields and stores the rest as compressed columns queried by scan. (See the companion log search guide.)

Traces → key-by-trace_id store

Spans are written keyed by trace_id so that all spans of a request can be fetched together to reassemble the trace. Because span volume is enormous, the store is backed by cheap object storage with a small index from trace_id (and searchable attributes) to the blocks that hold its spans — the model used by systems like Grafana Tempo. (See the companion distributed tracing guide.)

8. Correlation via Trace IDs & Exemplars

Correlation is the whole point of unifying the pillars, and it works because every signal carries shared context. Two mechanisms tie them together.

Metrics link to traces via exemplars, traces link to logs via trace_id, described as a correlation walk
From a metric spike, an exemplar — a sampled trace_id attached to a metric bucket — jumps you to a representative slow trace; from a span in that trace, the shared trace_id and span_id pivot you to the exact logs. The three pillars become one investigation.

The investigation flow this enables — metric spike → exemplar → slow trace → span logs — is what an interviewer is really testing when they ask you to “unify” observability. Shared, propagated context is the mechanism; a common query layer over all three stores is what surfaces it.

9. Cardinality, Multi-Tenancy & Cost

Metrics cardinality is observed label combinations. Hashing distinct unbounded IDs into distinct digests does not reduce it; hashing can also leave identities reversible through dictionary attacks. Use curated bounded labels, fixed buckets only with intentional semantics, and per-tenant series/churn limits.

Logs/traces also need bounded attributes/indexing cost, retention, authentication, access checks, query fairness, and ingest quotas. Sampling reduces coverage and can bias investigation/aggregates; expose policy and drop rates. Redact before durable brokers/archive/DLQ where the data policy requires it, not merely before one queryable sink.

Collection buffers are finite. Define behavior when memory/disk/export budgets fill, keeping production-request overhead bounded; a collector cannot promise isolation from an unbounded outage. Exemplar/trace-log correlation requires configured instrumentation and retained context, not automatic IDs in every line.

10. Failure Modes & Tradeoffs

FailureWhat happensDefense
A storage backend is slow or downCollectors back up, then drop dataCollector queues with retry; a Kafka buffer before the stores; per-signal isolation so logs failing never blocks metrics
Cardinality explosionTSDB memory and index blow up; queries time outPer-tenant series limits; drop or hash unbounded labels at the collector
One tenant floods ingestOther teams lose data or alertsPer-tenant rate limits and quotas at the gateway
Heavy dashboard queryQuery layer stalls; alerts evaluate lateSeparate alert evaluation from ad-hoc queries; query timeouts and cost limits
The monitoring system itself diesSilence looks like healthMeta-monitoring on a separate stack plus a dead-man’s-switch alert
ChoiceOption AOption B
Log storageFull inverted index: fast search, costlyLabel index + compressed chunks or columnar: cheap, slower grep
Trace samplingHead: cheap, may miss errorsTail: keeps errors and slow traces, needs trace-aware routing and memory
Metric resolution1 s scrape: sharp, 15× the data15–60 s scrape: cheap, may hide short spikes
DeploymentAgent per host: local bufferingCentral gateway only: simpler, one hop more fragile

Worked example: a label hash does not solve cardinality

A service records one million distinct user IDs.

Worked example, in orderCount original labels: A user_id label can create one million distinct series for one metric.. Hash each ID: A cryptographic digest still has nearly one million distinct values. It hides representation, not cardinality, and is not necessarily anonymous.. Choose a bounded dimension: Aggregate by a meaningful bounded category or intentionally map to a fixed bucket count with changed semantics; keep per-user detail in a governed events/log store.. Correlate an investigation: An exemplar can link a histogram observation to a trace only if the SDK/config captured and retained it. Log injection and trace coverage are not automatic.1Count original labels2Hash each ID3Choose a bounded dimension4Correlate an investigation
Read top to bottom. The table below explains the state and guarantee at each step.
StepState, calculation, and reasoning
1. Count original labelsA user_id label can create one million distinct series for one metric.
2. Hash each IDA cryptographic digest still has nearly one million distinct values. It hides representation, not cardinality, and is not necessarily anonymous.
3. Choose a bounded dimensionAggregate by a meaningful bounded category or intentionally map to a fixed bucket count with changed semantics; keep per-user detail in a governed events/log store.
4. Correlate an investigationAn exemplar can link a histogram observation to a trace only if the SDK/config captured and retained it. Log injection and trace coverage are not automatic.
Check your understanding: Does an OTel collector guarantee no application slowdown during any backend outage?

No. Memory/disk queues are finite and exporter SDK policies vary. Choose bounded buffering/drop/backpressure and expose loss.

Run a small, testable model

Download concepts.py. It uses only the Python 3.10+ standard library. From the directory where you saved it, run:

python3 concepts.py observability
python3 concepts.py test

The model checks label-cardinality preservation and sampling limits, then prints SLO budget arithmetic.

The program contains deterministic assertions and failure traces. It is a learning model, not a production service: it does not implement network consensus, distributed durability, authentication, or third-party delivery. Read its contracts and try changing an input before extending it.

Primary references for these concepts

11. 45-Minute Interview Walkthrough

MinutesWhat to do
0–5Ask the clarifying questions. Pin fleet size, signals, retention and tenancy.
5–10Run the estimate. Point out that logs dominate bytes and traces need sampling.
10–20Draw SDK → agent → gateway → three stores → query layer. Name OTLP.
20–30Deep dive one store the interviewer cares about. Metrics TSDB or log tiering is common.
30–38Show correlation: exemplar → trace → logs by trace_id.
38–45Cardinality, multi-tenancy, cost, and who monitors the monitor.

12. Common Mistakes

Watch out

13. Summary

A unified observability platform collects all three signals through one standard pipeline, stores each in a backend matched to its shape, and — the differentiator — correlates them with shared context so investigation is one flow instead of three.

ConcernMechanism
What are the pillars?Metrics (is it broken?), logs (what happened?), traces (where in the path?) — three views of the same events.
How do we instrument once?OpenTelemetry SDK + Collector (receivers → processors → exporters) decouple apps from backends.
Where does each signal live?TSDB for metrics, inverted-index/columnar for logs, key-by-trace_id object store for traces.
How do the pillars connect?Exemplars link metrics → traces; shared trace_id/span_id link traces ↔ logs.
What breaks it at scale?High cardinality — bound labels, drop/hash at ingest, enforce per-series limits.
How do many teams share it?Per-tenant quotas, isolation, and fair query scheduling.
How do we afford it?Tiered retention, downsampling, trace sampling, index only high-value fields.
The recurring theme: unify at the edges (one instrumentation standard, one collection pipeline, one query layer) but specialize in the middle (a store per signal). The value is not the three pillars individually — it is the shared, propagated context that lets you walk from a metric to a trace to a log describing the same event.

14. Check Yourself

  1. Why do logs dominate the storage bill in this design?
    Answer

    Each request makes about 10 lines of 500 B. That is 5 GB/s, or about 432 TB/day raw. Metrics are about 3.5 TB/day and kept traces about 13 TB/day.

  2. What is an exemplar and what problem does it solve?
    Answer

    It is a sample trace_id attached to a metric bucket. A histogram loses per-request detail. The exemplar lets you click from a p99 spike to a real slow trace.

  3. Why does tail sampling need a gateway tier?
    Answer

    The keep-or-drop choice needs every span of a trace. Spans come from many hosts. A gateway routes by trace_id so one node sees the whole trace.

  4. A team adds user_id as a metric label. What breaks, and how do you stop it?
    Answer

    Each user makes a new series. The TSDB index and memory blow up. Stop it with per-tenant series limits and a collector rule that drops or hashes the label.

  5. Why use OpenTelemetry instead of each vendor’s agent?
    Answer

    Apps instrument once against one API and wire format. You can then swap or fan out backends without touching app code.

  6. How do you know the monitoring system itself is healthy?
    Answer

    Run meta-monitoring on a separate stack. Add a dead-man’s-switch alert that fires when the expected heartbeat stops.

15. Key Terms

Metric
A number over time, keyed by name and labels.
Series
One unique name + label set. The unit of TSDB cost.
Cardinality
The number of distinct identities a store must track. A metric series is its metric name plus the full label combination. The product of label-value counts is an upper bound when not every combination occurs. Example and limits above.
Span
A span records one operation with a trace ID, span ID, parent or links, timing, and attributes. Context propagation sends the current trace/span identity across HTTP, RPC, thread, and queue boundaries so related work can be assembled. Example and limits above.
Trace
The tree of spans for one request.
Exemplar
A sample trace_id stored on a metric bucket.
OTLP
OpenTelemetry’s wire protocol for all three signals.
Collector
OTel process: receivers, processors, exporters.
Downsampling
Storing old metrics at coarser resolution.
Dead-man’s switch
An alert that fires when a heartbeat stops.

16. Further Reading


⌂