Engineering Atlas › System Design Fundamentals

Collecting logs into one place

11 min read · last reviewed 2026-10-17

Open in the interactive app: practice, quiz, mock interview →

In one minute

How it works

Emit

Applications should write structured logs, one JSON object per line, with consistent fields: timestamp, level, service, trace_id, message, and event-specific keys. The OpenTelemetry log data model is a good target schema, since its records already carry severity, trace ID and span ID (OTel logs data model). Writing to stdout and letting the platform capture it keeps shipping and rotation out of the app (Twelve-Factor).

Collect

An agent (Fluent Bit, Vector, the OpenTelemetry Collector) runs on every node, tails container log files, adds metadata such as pod, namespace, and region, and ships batches. It must checkpoint its read position so a restart loses nothing, and buffer to disk when the backend is slow. Fluent Bit's tail input keeps inode-to-offset pairs in a small SQLite file and warns that a few records may be read again after a crash, so "neither loses nor resends" is really "loses nothing, may resend" (Fluent Bit docs). The agent is not optional on Kubernetes: the kubelet keeps at most five 10 MiB files per container by default, and kubectl logs shows only the newest one (Kubernetes logging).

Buffer and process

At scale, agents send to a durable queue such as Kafka, which absorbs bursts and decouples producers from indexers. Processors parse unstructured lines, drop noise, redact secrets and personal data, and route streams to different destinations by team or retention class.

Store and query

Inverted-index stores tokenize every field and build a term-to-document index. Searches for any word are fast, but indexing costs CPU and the index can be as large as the raw data, so storage commonly runs 1 to 1.5 times raw. Label-indexed stores index only a few low-cardinality labels (service, environment, level) and keep log content as compressed chunks in object storage. Queries select streams by label, then scan chunks in parallel (Loki overview). Ingest and storage are much cheaper, often 10 times compression on object storage, but searching for a rare string across a whole day means scanning everything.

Retention tiers

Recent days stay on hot storage; older data moves to cold tiers or object storage and is deleted on a schedule set by debugging and compliance needs (Elastic ILM). Which events you collect matters as much as how long you keep them: an investigation can only search the event types that were switched on before the attack, which is why security teams decide audit logging in advance rather than during an incident.

Down the stack

Layer 1The contract

A log pipeline usually promises at-least-once delivery from the node to the store: every line written to stdout is eventually searchable, possibly duplicated, within a stated delay such as 30 seconds at p99. Ordering is promised only per source stream, by timestamp, not globally. Under sustained overload it promises explicit behaviour, either backpressure with bounded disk buffering or counted drops, never silent loss. Retention promises that data is deleted after its class's period.

Layer 2The mechanism

In Kubernetes, the container runtime writes each container's stdout to /var/log/pods/NAMESPACE_POD_UID/CONTAINER/0.log in CRI format: timestamp, stream, a partial-line flag, and the message. The kubelet rotates these files at containerLogMaxSize (10 MiB by default), keeping containerLogMaxFiles (5). The agent tails them with inotify, records the inode and byte offset of each file in a small database, and handles rotation by continuing to read the old inode until EOF before switching. Partial-line flags let it reassemble lines the runtime split at 16 KB. Batches are compressed and shipped; acknowledgement from Kafka or the backend advances the stored offset. Elasticsearch indexes each document into a Lucene in-memory buffer, makes it searchable after a refresh (1 second by default) by writing a new segment, and merges small segments in the background. A shard that has not been searched for 30 seconds stops refreshing until the next search arrives, unless the interval is set explicitly (Elasticsearch index settings). Loki groups lines by their label set into a stream, appends to an in-memory chunk per stream, and flushes chunks with a target size of 1.5 MB (chunk_target_size 1,572,864 bytes) to object storage with a small index entry per chunk (Loki config). High-cardinality fields that people still filter on, such as trace_id or pod name, go in structured metadata: stored beside each line, not indexed, and available only with the tsdb index and schema v13, which replaced the deprecated boltdb-shipper (Loki docs).

