Logging Framework
Design a logging library in the style of Log4j or Logback: named loggers, log levels, multiple destinations (console, file, and remote), configurable formats, and minimal overhead on the calling thread.
Clarifying Questions
- Which levels: TRACE, DEBUG, INFO, WARN, ERROR, and FATAL?
- Are loggers hierarchical, so com.app.db inherits from com.app?
- Must writes be synchronous for durability, or can they be asynchronous?
- When the async buffer fills, do we block, drop, or drop low levels first?
- Do we need structured key-value fields, or only message strings?
Core Requirements
- Create named loggers that inherit level and appenders from their parent.
- Filter messages below the effective level cheaply.
- Send each record to one or more appenders.
- Format records with a configurable layout, such as plain text or JSON.
- Support asynchronous writes with a bounded buffer.
- Change levels at runtime without a restart.
Canonical class breakdown
LoggerFactory: Creates and caches Loggers by name and builds the parent hierarchy.
Logger: Named entry point with debug, info, warn, and error methods; checks the level before building a record.
Level (enum): Ordered severities with a numeric rank for fast comparison.
LogRecord: Immutable event: timestamp, level, logger name, thread, message, fields, and throwable.
Appender (interface) / ConsoleAppender, FileAppender, RemoteAppender: Writes a formatted record to one destination.
Layout (interface) / PatternLayout, JsonLayout: Turns a LogRecord into bytes.
Filter (interface): Optional per-appender rule that accepts or rejects a record.
AsyncAppender: Decorator with a bounded queue and a worker thread that forwards records to a wrapped Appender.
Java sketchjava
// Java skeleton
enum Level {
TRACE(0), DEBUG(1), INFO(2), WARN(3), ERROR(4), FATAL(5);
final int rank;
Level(int rank) { this.rank = rank; }
}
record LogRecord(Instant timestamp, Level level, String loggerName, String thread,
String message, Map<String, String> fields, Throwable error) {}
interface Layout {
byte[] format(LogRecord r);
}
class PatternLayout implements Layout { ... }
class JsonLayout implements Layout { ... }
interface Filter {
boolean accept(LogRecord r);
}
interface Appender {
void append(LogRecord r);
void flush();
}
class ConsoleAppender implements Appender { ... }
class FileAppender implements Appender { ... }
class RemoteAppender implements Appender { ... }
class AsyncAppender implements Appender {
private final Appender delegate;
private final BlockingQueue<LogRecord> queue; // bounded
private final int discardThreshold; // drop TRACE, DEBUG, INFO when nearly full
public void append(LogRecord r) { ... }
public void flush() { /* drain queue */ }
}
class Logger {
private final String name;
private final Logger parent;
private volatile Level level; // null means inherit
private final List<Appender> appenders;
private final boolean additive; // also send to parent appenders
public boolean isEnabled(Level l) { return l.rank >= effectiveLevel().rank; }
public void info(String fmt, Object... args) { if (isEnabled(Level.INFO)) log(Level.INFO, fmt, args); }
public void debug(String fmt, Object... args) { ... }
public void error(String msg, Throwable t) { ... }
private void log(Level l, String fmt, Object[] args) { /* build record, call appenders up the chain */ }
private Level effectiveLevel() { /* walk parents, cached */ }
}
class LoggerFactory {
private static final ConcurrentHashMap<String, Logger> loggers = new ConcurrentHashMap<>();
public static Logger getLogger(String name) { /* builds parents from dotted name */ }
public static void setLevel(String name, Level level) { /* invalidates cached levels */ }
}
Key Decisions to Defend
- The level check runs before any record or message string is built, so disabled debug logging costs almost nothing.
- Appender and Layout are separate interfaces, so any destination can use any format.
- AsyncAppender is a Decorator around any Appender, which makes async delivery a config choice per destination.
- The effective level comes from walking the logger hierarchy, cached and invalidated on reconfigure.
- Following Logback, a nearly full async buffer discards TRACE, DEBUG, and INFO while still queuing WARN and ERROR.
Likely follow-ups
- · The remote appender's endpoint is down for an hour, what happens to memory?
- · Add a request ID to every log line without passing it through every call.
- · Rotate log files by size and date, which class owns that?
- · How do you make sure ERROR logs are flushed before the process crashes?
Dive Deeper
A logging framework tests Strategy, Decorator, and Chain of Responsibility in one small system, plus a real performance concern. Strong candidates make the disabled path nearly free, separate where logs go from how they look, and treat async delivery as a wrapper. The overflow policy is the deeper probe: a logger that blocks the app or silently drops errors under load causes outages of its own.