Logging Framework
A logger that can write to a console today and a file, or a network sink, tomorrow - without a single call site changing. The two decisions that matter are "should this line even be logged" and "where does it go once it is."
Requirements
Functional
- Code logs a message at a level (DEBUG, INFO, WARN, ERROR).
- A logger has a minimum level; messages below it are dropped before any formatting or writing happens.
- A message can be written to more than one destination at once (console and file, say).
- Each destination can format the same message differently (plain text on console, a structured line in a file).
Non-functional
- Adding a new destination (a network sink, a metrics pipeline) must not touch
Loggeror any existing appender. - Filtering by level must happen before formatting, so a dropped DEBUG line never pays the cost of building its output string.
Design
Logger checks the level once, then hands the message to every registered Appender.
Each appender owns its own Formatter and its own write target, so "log to two places in
two formats" is just "register two appenders," not a special case.
- 1The caller logs at a level without knowing where it will end up.
- 2The logger checks its threshold first - a level below it never reaches an appender.
- 3Every registered appender gets the same record, regardless of how many there are.
- 4The appender asks its own formatter for a string - console and file can format differently.
- 5Each appender writes to its own target: stdout, a file, or a network socket.
Splitting Formatter out of Appender means the same appender class can be reused with a
different format (say, a FileAppender with a JSON formatter instead of a plain one)
without writing a new appender at all.
Class diagram
Code
Design decisions
- Level filtering happens once in
Logger, not once per appender. If every appender re-checked "is this level enabled," a DEBUG line dropped by the logger's threshold would still get formatted and passed around per destination. Checking it once at the entry point means a disabled level costs nothing beyond a single comparison. Formatteris separate fromAppenderinstead of baked into it. An appender's job is "where does this go"; a formatter's job is "what does it look like." Keeping them separate means a console appender and a file appender can share the same plain-text formatter, or diverge, without either concern leaking into the other's class.- Appenders are a list the logger fans out to, not a single configured destination.
"Log to console and file" falls out of registering two appenders rather than requiring a
MultiAppenderwrapper class - the logger's fan-out loop already handles any number of them. - What's missing for a real system: this framework writes synchronously on the calling thread; a production logger would hand records to a bounded queue drained by a background writer so a slow file system or network sink can't add latency to the code path that's logging, and would support per-appender levels (route ERROR to a network sink, everything to a local file) rather than one global threshold.
Common follow-ups
- How would you route only ERROR logs to a network sink while keeping everything on the
console? Per-appender levels - each
Appenderwould need its own minimum level checked before formatting and writing, not justLogger's single global threshold. The page's own "what's missing" section flags exactly this gap. - What happens if one appender's
write()is slow, like a flaky network sink?Logger.log's loop calls each appender synchronously in sequence, so a slow appender blocks every appender after it, and blocks the caller's own code from continuing - the same synchronous-fan-out limitation the notification system page runs into, with the same fix: a queue per appender drained by a background writer. - How would you add log rotation to FileAppender without touching Logger? Entirely
inside
FileAppender- it already owns "where does this go," so rotation (checking file size or date before each write and rolling over) is local state and logic added to one class.LoggerandFormatternever need to know how an appender manages its target. - Why is checking the level threshold cheaper than formatting first and discarding?
Comparing two enum values is one integer comparison, while formatting builds a full
string. A dropped DEBUG line under the threshold never pays that cost, because the check
happens before a
LogRecordor any formatted string is even created.
Check yourself
Why does Logger.log check the level threshold before creating a LogRecord or calling any appender?