System design · must-tier · asked at Amazon, Apple, Google, Meta, Microsoft, Netflix

Logging Framework

mediumlldchain-of-responsibilityasync

Prompt

"Design a logging library for a server. Code calls something like log.info('...'); the library decides whether to keep it and where it goes: console, a file, maybe a remote collector. Show me the classes and the path a message takes."

Clarifying questions

  • What levels? DEBUG, INFO, WARN, ERROR. A message is emitted if its level is at or above the logger's threshold.
  • Are loggers named and hierarchical? Yes: app.db.pool inherits configuration from app.db and app unless overridden, the way most production libraries work.
  • Can one message go to several destinations? Yes. Each logger can have multiple handlers, and a message propagates to ancestor loggers' handlers too.
  • Synchronous or asynchronous writes? Both must be possible. The interviewer will ask what happens when the file system stalls.
  • Formatting? Pluggable; a default line format with timestamp, level, logger name, message.
  • Structured fields? Key-value pairs attached to a record are a follow-up. Design the record so they fit.
  • Thread safety? Required; many request threads log concurrently.

Requirements

Functional

  • Named loggers with level thresholds inherited from ancestors.
  • Handlers attached to loggers; a record flows to the logger's handlers and up the ancestor chain.
  • Filters on handlers to drop records selectively.
  • Formatters to turn a record into a string.
  • An asynchronous handler that decouples callers from slow sinks.

Non-functional

  • A disabled level (debug when the threshold is INFO) must cost almost nothing: no record allocation, no string formatting.
  • Emitting a record must not block the caller on I/O when the async handler is used, and must not lose records silently under normal operation.
  • Ordering within one thread must be preserved.
  • Clean shutdown flushes pending records.

Numbers

  • A busy server: 2,000 requests per second x 5 log calls each = 10^4 calls per second, of which 90% are DEBUG and disabled. A disabled call at 100 ns is 1 ms per second total; at 5 microseconds (allocating and formatting) it is 50 ms per second, 5% of a core. That is the argument for the early level check.
  • Enabled records: 10^3 per second x 200 bytes = 200 KB per second to disk. Trivial for a file, not trivial for a remote collector with 50 ms round trips, which is the argument for batching in the async handler.

Last reviewed 8 September 2026