Problem 4.17 — Logging Framework
In one line
Design a mini SLF4J/Logback: log levels, multiple appenders (console, file, remote), formatters, per-logger configuration, and asynchronous writing that never blocks the application.
Think of it like this
A newsroom. Reporters (application code) write stories with an urgency label (log level); editors (filters) drop the unimportant ones; formatters lay them out; and the paper is printed, posted online, and archived (appenders), all without reporters waiting for the printing press.
Key ideas
- 01
Requirements: levels DEBUG < INFO < WARN < ERROR with a configurable threshold per logger; messages go to one or more APPENDERS; each appender has a FORMATTER; loggers are hierarchical by name (
com.shopconfigurescom.shop.orders); logging must be thread-safe and cheap when disabled. - 02
Patterns: SINGLETON/registry for the logger factory; STRATEGY for formatters; OBSERVER-like fan-out to appenders; CHAIN OF RESPONSIBILITY for filters; DECORATOR for an async appender that wraps any appender with a queue and a background thread (Phase 2).
- 03
Performance: check the level BEFORE building the message (parameterised messages
log.info("order {}", id)avoid string building when disabled); async appenders use a bounded queue and a policy when full (drop DEBUG, block, or discard) (Phase 13B, backpressure). - 04
Production reality: structured JSON output with context (MDC: request ID, trace ID) that log pipelines can parse (Stateful Systems course, log pipeline).
Code & diagrams
Levels, a logger that filters by level, pluggable formatters, and an async decorator.
enum Level { DEBUG, INFO, WARN, ERROR }
record LogEvent(Instant time, Level level, String logger, String message, Map<String, String> context) {}
interface Formatter { String format(LogEvent e); }
interface Appender { void append(LogEvent e); }
final class Logger {
private final String name;
private volatile Level threshold;
private final List<Appender> appenders;
Logger(String name, Level threshold, List<Appender> appenders) { this.name = name; this.threshold = threshold; this.appenders = appenders; }
void info(String pattern, Object... args) { log(Level.INFO, pattern, args); }
void error(String pattern, Object... args) { log(Level.ERROR, pattern, args); }
private void log(Level level, String pattern, Object... args) {
if (level.ordinal() < threshold.ordinal()) return; // cheap exit when disabled
String msg = MessageFormatter.format(pattern, args); // "{}" substitution
LogEvent e = new LogEvent(Instant.now(), level, name, msg, Mdc.copy());
for (Appender a : appenders) a.append(e);
}
}
final class AsyncAppender implements Appender { // Decorator
private final BlockingQueue<LogEvent> queue = new ArrayBlockingQueue<>(10_000);
AsyncAppender(Appender delegate) {
Thread t = new Thread(() -> { try { while (true) delegate.append(queue.take()); } catch (InterruptedException ignored) { } });
t.setDaemon(true); t.start();
}
public void append(LogEvent e) {
if (!queue.offer(e) && e.level().ordinal() >= Level.WARN.ordinal()) {
try { queue.put(e); } catch (InterruptedException ex) { Thread.currentThread().interrupt(); } // never drop WARN/ERROR
} // DEBUG/INFO dropped when full
}
}Explain without notes
Why use parameterised messages instead of string concatenation in log calls?
Practice
Add a rolling file appender that starts a new file at 100 MB and keeps the last 7 files.
Trade-offs
- ↔
Async logging protects latency but can lose buffered events on a crash; synchronous logging is safer for audit logs.
Run it in production
You've designed it. Now build, operate, and break the same idea hands-on in the DevOps courses:
Completion checklist
I can map logging concerns to Singleton, Strategy, Chain of Responsibility, and Decorator