Design centralised logging
The interviewer asks
“Design a centralised logging platform for a company running about 3,000 microservice instances on Kubernetes. Engineers must be able to search any service's logs from the last few minutes to the last 30 days, and the platform must keep working during incidents, when log volume spikes.”
What a strong answer looks like
A strong answer treats logging as a write-heavy pipeline with a rare, bursty read path. Agents on every node (Fluent Bit, Vector or the OTel Collector) tail container stdout, add Kubernetes metadata, redact PII and ship batches to Kafka, which absorbs spikes and decouples producers from storage. Indexer consumers write to a store chosen on purpose: a label-indexed, chunk-in-object-storage design (Loki-style) or a columnar store (ClickHouse) is 5-10x cheaper than full-text indexing everything in Elasticsearch. Retention is tiered: hot (SSD, 3-7 days), warm/cold in S3 for 30-90 days. Then cover the hard parts: volume and cost control (sampling, per-team quotas, dropping debug logs), structured logs with trace_id, multi-tenant query isolation, and what happens when the pipeline itself is overloaded during an outage. Senior candidates say logs are the most expensive signal and push teams to metrics and traces for anything aggregate.
Concepts to mention
1 Requirements
Functional
- Collect stdout/stderr and file logs from every container and VM automatically, with no code change
- Enrich each line with service, namespace, pod, node, region, version and trace_id when present
- Search by time range, service, level, free text and structured fields (e.g. order_id=123)
- Live tail of a service's logs with a few seconds of delay
- Retention per team and per log class: 7 days hot, 30 days searchable, 1 year archive for audit-relevant streams
- Redact PII (phone, email, card, Aadhaar-like IDs, auth tokens) before logs are stored
- Alerts on log patterns (e.g. more than 100 'payment timeout' errors in 5 minutes)
Non-functional
- Ingest ~5 TB/day normally, survive 3-5x spikes during incidents without losing logs
- End-to-end delay from log line to searchable: p95 under 30 seconds
- Search over the last 1 hour for one service: p95 under 3 seconds; 7-day searches under 30 seconds
- Logging must never slow or crash the application (agent CPU under ~5% of a node, bounded memory)
- Platform availability 99.9%; ingestion durability 99.99% once a line reaches Kafka
- Cost target: well under 10% of total infra spend
Out of scope (say it out loud)
- Metrics and dashboards (separate metrics system)
- Distributed tracing storage (separate design)
- Tamper-proof audit trails for compliance (separate audit log system)
- SIEM / security threat detection
2 Estimates
3 API
| Endpoint | What it does |
|---|---|
POST/v1/logs:push (OTLP/HTTP or gRPC) | Agents push compressed batches; returns 429 when the tenant is over quota so the agent buffers or drops by priority. |
GET/v1/query?q={service="checkout",level="error"} |= "timeout"&start=&end=&limit= | Search: label selector narrows streams, then a filter scans chunks. Returns lines plus stats (bytes scanned). |
WS/v1/tail?q=... | Live tail; server pushes matching lines as they arrive, with a rate cap per connection. |
PUT/v1/tenants/{team}/limits | Set ingest rate, retention and max query range per team. |
POST/v1/alerts/rules | Log-pattern alert rule evaluated every minute by the ruler. |
4 Data model
log_chunksS3 (object storage)object key = tenant/stream_hash/from_ts-to_ts-checksum; body = compressed lines (ts, line, structured metadata)Chunks of ~1-2 MB compressed per stream. Object storage is cheap and durable; the price is higher read latency, hidden by parallel fetches and caching.
stream_indexBoltDB/TSDB index files in S3 (or Cassandra/DynamoDB)label set hash → list of chunk refs per day: {chunk_keyfromto}Index only low-cardinality labels (service, namespace, level, region). Never index user_id or order_id as labels: every unique value creates a new stream.
ingest bufferKafkatopic logs.rawkey = tenant + stream hashvalue = batch of linesPartition by stream so one stream's lines stay ordered; 7-24 h retention gives replay after an indexer bug.
recent chunksIngester memory + WAL on local SSDopen chunk per streamflushed after 1-2 MB or 5-30 minWAL replays after a crash; replication factor 3 across ingesters before ack to Kafka offset commit.
tenant_limitsPostgres + config mapteam_id (PK)ingest_mb_sburst_mbretention_daysmax_query_daysmax_bytes_scannedRead by distributors and queriers; small and rarely changed.
5 Architecture
Hover a box to highlight its connections.
6 Request flows
A log line from a pod to storage
- 1The checkout service writes a JSON line to stdout: {level: error, msg: 'PSP timeout', order_id: 123, trace_id: abc}.
- 2The container runtime writes it to /var/log/pods/...; the Fluent Bit DaemonSet tails the file and tracks its offset in a local DB so restarts do not resend or skip.
- 3The agent parses JSON, adds Kubernetes labels (namespace, app, pod, node), runs redaction filters, and batches lines (e.g. 1 MB or 1 s).
- 4The batch goes to the distributor, which authenticates the team, checks the per-team ingest rate, and validates labels (rejects high-cardinality labels).
- 5The distributor produces to Kafka keyed by tenant + stream hash and acks the agent; if Kafka is slow the agent buffers on local disk (bounded, e.g. 2 GB).
- 6Ingesters consume, append to the open chunk for that stream and to a local WAL; recent data is queryable from memory at this point (seconds).
- 7When a chunk reaches ~1.5 MB compressed or 15 minutes, it is flushed to S3 and the index entry is written; then the Kafka offset is committed.
- 8The compactor later merges small index files and deletes chunks past each tenant's retention.
Search during an incident
- 1An on-call engineer queries {app="checkout", level="error"} |= "timeout" for the last 6 hours.
- 2The query frontend splits it into 24 15-minute sub-queries, checks the results cache for completed intervals, and queues the rest fairly per tenant.
- 3Queriers look up the stream index for matching label sets and get chunk references for each interval.
- 4The last ~30 minutes are read from ingesters' memory; older chunks are fetched from S3 in parallel (chunk cache first) and scanned with the line filter.
- 5Partial results are merged and sorted by time; the frontend returns the newest 1,000 lines plus bytes-scanned stats.
- 6The engineer clicks a trace_id and jumps to the trace view for the same request.
7 Deep dives
Index everything vs index labels only
There are two families of log stores. Elasticsearch/OpenSearch builds an inverted index over every word in every line. Any free-text search is fast, but the index is often as large as the raw data, it lives on SSD, and indexing burns CPU. At 5 TB/day that means roughly 5-7 TB/day of hot SSD storage plus replicas, which is very expensive. The other family (Grafana Loki, and also columnar designs on ClickHouse) indexes only a few low-cardinality labels such as service, namespace, level and region. The log text is stored as compressed chunks in S3. A query first selects streams by label, then brute-force scans those chunks in parallel. Storage is 5-10x cheaper and ingestion is simple; the cost moves to query time, which is fine because 95% of queries are 'this service, this hour, contains X'. ClickHouse sits between the two: columnar storage with good compression, fast scans, and optional token bloom-filter skip indexes for text. A good answer picks label-indexed or columnar storage for the bulk of logs, and keeps full-text indexing only for small, high-value streams (for example security logs) where needle-in-haystack search across all services matters.
Surviving the incident spike
Log volume is highest exactly when the system is failing: services log every retry and stack trace, and engineers run many queries at once. A logging pipeline that falls over during an outage is worse than useless. The design defends in layers. First, the agent has a bounded memory buffer plus a bounded disk buffer; when both fill, it drops by priority (debug first, then info, keep warn and error) and counts what it dropped, instead of blocking the application's stdout. Second, the distributor enforces per-team ingest rates with a token bucket and returns 429, so one team's crash loop logging 50 MB/s cannot starve everyone. Third, Kafka sits between ingestion and indexing with hours of retention, so if ingesters or S3 are slow, lag grows but nothing is lost; ingesters catch up later. Fourth, on the read side, the query frontend splits and queues queries per tenant with limits on bytes scanned and concurrency, so one engineer's 30-day regex over all services does not block the on-call queries. Finally, services should rate-limit their own repeated logs (log the first 10 identical errors per minute, then a counter).
Cardinality, structure and cost control
The biggest operational mistake in label-indexed stores is high-cardinality labels. If a team adds user_id as a label, every user creates a new stream: millions of tiny chunks, a huge index and slow queries. The distributor should reject labels outside an allow-list and cap streams per tenant (e.g. 10,000 active streams). High-cardinality values belong inside the line as structured fields and are filtered at query time (| json | order_id="123"). Structured JSON logs with consistent field names (ts, level, service, trace_id, msg) make parsing cheap and enable jumping to traces. Cost control is a product feature, not an afterthought: show each team its GB/day and cost in a dashboard, make debug logging off by default in production, sample noisy success logs (keep 1% of 200 OK access logs, 100% of errors), and set retention by class. Many companies find 20-30% of log volume comes from a handful of noisy services; fixing them is cheaper than any storage optimisation.
8 Trade-offs
Kafka in the middle vs direct write to the store
Agents can push straight to ingesters (simpler, fewer parts, lower delay). Kafka adds cost and one more system to run, but gives hours of buffering, replay after an indexer bug, and the ability to feed other consumers (security SIEM, anomaly detection) from the same stream. At 5 TB/day with strict 'never lose logs during an incident' requirements, Kafka is worth it; a startup with 50 services can skip it.
Agent choice: Fluent Bit vs Vector vs OTel Collector
Fluent Bit is small (a few MB of memory) and battle-tested as a DaemonSet. Vector has a strong transform language and good performance. The OpenTelemetry Collector unifies logs, metrics and traces and the OTLP format, which reduces vendor lock-in. Any is fine; what matters is bounded buffers, disk spooling, offset tracking and redaction at the edge.
Self-hosted vs managed (Datadog, Elastic Cloud, CloudWatch)
Managed tools are fast to adopt but are usually priced per GB ingested and indexed; at terabytes per day the bill becomes one of the largest infra lines. Self-hosted Loki or ClickHouse on S3 is far cheaper per GB but needs a platform team. Data-localisation rules (RBI for payment data in India) can also force logs containing payment details to stay in Indian regions.
9 Bottlenecks
Hot stream on one ingester
A single chatty service (e.g. API gateway access logs at 30 MB/s) maps to one stream and one ingester. Shard it: add a synthetic shard label (stream_shard=0..15) at the distributor for streams above a rate threshold, spreading the load; queries fan out over shards automatically.
Long-range queries scanning terabytes
A 30-day regex over all services can scan 20 TB. Enforce max bytes scanned per query, require at least one label selector, cache results per time split, and offer pre-computed metrics (log-derived counters by the ruler) for trend questions instead of raw scans.
Small-object explosion in S3
Many low-volume streams flush tiny chunks, raising S3 PUT cost and query fan-out. Use a max chunk age with a minimum size, and let the compactor merge index files daily.
10 Failure handling
Kafka or storage outage
Agents keep buffering to local disk (bounded) and retry with backoff; if Kafka is down for longer than the buffer, they drop lowest-priority logs first and emit a 'logs dropped' metric. If S3 is down, ingesters keep data in memory and WAL and stop committing offsets, so Kafka holds the backlog; once S3 recovers they flush and catch up.
Ingester crash
Each ingester replays its WAL on restart. With replication factor 3 (or because offsets are committed only after flush), no acknowledged data is lost; queries for very recent data may briefly miss the crashed ingester's in-memory lines.
Agent misconfiguration floods the pipeline
A bad deploy turns on debug logging everywhere and volume jumps 10x. Per-team rate limits contain it to that team; alerts fire on per-team ingest rate; the team's own logs are throttled rather than everyone's.
11 What impresses
- Does the storage math and shows why indexing every word at 5 TB/day is the expensive choice
- Designs for the incident spike explicitly: the logging system must stay up when everything else is failing
- Bans high-cardinality labels and explains what goes wrong (stream explosion)
- Redacts PII in the agent, before logs leave the node, and mentions tokens in Authorization headers
- Treats cost as a product feature: per-team usage dashboards, sampling of success logs, retention classes
- Points out that aggregate questions should be metrics, not log scans
12 Mistakes
- Sending logs synchronously from application code to a remote store, so a slow log backend slows the app
- Putting everything into one big Elasticsearch cluster without doing the storage and cost math
- No backpressure or per-team limits, so one crash-looping service takes down logging for everyone
- Using user_id or request_id as an index label
- Ignoring PII and secrets in logs
- No retention policy: storing everything forever on hot storage
13 Follow-ups
Lessons this design uses
Practice questions
- A customer support agent gives you an order ID and says the order failed yesterday. It passed through 6 microservices. How should logging be designed so you can find what happened in two minutes?
- Your company now ingests 10 TB of logs per day into Elasticsearch and the observability bill is larger than the database bill. How would you bring cost down without losing the ability to debug?
- Your Elasticsearch cluster has 2,000 shards for 300 GB of data, and search latency is high. Another team has one index with 3 shards of 200 GB each. What is wrong in each case and how do you size shards?
- Your company ingests 5 TB of application logs per day into Elasticsearch, and the bill is growing fast. Engineers mostly search the last 2 days by service and trace id. What storage design would you propose?
Saved on this device. Create a free account to keep it on every device.
Practise this with Sam
A real mock interview, out loud, with an honest scorecard. Free to start.