Layer 3The machine

Tailing is cheap: inotify wakes the agent on writes, and reads hit the page cache because the data was just written. The expensive parts are compression and indexing. Gzip or Zstd at 100 to 500 MB/s per core means a node shipping 20 MB/s spends a few percent of a core. Lucene indexing, by contrast, tokenises every field and builds postings lists; a single core indexes on the order of 5,000 to 20,000 small log documents per second, so 300,000 lines per second needs tens of cores for indexing alone, plus disk writes for segment merges that rewrite data several times over. Loki avoids most of that CPU and write amplification, paying instead at query time by decompressing chunks at around 1 GB/s per core.

Why it's built this way

  1. Applications should not block or crash because the log backend is slow, so they write to local files and an agent ships asynchronously.
  2. Agents and networks fail, so offsets are checkpointed and delivery is at-least-once.
  3. Log bursts are much larger than average traffic, so a durable queue absorbs them.
  4. Indexing every token is CPU- and storage-heavy, so cheaper designs index labels only and scan compressed chunks when needed.

Diagram

flowchart LR
  APP["App: JSON to stdout"] --> AG["Node agent: tail, enrich, buffer"]
  AG --> KQ["Kafka buffer"]
  KQ --> PR["Processors: parse, redact, route"]
  PR --> HOT["Hot index (7 days)"]
  PR --> OBJ["Object storage archive"]
  HOT --> UI["Search UI"]
  OBJ -.->|"rehydrate on demand"| UI

Worked example

A company runs 2,000 pods across 40 services. Average log rate is 150 lines per second per pod at 400 bytes, so 300,000 lines per second, 120 MB/s, about 10 TB per day raw.

Option A: Elasticsearch with 7 days hot. Index size near raw size, times two for one replica, gives 20 TB per day and 140 TB on disk: about 18 data nodes at 8 TB usable SSD each, a count that indexing 300,000 documents per second needs for CPU anyway.

Option B: Loki-style storage. Logs compress about 10 times into 1 TB per day of chunks in object storage. Thirty days is 30 TB, about $700 per month in storage. Ingesters and queriers need maybe 10 to 15 nodes. A query like "errors from checkout in the last hour" selects one stream set and scans 2 GB in seconds. A search for one order ID across all services for 7 days scans 7 TB compressed and takes minutes, unless the order ID is also emitted to a cheaper index.

Reduction first: the team finds that 45 percent of volume is DEBUG from two services and health-check access logs. Dropping health checks and sampling successful-request access logs at 10 percent cuts volume to 5 TB per day before choosing any backend.

In the wild

Trade-offs

OptionStrengthsWeaknessesChoose when
Full-text index (Elasticsearch, OpenSearch)Fast search on any term, rich aggregationsHigh CPU, RAM, and storage cost; cluster tuningSecurity analytics, heavy ad hoc search
Label index plus scan (Loki)Cheap ingest and storage on object storageSlow needle-in-haystack queries over long rangesOperational debugging scoped by service and time
Columnar store (ClickHouse)Fast aggregations, good compression, SQLSchema design effort, self-managedStructured logs used like analytics events
SaaS (Datadog, Splunk Cloud)No operations, polished UXCost per GB scales painfullySmall teams or until volume makes it unaffordable

Pitfalls in production

Leadership lens

Check yourself

  1. Why is a label-indexed store much cheaper than a full-text index?

    Show answer

    It indexes only a few low-cardinality labels and stores compressed content in object storage, avoiding the CPU and disk cost of indexing every token.

  2. What role does a Kafka buffer play between agents and the store?

    Show answer

    It absorbs bursts, decouples ingestion from indexing speed, and allows replay if the store is down or a processor has a bug.

  3. Why put trace_id in every log line?

    Show answer

    It links logs to the distributed trace for the same request, so you can move from a slow span to the exact log lines it produced.

Sources and further study

Primary

Systems docs

Books

Other curricula

Watch

Free resources

Open in the interactive app: practice, quiz, mock interview →