Skip to content
SeeItFlow
System Design · 16 chapters · ~34 min read

Distributed Logging System Design: From a Local File to a Multi-Tenant Platform

A logging platform looks, at first glance, like a text box you can grep. It isn't. What it actually has to guarantee is that a log record survives the failure of the exact process that produced it, that it does so without ever slowing down the request that generated it, and that the system can tell an honest, specific story about where any given record currently stands — buffered, durably accepted, indexed, searchable, or deleted — without ever collapsing those into one vague claim of 'logged.' This module builds one step by step: a local file, a naive central server, async agents, bounded disk spools, collector gateways, a durable partitioned bus, independent indexers, two genuinely different storage philosophies, a tenant-aware query layer, tiered retention, and multi-region reliability — all illustrated by one fictional checkout failure followed from source to search result.

Mangesh MandlikLast updated: How lessons are made
Chapter 01

Problem Definition

Northstar Commerce (fictional, for this module) runs a dozen services across thousands of short-lived instances and multiple regions. A customer reports a failed checkout. Nobody yet knows which service, which instance, or which region produced the error — or whether evidence of it even still exists.

Swipe to pan
A Request Crosses Three Services, Three FilesNo shared identifier connects checkout-api, payment-service, inventory-servicecheckout-apihost A — own filepayment-servicehost B — own fileinventory-servicehost C — own file💥 No trace_id3 files, 3 hosts,no way to join them
A request crosses three services, three hosts, and (without a shared identifier) three unrelated local files

This is not primarily a storage problem, even though it looks like one. Writing a line of text somewhere is trivial. What's actually hard is making that line of text reliably survive the lifecycle of the ephemeral compute that produced it, correlating it with the other lines produced by the same logical request across service boundaries, making it searchable within a useful time budget, and doing all of this without that machinery ever being allowed to slow down or fail the request it's merely observing.

A logging system earns every piece of its complexity by solving one of these demonstrated problems — survivability, correlation, searchability, isolation from the request path, bounded retention, tenant privacy, and regional reliability — never by assumption. This module adds exactly one new mechanism per chapter, each introduced immediately after the failure that makes it necessary.

Interactive

Visual Design Journey

Watch a logging platform evolve from a single local file into a multi-tenant, region-aware pipeline — async agents, disk spools, a durable partitioned bus, independent indexers, and tenant-scoped query — following one checkout error from source to search result through every failure along the way.

Chapter 02

Requirements

Scope this explicitly before any diagram — the scope decisions are half the design.

Functional

Collect structured and unstructured log records from every application instance, across every service and region. Make records durable before acknowledging receipt. Make them searchable by free text, structured field, service, time range, and correlation ID. Enforce tenant-scoped access on every search. Retain records on a documented, tiered schedule, with an explicit override for legal hold. Administration UIs, alerting rule engines, and full APM/tracing backends are explicitly out of scope — this module focuses on the collect → transport → index → query pipeline itself.

Non-functional

Application isolation

No amount of logging backpressure, slowness, or outage may add latency to, or fail, the application request that generated the log line. Logging must never sit synchronously on the customer-facing request path.

An explicit, honest durability boundary

The design must be able to say, for any given record, exactly which of several distinct stages it has reached — generated, buffered, spooled, durably accepted, indexed, queryable, retained, deleted — and must never conflate a weaker claim (e.g. 'accepted') with a stronger one (e.g. 'indexed').

Bounded resource use

Every buffer, spool, queue, and index has a finite capacity and a documented, explicit overload policy. An unbounded buffer is not a design decision — it is a deferred outage.

Tenant isolation

One tenant's log data, and the ability to search it, must never be reachable by another tenant's credential — enforced from the authenticated connection identity, never from a client-supplied field.

Search honesty

The system must never claim a record is retained or searchable when it is actually only accepted, indexing-delayed, or already past its retention window. A stale index must show its own staleness, not hide it.

Illustrative scale

For reasoning purposes only: roughly 10,000 active application instances, each producing an average of 5 log events per second, for an average of 50,000 events/s fleet-wide at an 800-byte average event size — about 40 MB/s, or 3.456 TB/day, before compression or replication. A sales-event peak of 12× that average is used later in this module. These are planning numbers for this fictional case study, never measured production traffic.

