Sweaty Imposter

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.

All OOD Prompts