Courses / System Design Interview / Chapter 19
Module 2 · Building Blocks · Chapter 19 of 39
Distributed Logging
Builds on Monitoring: Metrics, Logs, Traces and Alerts, Message Queues, Blob Stores and Distributed Search. Next up: Distributed Task Schedulers.
Part 1 · Why Logs Need One Home
Forty services, one broken order
It is 8:30 pm on a Friday, the IPL final is halfway through its first innings, and Bengaluru is ordering dinner. A food delivery app starts failing one order in twenty with "payment could not be completed". Each order passes through five of the app's forty services, each running as dozens of copies, and every copy writes its own log file on its own machine. The on-call engineer has three terminals open and is searching files one machine at a time, while autoscaling has already replaced two of the failing machines, taking their logs with them. Central logging turns that hunt into one search.
Logs are timestamped lines describing what a program did. Centralised logging ships every line, soon after it is written, to one place where it is kept for a set time and can be searched in seconds. It is one of the three views from the monitoring chapter: metrics tell you something is wrong, traces show where the time went, and logs record the details of what happened. This chapter builds that pipeline for the food delivery app.
Many people read logs, each for a different reason.
| Who reads logs | For what |
|---|---|
| On-call engineers | Fixing an incident in the next ten minutes |
| Developers | Understanding a bug report from last Tuesday |
| Security teams | Finding who accessed what, and when |
| Support staff | Checking whether a refund or a login really happened |
Each reader needs a different slice, from the last ten minutes to last year, and that spread is what makes retention a design question.
What it must do, and how much data
Pin down the requirements before drawing any boxes, then estimate the volume, because for logging the volume decides most of the design.
- Never block the app: logging must not slow requests, and a log system outage must not take services down.
- Lose little: keep every error line, even while the log store is down.
- Search fast: find all lines for one request or one user, across all services, within seconds.
- Stay fresh: a line written now should be searchable within roughly 10 to 30 seconds.
- Stay affordable: recent logs fast, old logs cheap, and useless logs deleted.
The volume follows from the fleet: forty services at about fifty copies each.
| Quantity | Value | How |
|---|---|---|
| Service instances | 2,000 | 40 services, about 50 copies each |
| Lines per instance | 50 per second | an average; far higher during incidents |
| Lines per second | 100,000 | 2,000 × 50 |
| Bytes per line | 500 B | a structured line with a dozen fields |
| Ingest rate | 50 MB/s | 100,000 × 500 B |
| Per day | ≈ 4.3 TB | 50 MB/s × 86,400 s |
| 30 days, uncompressed | ≈ 130 TB | before indexes and replicas |
That can be more data than the app's own business database. Log volume grows with traffic and with every line a developer adds, so cost and retention are design questions from day one, not afterthoughts.
Part 2 · Logs Worth Searching
Structured logs
Volume is one problem; finding the right line is the other. A plain text line is easy to write and hard to search. A structured log is a line made of named fields, usually one JSON object per line, so the store can filter on level or duration_ms exactly instead of guessing with text patterns.
Plain text: 2026-10-16 20:31:07 ERROR payment failed for order 88231 after 3021ms Structured, one JSON object per line: {"ts":"2026-10-16T15:01:07.412Z", "level":"ERROR", "service":"payments", "version":"4.12.0", "host":"pay-7f9c", "request_id":"r-5f2a9c", "order_id":88231, "event":"charge_failed", "reason":"gateway_timeout", "duration_ms":3021}
The same event. The structured line stores time in UTC (20:31 IST is 15:01 UTC) and can be filtered on any field.
A few fields earn their place in every line:
| Field | Why it earns its place |
|---|---|
ts | UTC time with milliseconds, so lines from different machines sort together |
level | Filter to errors instantly; drop debug lines cheaply |
service, version, host | Which code on which machine wrote it; a bad release shows up as one version |
request_id | Ties together every line from one user action, across services |
event | A fixed name like charge_failed that is easy to count and alert on |
duration_ms, status | Numbers you can sort, chart and compare |
If one team logs requestId, another req_id and a third trace, cross-service search breaks. Put the common fields in a shared logging library so every service gets them without thinking.
One request id across every service
Of those fields, one ties the services together. A request id (also called a correlation id) is a unique id created when a request first enters the system, usually at the gateway. Every service writes it into each log line and passes it on in a header, such as X-Request-Id or the standard traceparent header used by tracing tools.
- 1Gatewaycreates r-5f2a9c and logs: order request received
- 2Order servicelogs: order 88231 created
- 3Payment servicelogs: charge_failed, reason gateway_timeout
- 4Order servicelogs: order 88231 marked payment_failed
- 5Notification workerreads the id from the queue message and logs: failure SMS sent
Searching request_id = r-5f2a9c now returns the whole story as one list. The id must also travel through queues and background jobs: put it inside each message and have the consumer log it. Otherwise the trail stops at the first queue.
Two machines' clocks can differ by a few milliseconds or more, so sorting by timestamp can place a reply before its request. Use timestamps for rough order, and the parent-child links of a trace (see the monitoring chapter) when exact order matters.
Log levels and sampling
Not every line deserves to be stored, let alone stored for months. Log levels mark how important a line is, and each service decides which levels it writes.
| Level | Use it for | In production |
|---|---|---|
| DEBUG | Step-by-step detail for developers | Off, or on for one service for a short time |
| INFO | Normal events: order placed, user logged in | On, often sampled |
| WARN | Something odd that the code handled, like a retry that worked | On |
| ERROR | A request failed | On, never sampled |
| FATAL | The process cannot continue | On, and usually raises an alert |
Levels decide what is written; sampling decides how much of it is kept. Sampling keeps only a fraction of routine lines: keeping 5% of successful requests cuts their volume twentyfold while still showing what normal looks like. Decide per request, not per line, so a kept request keeps all its lines and you never read half a story.
- Head sampling decides when the request starts. It is simple and cheap, but it may drop the very request that later failed.
- Tail sampling holds a request's lines briefly and decides at the end: keep every failed or slow request, sample the rest. Better results, more memory.
- Collapse repeats: a bug that logs the same error 10,000 times a second should produce one line with a count.
- Change levels at runtime: switching DEBUG on for one service for 15 minutes beats a redeploy.
- Never sample audit events: records of who logged in or changed a payout account go to a separate stream, covered near the end of this chapter.
Part 3 · The Pipeline
Agents, buffering and shipping
With the lines worth keeping defined, the next question is how they leave the machine. Services should not send logs over the network themselves. They write to standard output or a local file, which is fast and never waits on the network. A small collection agent on each machine, or on each node of a container cluster, does the rest.
- 1Tailfollow log files as they grow
- 2Enrichadd host, region, container and app version
- 3Batch and compressgroup lines for about 1 s or 1 MB, whichever comes first
- 4Shipsend the batch to the queue; retry with backoff if it fails
- 5Checkpointonce the queue confirms a batch, save the file position, so a restart resumes there
When the network or the queue is down, the agent keeps reading into a buffer on local disk with a fixed size limit. If the buffer fills, it must drop something, and the sensible order is DEBUG first, then INFO, and errors last. Shipping is at least once: after a crash the agent resends from its last checkpoint, so a few lines may arrive twice, which is usually harmless for logs.
Cap the agent's CPU, memory and disk use. A disk filled by runaway logs can crash the very service you are trying to debug, so rotate log files and delete local copies once they are shipped.
Watch: a log storm meets a queue
Agents could write straight into the log store, but large pipelines put a queue, a replicated log as in the message queue chapter, in the middle. Watch one failed order's lines travel from two machines to the on-call engineer's screen, then watch an incident push the store behind.
Predict before you watch: during an incident, the log store indexes more slowly than new lines arrive. What happens to the lines written meanwhile?
- They wait in the queue and show up in search late.
- None are lost, as long as the store catches up within the queue's retention, 3 days here.
Watch for r-5f2a9c: one failed order leaves lines on two machines
Read it as steps
Lines from two services meet in one search, and a queue holds them while the store falls behind.
The request id is what turns files on separate machines into one story. The queue is what lets the store fall behind without losing lines: the queue turns a store outage into a delay instead of data loss. The next page sizes it.
Sizing the queue
The scene showed what the queue absorbs. It earns its place in four ways:
- It absorbs bursts: an incident can multiply log volume five times over, exactly when you need the logs most.
- It decouples: the store can be upgraded, slowed or restarted while agents keep shipping.
- It feeds many readers: the search index, a cheap archive in a blob store and security tools each read the same stream at their own pace.
- It allows replay: if a parser bug mangled an hour of lines, fix the parser and read that hour again.
Now the numbers. One partition of a log-based queue handles about 10 MB/s (the message queue chapter). At the app's 50 MB/s, that is at least 5 partitions on an ordinary evening, and a storm at 250 MB/s needs 25, so plan for about 32. Spread lines across partitions by host rather than by service, or the busiest service makes one partition hot. Three days of retention at 50 MB/s is 50 MB × 259,200 s ≈ 13 TB per copy, about 39 TB with three replicas. Sizing on raw bytes leaves headroom, since agents compress batches 5 to 10 times.
The advice flips for small systems. With a few dozen machines, agents with disk buffers can write straight to the store, and a queue would be one more system to run for little gain. It pays off once storms, several readers or replays matter.
Part 4 · Storing, Searching and Running It
Indexing and searching logs
Lines leave the queue for a store that must find them fast. Logs are written once, almost never changed, and nearly always searched by time first: "errors from payments in the last 15 minutes". Log stores exploit this with time-based partitions, one per hour or per day. A query opens only the partitions in its time range, and deleting old data means dropping whole partitions. Two indexing styles compete:
| Index every word | Index labels only | |
|---|---|---|
| What is indexed | Every field and word of every line | A few labels: service, level, host, time |
| Finding a rare word | Milliseconds | Seconds or more: scan the compressed lines that match the labels |
| Storage cost | High: the index can rival the data in size | Low: a tiny index, lines stay compressed |
| Good for | Security investigations and free-text digging | Huge volumes where queries filter by service and time |
Indexing every word is the inverted index from the distributed search chapter, applied to log lines. Indexing only labels and scanning the rest sounds slow, but the haystack is small: one service's lines for 15 minutes come to about 1 GB before compression (50 copies × 50 lines a second × 500 B × 900 s), which a few machines can scan in seconds. Many teams mix both: full indexing for a few days of hot data, labels only for older data.
A query without a time range must open every partition. Good log tools apply a default window, such as the last hour, and ask before scanning a whole month.
Retention and cost tiers
Most log searches look at the last few hours. Hardly anyone reads a line from eight months ago unless a security team or an auditor asks. So logs move through tiers, from fast and expensive to slow and cheap, and are deleted at the end.
| Tier | Age | Where it lives | Search speed | Cost |
|---|---|---|---|---|
| Hot | 0 to 7 days | SSD, fully indexed | Seconds | High |
| Warm | 7 to 30 days | Cheaper disks, fewer replicas | Tens of seconds | Medium |
| Cold | 30 days to 1 year | Compressed files in a blob store | Minutes; restore first | Low |
| Deleted | After the retention period | Nowhere | Not possible | None |
Age0 to 7 days
Where it livesSSD, fully indexed
Search speedSeconds
CostHigh
Age7 to 30 days
Where it livesCheaper disks, fewer replicas
Search speedTens of seconds
CostMedium
Age30 days to 1 year
Where it livesCompressed files in a blob store
Search speedMinutes; restore first
CostLow
AgeAfter the retention period
Where it livesNowhere
Search speedNot possible
CostNone
The hot tier is where the money goes. Seven days at 4.3 TB a day is about 30 TB of raw lines. With every field indexed, assume the stored lines plus the index take about twice that, plus one replica so a dead node loses nothing: 30 × 2 × 2 ≈ 120 TB of SSD. At an assumed $0.10 per GB-month for network SSD (check current prices), that is about $12,000 a month for the disks alone, before the machines.
Logs compress well, often 5 to 10 times, because lines repeat the same field names and values. Indexing labels only and keeping lines compressed 10× shrinks that week to about 6 TB, about $600 a month. A year of cold archives at 10× is about 160 TB, roughly $3,200 a month at about $0.02 per GB-month for object storage.
Retention differs by kind: debug lines for days, request logs for weeks, audit trails for years. The law can set a floor: in India, CERT-In's 2022 directions ask many organisations to keep logs of their systems for a rolling 180 days, stored in India, so check what applies to yours.
The cheapest log line is the one never written. Dropping noisy debug lines and sampling successful requests usually saves more than any storage trick.
Privacy: what never goes in a log
Long retention has a dark side. Logs are copied to many machines, kept for months and read by many people, so anything sensitive written into a line leaks to all of them. Treat the log store as a place that holds no secrets and as little personal data as possible.
| Never log | Log this instead |
|---|---|
| Passwords, OTPs, PINs | Only the outcome, such as otp_verified=false |
| Access tokens, API keys, session cookies | A token id, or the last 4 characters |
| Full card numbers, CVV, bank details | The last 4 digits of the card |
| Aadhaar or PAN numbers | An internal user id |
| Phone numbers, emails, addresses | An internal user id; a keyed hash if lines must be matched |
| Whole request and response bodies | Selected, named fields only |
The safest data is data never written. Four habits make that stick:
- Allow-list, do not block-list: the logging library writes only fields known to be safe, instead of trying to spot every dangerous one.
- Redact in the pipeline too: a second check in the agents or indexers masks patterns, such as card numbers, that slipped through.
- Control who can search: restrict access, record every search of the logs, and keep production logs off laptops.
- Remember the law: privacy laws such as India's Digital Personal Data Protection Act, 2023 give people rights over their data, and erasing one person from months of logs is very hard. Not logging it is far easier.
Audit logs: a separate stream
Some lines are evidence, not debugging help: who logged in, who changed a restaurant's payout account, who exported customer data, which admin approved a refund. Security teams and auditors may need them years later, and they must trust that nobody edited them. These audit logs travel in a separate stream with stricter rules.
| Application logs | Audit logs | |
|---|---|---|
| Purpose | Debugging and incidents | Evidence of who did what, and when |
| Sampling | Routine lines sampled; debug often off | Never sampled; every event kept |
| How it is written | Best effort: an agent may drop debug lines when full | Recorded as part of the action, such as an outbox row in the same database transaction, then shipped |
| Changes | Deleted by tier on a schedule | Append-only: no edits, no early deletes |
| Who can read | Engineers on the team | A small security group; every read is itself recorded |
| Kept for | Days to a year | Years, as long as the law or contracts require |
An action and its audit record succeed or fail together, so no payout change can happen without its record. Keep audit events in write-once storage, such as a blob-store bucket set to refuse deletes until a retention date. Link each batch to the one before it with a hash, so a missing or edited batch shows up, and keep the latest hash where the log writers cannot change it, such as a separate account or a published daily digest: anyone who can rewrite the whole chain can also recompute every hash. An audit line still records the user id and the action, never the card number.
What goes wrong
Logging pipelines fail in predictable ways, and most failures peak during incidents, when the logs matter most.
| Problem | Effect | Fix |
|---|---|---|
| Log storm in an incident | The pipeline falls behind exactly when needed | Queue buffer, collapse repeated lines, drop debug lines first |
| Agent buffer full | Lines dropped on that host | Bounded disk buffer, an alert on drops, priority by level |
| Log store slow or down | Search results are stale | Keep a day or more in the queue; replay when the store is back |
| Missing request ids | Trails stop at a queue or an old service | Shared library, ids inside messages, a check in code review |
| A secret in the logs | A security incident | Allow-listed fields, pipeline redaction, rotate the leaked secret |
| Runaway cost | The log bill grows faster than traffic | Sampling, tiers, per-team volume reports and quotas |
Almost every fix either sheds low-value lines first or buys time with buffers. Three numbers catch most of these early: lines received per service, queue lag, and lines dropped by agents. A busy service that suddenly sends zero lines means the pipeline is broken, not the service.
In the interview
Logging usually comes up inside a bigger design, and a few sentences show that you know the whole path from the line in the code to the search box.
- Say
- "Services write JSON lines locally and an agent ships 1 s or 1 MB batches to a queue, so logging never blocks a request; a few lines may arrive twice."
- Say
- "At 50 MB/s, a fully indexed week is about 120 TB of SSD, so after 7 days logs move to cheaper tiers, and after 30 to compressed files at a tenth the size."
- Say
- "Errors are never sampled; successful requests are kept at 5%, a twentyfold cut. Audit events go to their own unsampled, append-only stream."
- Avoid
- "Every service writes its logs straight into the database." One slow store would then slow every request.
- Avoid
- One retention rule for everything: debug lines kept for a year waste money, and audit trails deleted after 30 days fail an audit.
Wrap-up
| Term | Meaning |
|---|---|
| Structured log | A line of named fields, usually one JSON object |
| Request id | An id created at the edge that every service logs and passes on |
| Collection agent | A process on each host that tails, batches and ships logs |
| Tail sampling | Keeping or dropping a request's lines after seeing how it ended |
| Hot, warm and cold tiers | Storage that gets slower and cheaper as logs age |
| Audit log | Append-only evidence of who did what, kept for years |
Next comes Distributed Task Schedulers: work that runs later and on time, built on the same at-least-once thinking you used here to ship logs.
Part 5 · Check yourself
Quick check
Q1Why put a queue between the agents and the log store?
Think it through, then check.
- It absorbs bursts such as incident log storms, so the store can fall behind or restart without losing lines.
- Several readers use the same stream at their own pace, and a bad hour can be replayed after a parser fix.
- The cost: one more system to run, and a few seconds of delay.
Q2Search shows no logs from the payments service for the last 20 minutes, yet its dashboards show normal traffic. What could be wrong, and how would you catch it sooner?
Think it through, then check.
- Payments is alive, so the log path is broken: its agents crashed or filled their buffers, or a release writes logs where the agent does not look.
- Further along: queue lag, indexer parse errors after a library change, or wrong timestamps outside the window.
- Next time, alert on lines received per service: zero lines with normal traffic means a broken pipeline.
Q3In an incident, log volume jumps from 50 to 250 MB/s for 30 minutes, but the store indexes only 100 MB/s. How much piles up in the queue, and how long until search catches up?
Think it through, then check.
- The backlog grows by 250 − 100 = 150 MB/s: 270 GB in 30 minutes (× 1,800 s).
- Afterwards the store has 100 − 50 = 50 MB/s spare, so catching up takes 270 GB ÷ 50 MB/s = 5,400 s, about 1.5 hours.
- Give error lines their own small stream so they stay searchable during the backlog.
Q4To chase a payment bug, a developer asks to log full request bodies for a week. What do you decide, and what does each option cost?
Think it through, then check.
- Not in the main logs: bodies can hold card numbers, tokens and personal data, seen by everyone with log access and kept for a year.
- Instead, log selected safe fields with the request id, or capture one service's bodies for an hour in a locked store that deletes itself.
Q5A teammate wants a user_id label on the payment-errors metric so support can see each user's failures. Using the monitoring chapter, what do you say?
Think it through, then check.
- Every label value creates a separate time series; 10 million users means 10 million series, enough to bring the metrics system down.
- Keep metric labels to a few values each, such as service, region and status.
- Put
user_idin the structured log line, where support can search one user's requests.