Chapter 03

V0 — One Service, One File, One Grep

Start with the simplest thing that could possibly work: the order service writes a timestamped line to a local file for every event. An engineer greps it when something breaks.

Swipe to pan
V0: One Service, One File, One GrepLegitimate for exactly one service on one host — not a strawmanorder-servicewrites local filecheckout.logappend-only text fileEngineerssh + grep
Legitimate for exactly one service on one host — not a strawman

This is not a strawman. For a single service on a single host, a local file and grep is a completely legitimate solution — fast, debuggable, and free of any distributed-systems complexity. A logging platform earns its complexity later, by solving demonstrated problems at scale, not by assumption.

The cracks appear the moment a request crosses more than one service. checkout-api calls payment-service calls inventory-service, each on a different host, each with its own local file and no identifier connecting the three fragments of one logical request. And ephemeral compute makes the fragility worse: when Kubernetes restarts a crashed pod, the old container's filesystem — and whatever log evidence it held — disappears with it.

Chapter 04

V1 — A Naive Central Log Server, and Its Limits

Replace local files with one central server that every application POSTs log lines to, synchronously, and that also serves search. At low volume, this is a legitimate, shippable V1.

Swipe to pan
V1: Synchronous Push to a Central ServerWorks at low volume — ingest and search share one box and one thread poolcheckout-apipayment-svcCentral Log Serveringest + search, 1 boxSync write blocks the request path
A single box doing both ingest and search — fine until either workload grows

As usage grows, two problems appear at once. First, every application request now pays a synchronous network call to the central server just to log something — a cost paid on the hot path of the actual customer request, not a background concern. Second, that same box is also answering search queries, and a broad, unindexed search competing for the same CPU and I/O as live ingest slows both down together.

When an engineer runs a heavy search during a normal traffic period, the central server's write path degrades along with its read path — and because every application's own request blocks on that write, checkout-api's own customer-facing latency rises in lockstep with the logging backend's load. A component whose entire purpose is to help diagnose problems has become capable of causing one, directly on the customer request path.

Chapter 05

Decoupling: Async Loggers and the Node Agent

Give the application an in-process async buffer, and move the actual network call to a separate node agent process shipping on its own schedule.

Swipe to pan
Tailing Rotating Files SafelyCheckpoint on inode + byte offset, not just a file pathcheckout.loginode 88214checkout.log.1old inode, renamedNode Agenttail -f + checkpointCheckpoint Storeinode + offset + partial line
Tailing rotating files safely: file identity plus an acknowledged byte offset, never just a path

Return immediately, ship separately

Writing a log line becomes an append to an in-process queue, returning in microseconds. A background thread in the application writes those lines to stdout, a log file, or a local socket. A separate agent, either a sidecar or a node-level DaemonSet in a container environment, tails that file or receives from that socket and forwards it independently. The agent cannot read the application's memory; it only sees what the application or its runtime puts where the agent can reach it. Whatever happens to the logging backend afterward is invisible to the request that generated the log line.

File rotation is a real correctness hazard

A tailer that only tracks a file path can lose its place when logrotate renames or truncates the file underneath it. A correct agent identifies the file (inode plus a fingerprint), tracks a byte offset that only advances after the next durable stage acknowledges the data, and never checkpoints into the middle of a line. It handles rename-based rotation and copytruncate rotation differently, because they change file identity differently. If it crashes before the acknowledgment, it re-reads and resends, so delivery is at-least-once.

Chapter 06

Bounded Buffers and the Disk Spool

The agent's in-memory buffer is finite. When the backend is briefly unreachable, that buffer fills — and the design has to make an explicit, honest choice about what happens next.

Swipe to pan
Three Different, Honestly-Labeled Durability ClaimsMemory buffer ≠ disk spool ≠ replicated broker commitIn-Memory Bufferlost on process crashDisk Spool (fsynced)survives agent crash, not disk lossCommitted to Brokeracks=all, min ISR metIncreasing durability guarantee, increasing latency and cost →
Memory buffer, disk spool, and replicated broker commit are three different, honestly-labeled durability claims

