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
implementsusescreates
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.