Three real options exist once the memory buffer is full: drop the newest events, block the application until space frees up, or spool overflow to local disk. Blocking the application is never acceptable — it reintroduces exactly the synchronous coupling the previous chapter removed. The real choice is between dropping and spooling, and the right answer depends on the log's class: best-effort debug logs can tolerate drops, while error and audit-class events generally cannot.

A bounded, fsynced disk spool

A file-backed spool between the in-memory buffer and the network survives an agent process restart. Durability here means something specific: a write only counts as spooled once it has actually been flushed and fsynced to disk, not merely handed to the OS page cache — a crash before that fsync can still lose it. The spool itself has a configured capacity; once full, the same drop-or-block choice returns, just with a much larger runway.

Chapter 07

The Collector Tier: Auth, Redaction, Batching

Insert a collector gateway between agents and the rest of the platform — the natural place for authentication, payload validation, redaction, and batching.

Swipe to pan
Collector Gateway: Four Stages, In OrderRedaction happens before durable persistence, not afterAuthenticate(mTLS)ValidateschemaRedactsensitiveBatch +compress
Four ordered stages: authenticate, validate, redact, batch — in that order, not reshuffled

Tenant attribution from the connection, not the payload

The collector authenticates each agent connection (typically mTLS) and attributes every batch to a tenant based on that verified identity — never a tenant_id value the agent merely claims inside the payload. This is the first layer of the tenant-isolation boundary developed further in Chapter 14.

Redaction happens before durable persistence

A secret-looking value — a credit-card-shaped token, an accidental API key in a debug line — gets stripped by the collector's redaction rules before anything durable happens to the record. This ordering matters: encrypting the store afterward protects data at rest, but does nothing to undo an exposure that already happened upstream of redaction. A redaction rule that can't confirm a field is safe should fail closed, holding the field or record back, rather than forwarding it unredacted.

Chapter 08

Absorbing Bursts: A Durable Partitioned Bus

A sales promotion pushes ingest from an illustrative 50,000 events/s average to 600,000 events/s peak — twelve times normal. Nothing downstream sized for the average day survives this burst unmodified.

Swipe to pan
Raw Bytes Are Not Provisioned Disk (illustrative)10,000 instances × 5 events/s × 800 bytes avgRaw daily ingest40 MB/s × 86,400s = 3.456 TB/dayAfter ~0.35 compression≈ 1.21 TB/dayWith 1 replica (×2)≈ 2.42 TB/day+ index / merge headroommaterially more — measure, don't assume
Raw ingest math: 10,000 instances × 5 events/s × 800 bytes ≈ 40 MB/s average, 3.456 TB/day raw

If every event still had to be synchronously indexed before the collector could acknowledge it, indexing throughput would become the ceiling for the entire ingest path during a burst. The fix is to insert a durable, partitioned log bus — conceptually Kafka-like — between the collector and indexing, and to redefine what 'accepted' means: success now means durably written to the bus, not indexed and not searchable.

Ingest and indexing, fully decoupled

Indexing becomes an independent consumer reading the bus at its own pace. A burst fills the bus's retention window instead of backing up all the way to the collector and the agents behind it — this single change is what lets a 12× peak get absorbed without the ingest path stalling.

Chapter 09

Partition Keys and Independent Indexers

Partitioning by raw service name alone sends one chatty service's entire volume to a single partition — overloading it while others sit idle, regardless of how many brokers exist in the cluster.

Swipe to pan
Partition Key Choice Decides Who Shares a Hot SpotService-name-only keying funnels one chatty service onto one partitionPartition 0checkout-api — 95% loadPartition 1payment-svc — 20% loadPartition 2inventory-svc — 15% loadFix: composite keyservice + instance_id, orservice + hashed shard —spreads one hot serviceacross many partitions
A composite key (service + instance or service + hashed shard) spreads a hot service across many partitions

A consumer group's parallelism for a single partition is capped at one consumer reading it at a time — adding more brokers doesn't raise that ceiling. The fix is a better partition key, chosen for what it's meant to protect: ordering scope, a specific noisy tenant, or load distribution. A genuinely dominant tenant may need a dedicated partition set and its own quota rather than sharing the default pool.

Independent indexers consume the bus as a separate consumer group, turning durable records into searchable documents on their own schedule. 'Durably accepted' and 'searchable' are now visibly different moments — typically single-digit seconds apart under healthy operation, and measurably longer during an indexing outage (Failure & Scale Scenarios covers this in depth).

Chapter 10

The Full Event Lifecycle

Name every stage a record passes through explicitly — this is the single sentence that should anchor every durability claim this design makes.

Swipe to pan
The Full Event LifecycleTen named states — each a place the record can stop existing or become staleGENERATEDBUFFEREDFLUSHEDTO AGENTDISKSPOOLACCEPTED(ingest)DURABLE(broker)READ BYINDEXERINDEXEDQUERYABLEARCHIVEDDELETED
Generated → buffered → flushed → (optional disk spool) → accepted → durable → read by indexer → indexed → queryable → archived → deleted

Each arrow in this chain is a place a record can stop existing, or become stale, independently of every other stage. 'The application logged it' only means generated. 'The agent sent it' only means flushed. 'The collector accepted it' is weaker than 'the broker durably committed it,' which is weaker still than 'the indexer has processed it,' which is weaker than 'it's visible in search results right now.'

Chapter 11

Two Storage Philosophies: Full-Text vs. Label-Chunk

How a record becomes searchable is a genuine architectural fork, not a vendor preference — two meaningfully different philosophies exist, and neither is universally better.

Swipe to pan
Inverted Index (ES/OpenSearch style)Every token in every indexed field becomes a posting list entryLog document"payment timeout" ..."payment" → [doc12, doc88, …]"timeout" → [doc12, doc41, …]"error" → [doc5, doc12, …]Fast free-text search, paid for at write time
An inverted index (ES/OpenSearch style): every indexed token becomes a posting-list entry
Swipe to pan
Label Index + Compressed Chunks (Loki style)Only labels are indexed; message text lives in scanned chunksLabel Indexservice=payment, level=errorCompressed Chunkraw lines, text-scanned on queryCheap at ingestOnly a handful of labelsare ever indexed —free-text search meansscanning more chunks
A label index over compressed chunks (Loki style): only a handful of low-cardinality labels are ever indexed

A full-text inverted index tokenizes chosen fields and the message body, enabling fast, flexible free-text search and rich aggregations — at the cost of real write amplification and storage overhead paid continuously, on every event. A label-and-chunk design indexes only a small set of low-cardinality labels (service, region, level) and stores raw content in compressed, text-scanned chunks — dramatically cheaper at ingest, with a real trade-off in free-text flexibility and a sharp failure mode if a high-cardinality value (a request_id, a user_id) is mistakenly used as a label.

Chapter 12

A Tenant-Scoped, Budgeted Query Layer

A query is not a direct call to storage. It passes through a frontend that authenticates, scopes, prunes, fans out under budget, and merges deterministically.

Swipe to pan
Query Path: Auth → Prune → Fan-Out → MergeBounded, budgeted, deterministic sort for stable paginationAuth + ScopeTime PruneShard AShard BShard CMerge + Sorttie-break by (ts, id)
Auth → tenant scope from credential → time prune → bounded parallel fan-out → deterministic merge and sort

Every query authenticates the caller and resolves the authorized tenant scope from that verified identity — never from a client-supplied tenant field, which is a hint at best, never an authorization boundary. The frontend prunes the search to the requested time range before fanning out to the shards or time-sliced chunks that could possibly match, running that fan-out in parallel under an enforced scan-byte and concurrency budget so one expensive query can't starve every other operator.

Results from multiple shards merge under a deterministic sort — timestamp plus a stable tie-break — because two shards can genuinely report events with the same timestamp, and pagination has to stay stable across that merge. With a consistently propagated trace_id, a correlated incident view across services becomes possible for the first time in this story.

Chapter 13

Hot, Warm, Cold — and Legal Hold

Not every log needs to live in the fastest, most expensive tier for its entire retention period.

Swipe to pan
Hot / Warm / Cold Retention Tiers (illustrative windows)Cost drops and query latency rises at each boundaryHot0–7 days · fastest, priciestWarm7–30 daysCold Archive30–90 days · object storage
Hot (0–7 days, fastest), warm (7–30 days), cold archive (30–90 days, object storage) — illustrative policy windows

An illustrative tiered policy ages recent, high-value logs through a fast hot tier, a cheaper warm tier, and finally a cold object-storage archive for long-term retention. These exact windows are policy choices, not laws of physics — a different compliance posture would choose different numbers. What matters is that the choice is explicit, that moving data between tiers is itself a monitored background process, and that querying the archive tier honestly costs more latency than the hot tier, not the same speed behind a spinner.

Legal hold overrides the schedule, by policy, not by a bigger TTL

A record flagged for legal hold bypasses its normal deletion lifecycle entirely, as a distinct, auditable control-plane decision — not simply a longer retention number. Standard debug logs still expire on schedule around it.

Chapter 14

Multi-Tenancy, Secrets, and the Admission Path

Every layer a log record passes through needs its own tenant-scoping check — a single missed layer can leak one tenant's data to another even if every other layer was correctly scoped.

Swipe to pan
tenant_id Is a Hint From the Client, Never the BoundaryAuthorized scope is always derived from the verified credentialCaller: tenant-betaclaims tenant_id=tenant-acmeQuery Plannerscope = credential403scope mismatch
tenant_id from the client is a hint; the authorized scope always comes from the verified credential

Isolation at every hop

Collector attribution, broker partition/topic scoping, index naming or tenant field, and query-time authorization all need an explicit tenant check — not just the first one at ingest. Audit this list the same way for every multi-tenant system in this curriculum.

Minimization before durability

Sensitive values are redacted before the record becomes durable anywhere, and access to search is scoped by identity, not by a value a caller can set. See Chapter 07 for where in the pipeline this happens and why the ordering matters.

Chapter 15

Multi-Region Residency and Watching the Watcher

At multi-region scale, two more constraints appear that don't exist in a single-region deployment: data residency and the question of who notices when logging itself breaks.

Swipe to pan
Independent Regional Pipelines, Governed FederationNo silent cross-region replication of raw log dataRegion: us-eastfull local pipelineRegion: eu-westresidency-bound pipelineFederated Querypolicy-governed, audited
Independent regional pipelines, with federation as an explicit, policy-governed query capability — never silent replication

No silent cross-region failover

Each region runs its own complete pipeline. A residency-bound tenant's logs stay in their region by default; a regional outage for that tenant means bounded local buffering and reduced availability, honestly, rather than routing their data across a boundary their compliance posture forbids.

Self-observability through an independent path

Agent drop rates, broker consumer lag, indexing delay, and query p99 must be monitored through an alerting path independent of the logging pipeline itself — if the only way to know logging is broken is to check the logs, a total outage becomes invisible exactly when it matters most.

Chapter 16

Final Architecture

Every component below exists because of a specific, demonstrated failure earlier in this story. A small, single-service deployment doesn't need every piece at once.

Swipe to pan
CO-9931: Generated to QueryableMilliseconds to durable, seconds to searchable — with one honest residual gapGeneratedBufferedSpooledCollectedDurableIndexedQueryableResidual uncertainty: ±4s clock skew across services — named, not hidden
CO-9931: generated to queryable, milliseconds to durable, seconds to searchable — with one honest residual gap
Swipe to pan
Distributed Logging, Final ArchitectureCollection, durable transport/indexing, and query — three independently-scaling planesAppsAgentSpoolCollectorLog BuspartitionedIndexerHot StoreArchiveQuery FEControl PlaneSelf-Observability
Collection, durable transport/indexing, and query — three independently-scaling planes, plus a control plane and self-observability
Key takeaways
  1. 01Agent + Spool: in-process async buffering, bounded disk spool, rotation-safe checkpointing — the application never blocks on logging
  2. 02Collector: authenticates by connection identity, redacts before durability, batches and compresses
  3. 03Log Bus: durable, replicated, partitioned — the real durability boundary, decoupling ingest from indexing
  4. 04Indexer: independent consumer group turning durable records into searchable documents, idempotent on replay
  5. 05Hot Store + Archive: tiered retention trading cost against query latency, with legal hold as an explicit override
  6. 06Query Frontend: identity-derived tenant scope, bounded and budgeted fan-out, deterministic merge
  7. 07Control Plane: identity, policy, retention and quota governance
  8. 08Self-Observability: independent alerting path so a logging outage can never hide itself