"Design a logging framework. It should support multiple severity levels, multiple output destinations (console, file, network), configurable formatting, and must not become a bottleneck for the application threads that call it."
B. Difficulty
Intermediate, but unusually dense with legitimate design patterns for a problem this small — often used specifically as a "how many patterns can you correctly justify, and which ones do you correctly avoid" showcase rather than a test of any single hard algorithm.
C. What This Problem Tests
Recognizing five plausible patterns in one problem and picking which actually earn their place
The one case in this document where Singleton is the right call — a direct, deliberate contrast to Problems 1 and 5's Singleton rejections
Decorator for cross-cutting behavior (async-ifying an appender) versus Strategy for swappable behavior (formatting)
Async I/O off the critical path without losing thread-safety
Judging when a pattern (here, Chain of Responsibility) is a thin, honest fit versus force-fitted
D. Requirements
Functional
Log messages at a severity level (DEBUG, INFO, WARN, ERROR, FATAL)
Filter by a configurable minimum level, globally or per-logger
Write to multiple destinations simultaneously (console, file, network)
Support pluggable message formatting (plain text, JSON)
Allow configuration (level, destinations, format) without restarting the application
Non-Functional
Logging must not meaningfully slow down the calling application thread, even when a destination (e.g. network) is slow
Thread-safe under concurrent logging from many application threads
Extensible to new destinations/formats without modifying existing code
No log message should silently vanish due to a race in configuration changes
E. Clarifying Questions
#
Question
Why It Matters
Design Decision Affected
1
Do we need per-logger (per-class/module) level thresholds, or one global level?
Determines whether Logger is keyed by name with its own config
LoggerFactory registry shape
2
Should logging be synchronous or asynchronous by default?
Drives the biggest architectural decision in this problem
Whether an async decorator wraps appenders by default
3
Can multiple appenders be attached to one logger at once?
Determines whether appenders form a list or a single reference
Logger holds a list, not one appender
4
Is log rotation (size/time-based file rollover) required?
Adds real complexity to FileAppender specifically
FileAppender scope
5
What happens if a destination is slow or down — block, drop, or buffer?
Do different appenders need independent minimum levels (e.g. console shows INFO+, file captures DEBUG+)?
Determines whether filtering happens once at the logger or again per-appender
Appender-level threshold field
7
Is structured/contextual logging (e.g. request-ID, user-ID tags) needed?
Affects whether LogRecord needs an extensible context map
LogRecord shape
F. Assumptions
Confirmed: five standard levels; multiple simultaneous destinations; pluggable formatting; runtime-reconfigurable.
Assumption: logging is asynchronous by default for any I/O-bound appender (file/network); console may remain synchronous since it's typically fast and used mostly in development.
Assumption: each appender may carry its own minimum level in addition to the logger's, matching how real frameworks (Log4j, SLF4J) behave.
Assumption: a bounded queue backs async appenders, with a stated, explicit overflow policy (not an unbounded queue that can OOM the process).
Assumption: log rotation is a stated extension (V-something) for FileAppender, not baseline.
G. Actors
Actor
Responsibility
Application code
Calls logger.info(...)/error(...) etc.
Operator/Config
Adjusts levels and destinations at runtime
System
Filters, formats, and dispatches records to destinations without blocking the caller unduly
Per-name entry point; owns a level threshold + a list of appenders
name, level, appenders
log(level, msg), convenience info()/error() etc.
Actual writing (delegates to appenders)
LogFormatter (interface)
Turns a LogRecord into a string
—
format(record)
Where that string goes
LogAppender (interface)
Writes a formatted record somewhere
own minLevel, a formatter
append(record)
Deciding message content
AsyncAppenderDecorator
Wraps any appender to move its I/O off the calling thread
bounded queue, worker thread
append(record) (enqueues instead of writing directly)
The actual write logic (delegates to the wrapped appender)
LoggerFactory / LogManager (Singleton)
Process-wide registry of loggers by name + global config
name→Logger map
getLogger(name)
Formatting or writing — purely a registry
I. Class Relationships
Composition: a Logger owns its list of LogAppenders; an AsyncAppenderDecorator owns the appender it wraps.
Dependency:LogAppender → LogFormatter (interface), and Logger → LogAppender (interface), both injected, not hardcoded.
Decorator, not Strategy, for async: making a slow appender non-blocking is a cross-cutting wrapper behavior applicable to any concrete appender — console, file, or network — not a mutually exclusive alternative implementation of "how to write." The same Decorator-vs-Strategy distinction drawn for Ride-Sharing's surge pricing applies here.
Strategy, correctly, for formatting:PlainTextFormatter vs. JsonFormatter genuinely are mutually exclusive alternative ways to render the same record — a textbook Strategy fit, unlike the async case.
Why Singleton here, unlike Problems 1 and 5: see M.
J. Class Diagram
View Mermaid source
classDiagram
class LogLevel {
<<enumeration>>
DEBUG
INFO
WARN
ERROR
FATAL
}
class LogRecord {
timestamp
level
message
loggerName
context
}
class Logger {
name
level
+log(level,msg)
}
class LogFormatter {
<<interface>>
+format(record)
}
class LogAppender {
<<interface>>
+append(record)
}
class AsyncAppenderDecorator {
-wrapped: LogAppender
+append(record)
}
class LoggerFactory {
+getLogger(name)
}
LoggerFactory --> Logger : creates/registers
Logger --> LogAppender
Logger --> LogFormatter
LogAppender <|.. ConsoleAppender
LogAppender <|.. FileAppender
LogAppender <|.. AsyncAppenderDecorator
AsyncAppenderDecorator --> LogAppender : wraps
LogFormatter <|.. PlainTextFormatter
LogFormatter <|.. JsonFormatter
K. Responsibility Mapping
Class
Responsibility
Does NOT Handle
Logger
Level filtering + fan-out to appenders
Formatting, writing, async-ing
LogFormatter
Record → string
Where the string is sent
LogAppender
String → a destination
Deciding if this record should even be written (that's the level check, done before append is called)
AsyncAppenderDecorator
Moving I/O off the caller's thread
What the I/O actually is (delegates)
LoggerFactory
One registry, one place configuration is read from
Filtering, formatting, writing
L. SOLID Analysis
Principle
How It's Applied
Naive Violation
SRP
Filtering (Logger), formatting (Formatter), writing (Appender), and async-ing (Decorator) are four separate concerns
One Logger class with an if (async) { ... } else { ... } branch and inline string-building for every destination
OCP
New destination = new LogAppender; new format = new LogFormatter; zero changes to Logger
if (dest == "file") ... else if (dest == "console") ... inline in Logger.log()
LSP
Every LogAppender, including the async decorator, honors the same append(record) contract and can be substituted anywhere a plain appender is expected
An async appender whose append() sometimes returns before enqueuing and sometimes blocks — inconsistent enough callers can't reason about it
ISP
One-method LogAppender and LogFormatter interfaces
A fat "OutputHandler" interface also demanding rotate(), compress(), authenticate()
DIP
Logger depends on the LogAppender/LogFormatter interfaces, injected via the factory/config
Logger directly calling System.out.println and file APIs inline
M. Design Patterns
Pattern
Problem It Solves
Where It Appears
When NOT to Use
Singleton
Process-wide, name-keyed access to loggers without threading a reference through every function signature in the codebase
LoggerFactory/LogManager
N/A here — see the explicit justification below
Strategy
Formatting genuinely varies by output shape (text vs. JSON)
LogFormatter hierarchy
Skip if only one format will ever exist
Decorator
Async-ifying I/O is a cross-cutting wrapper over any appender, not an alternative appender
AsyncAppenderDecorator
Skip if everything can safely stay synchronous (e.g. console-only tooling)
Chain of Responsibility (thin fit)
Each appender independently re-checks its own minimum level before acting, after the logger's own check already passed
Logger-level check → per-appender level check, each an independent link
Don't force a full CoR class hierarchy for this — a simple per-appender if is honest and sufficient; presenting it as "CoR" is fine conceptually but doesn't need heavyweight handler-chaining machinery
Builder (optional)
LogRecord has several optional fields (exception, context tags, thread name)
LogRecord.Builder
Skip if the record's fields are few and mostly required — a constructor is fine then
Why Singleton is actually correct here, unlike Problems 1 and 5: real logging frameworks (Log4j, java.util.logging, Python's logging module) all use a global, name-keyed registry, and for good reason — logging is called from every layer of a codebase, and threading a Logger instance through every constructor via DI would be pure ceremony with no real benefit. Unlike ParkingLot or RideService, a Logger is a nearly stateless dispatcher: tests don't need two independent parking lots or ride services, but they very much benefit from one global logging entry point they can point at a test appender. The risk Singleton usually carries — hidden coupling that makes unit testing hard — is mitigated by swapping appenders in tests, not the registry itself.
N. What Is Likely to Change?
Behavior
Current Design
Anticipated Change
Protecting Abstraction
Output destinations
Console, File, Network
Cloud log aggregators (e.g. a hosted logging service)
LogAppender interface
Format
Plain text, JSON
Structured binary (protobuf) for high-volume pipelines
LogFormatter interface
Delivery guarantee
Best-effort async, may drop under overload
Guaranteed delivery via a durable local buffer that survives a crash
V4: introduce named loggers via LoggerFactory, each with its own level.
V5: add AsyncAppenderDecorator with a bounded queue + explicit overflow policy.
V6: log rotation for FileAppender, structured context tags, runtime-reconfigurable levels without restart.
What stays stable:Logger.log()/convenience method signatures. New destinations, formats, and delivery guarantees are all additive via new implementations, not edits to Logger.
Ownership/validation notes:log() checks the logger's own level threshold before doing any formatting work — cheap early exit for filtered-out records is the whole point of the level check existing before formatting, not after.
Q. Pseudocode — Critical Logic
Logger.log(level, message, context):
if level < this.minLevel: return // cheap early exit, no formatting done
record = LogRecord.create(level, message, context, now(), currentThread())
for appender in this.appenders:
if level >= appender.minLevel: // per-appender re-check
appender.append(record)
AsyncAppenderDecorator.append(record):
if !queue.offer(record): // non-blocking enqueue attempt
applyOverflowPolicy(record) // e.g. drop-oldest, drop-this, or block briefly
return
// background worker thread, separate from callers:
loop:
record = queue.take()
formatted = formatter.format(record)
wrapped.writeRaw(formatted) // the actual, possibly slow, I/O
void log(LogLevel level, String message, Map<String, Object> context) {
if (level.compareTo(this.minLevel) < 0) return; // cheap early exit, no formatting done
LogRecord record = LogRecord.create(level, message, context, now(), Thread.currentThread());
for (LogAppender appender : this.appenders) {
if (level.compareTo(appender.getMinLevel()) >= 0) { // per-appender re-check
appender.append(record);
}
}
}
// AsyncAppenderDecorator
void append(LogRecord record) {
if (!queue.offer(record)) { // non-blocking enqueue attempt
applyOverflowPolicy(record); // e.g. drop-oldest, drop-this, or block briefly
return;
}
}
// background worker thread, separate from callers:
void run() {
while (true) {
LogRecord record = queue.take();
String formatted = formatter.format(record);
wrapped.writeRaw(formatted); // the actual, possibly slow, I/O
}
}
void Logger::log(LogLevel level, const std::string& message, const std::map<std::string, std::any>& context) {
if (level < minLevel_) return; // cheap early exit, no formatting done
auto record = LogRecord::create(level, message, context, now(), std::this_thread::get_id());
for (auto& appender : appenders_) {
if (level >= appender->minLevel()) { // per-appender re-check
appender->append(record);
}
}
}
void AsyncAppenderDecorator::append(const LogRecord& record) {
if (!queue_.offer(record)) { // non-blocking enqueue attempt
applyOverflowPolicy(record); // e.g. drop-oldest, drop-this, or block briefly
return;
}
}
// background worker thread, separate from callers:
void AsyncAppenderDecorator::run() {
while (true) {
auto record = queue_.take();
auto formatted = formatter_->format(record);
wrapped_->writeRaw(formatted); // the actual, possibly slow, I/O
}
}
log(level, message, context) {
if (level < this.minLevel) return; // cheap early exit, no formatting done
const record = LogRecord.create(level, message, context, now(), currentThread());
for (const appender of this.appenders) {
if (level >= appender.minLevel) { // per-appender re-check
appender.append(record);
}
}
}
// AsyncAppenderDecorator
append(record) {
if (!this.queue.offer(record)) { // non-blocking enqueue attempt
this.applyOverflowPolicy(record); // e.g. drop-oldest, drop-this, or block briefly
return;
}
}
// background worker thread, separate from callers:
async run() {
while (true) {
const record = await this.queue.take();
const formatted = this.formatter.format(record);
this.wrapped.writeRaw(formatted); // the actual, possibly slow, I/O
}
}
log(level: LogLevel, message: string, context: Map<string, unknown>): void {
if (level < this.minLevel) return; // cheap early exit, no formatting done
const record: LogRecord = LogRecord.create(level, message, context, now(), currentThread());
for (const appender of this.appenders) {
if (level >= appender.minLevel) { // per-appender re-check
appender.append(record);
}
}
}
// AsyncAppenderDecorator
append(record: LogRecord): void {
if (!this.queue.offer(record)) { // non-blocking enqueue attempt
this.applyOverflowPolicy(record); // e.g. drop-oldest, drop-this, or block briefly
return;
}
}
// background worker thread, separate from callers:
async run(): Promise<void> {
while (true) {
const record: LogRecord = await this.queue.take();
const formatted: string = this.formatter.format(record);
this.wrapped.writeRaw(formatted); // the actual, possibly slow, I/O
}
}
R. End-to-End Use Case
Application calls logger.error("payment failed", ex)
│
▼
Logger.log(ERROR, ...) → passes level check
│
▼
For each appender: ConsoleAppender (sync) and FileAppender wrapped in AsyncAppenderDecorator
│ │
▼ ▼
writes to stdout queue.offer(record) → returns immediately to caller
│
(background worker thread)
▼
JsonFormatter.format(record) → FileAppender.writeRaw(json)
S. Data Structures
Structure
Used For
Why
Alternative & Trade-off
Bounded blocking queue (or ring buffer)
AsyncAppenderDecorator's internal buffer
Decouples producer (app thread) speed from consumer (I/O) speed with a bounded memory footprint
A lock-free ring buffer (LMAX Disruptor-style) gives higher throughput under heavy contention at the cost of real implementation complexity — worth naming, rarely worth building live
Map<String, Logger>
LoggerFactory registry
O(1) lookup by name, matches every real-world logging framework's pattern
A hierarchical name tree (like Log4j's dotted-package hierarchy with level inheritance) is more powerful but unnecessary unless explicitly asked for
T. Complexity Analysis
Operation
Time
Space
Logger.log (filtered out)
O(1)
—
Logger.log (passes filter, N appenders)
O(N) dispatch, formatting cost is separate per appender
O(1) per record before queuing
AsyncAppenderDecorator.append
O(1) amortized (queue offer)
O(queue capacity)
U. Testing Strategy
Happy path: a message at or above the configured level reaches every attached appender in the expected format.
Filtering: a message below the configured level is dropped before any formatter or appender runs (assert the formatter was never called — cheap-exit verification).
Async correctness: under a slow/blocked wrapped appender, the calling thread's append() still returns quickly (bounded by the queue, not by the slow I/O).
Overflow policy: when the queue is full, the configured policy (drop-oldest/drop-newest/block-briefly) behaves exactly as specified, with a countable metric for dropped records.
Concurrency tests: many threads logging simultaneously through one shared Logger — no corrupted/interleaved output, no lost records under the queue's stated capacity.
V. Interviewer Follow-Ups
Follow-Up
Testing
Design Change
Classes Changed
Pattern/Trade-off
"Logging is slowing down our hot path."
Recognizing the sync/async trade-off unprompted
Wrap the offending appender in AsyncAppenderDecorator
None beyond composition — no changes to Logger
Validates Decorator's whole reason for existing
"We're losing log messages under heavy load."
Naming a concrete overflow policy instead of hand-waving
Explicit bounded-queue policy: block-briefly / drop-oldest / drop-newest, each named with a trade-off
AsyncAppenderDecorator
An unbounded queue "fixes" this by risking OOM instead — call that out as the wrong answer
"Add log rotation by size and by day."
Extensibility within a single appender, not a redesign
FileAppender gains a rotation policy checked before each write
FileAppender only
Contained change — doesn't ripple into Logger or other appenders
"Why is LoggerFactory a Singleton but ParkingLot wasn't?"
Judgment, not pattern-matching by rote
N/A — this is a discussion question
N/A
See M's explicit justification; a strong candidate can articulate the difference unprompted
"Support per-request correlation IDs across log lines."
Context propagation without a global mutable variable
LogRecord.context map populated from a thread-local or explicit parameter at call sites
LogRecord, call-site discipline
Thread-locals are convenient but leak across thread-pool reuse if not cleared — worth flagging
W. Concurrency
Shared mutable state: a Logger's appender list (if reconfigured at runtime) and each async appender's internal queue.
Producer side (app threads): many threads call log() concurrently on the same shared Logger. The level check and formatting are stateless per-call, so the only real shared state is the appender list and the queue — both need thread-safe structures (a copy-on-write list for appenders, a genuinely concurrent queue for async buffering).
Consumer side: a single background worker thread per async appender avoids interleaved/corrupted writes to one destination without needing a lock on every write — trading potential throughput (one consumer) for simplicity and ordering guarantees (writes to one file stay in the order they were enqueued).
Runtime reconfiguration race: changing a logger's level or appender list while another thread is mid-log() call must not crash or silently corrupt state — an immutable snapshot swap (replace the whole appender list reference atomically) is simpler to reason about than mutating a shared list in place.
X. Persistence
Logs are themselves usually the "persistence layer" for other systems' auditing, but the logging framework's own concern is durability-vs-latency for its own queue: an in-memory bounded queue is fast but loses buffered-not-yet-written records on a crash; a queue backed by a small local disk-based buffer survives a crash but costs latency on every enqueue. Naming this trade-off explicitly, and picking in-memory-with-a-stated-loss-window as the default (since logging is rarely the system of record for financial-grade durability), reads stronger than assuming one is obviously correct.
Y. Scaling Boundary
Layer
What Belongs Here
LLD
Logger/Appender/Formatter classes, the async decorator, the level-filtering logic, the registry
HLD
A log shipping/aggregation pipeline (e.g. Fluentd/Logstash-style forwarders), centralized log storage and indexing (e.g. an ELK-style stack)
Distributed Systems
Cross-service correlation IDs, sampling strategies under extreme volume, and log-pipeline backpressure across many producing services
Z. Failure Scenarios
Destination unreachable (network appender, remote host down): the async queue absorbs a burst, but a sustained outage eventually triggers the overflow policy — must degrade gracefully, not crash the application.
Process crash with records still queued: those records are lost under an in-memory queue — an accepted, named trade-off, not a silent gap.
Formatter throws an exception (e.g. malformed context data): must not propagate back to the calling application thread and crash unrelated business logic — logging failures should be swallowed/self-logged, never fatal to the caller.
AA. Common Bad Designs
One giant Logger class with an if/else per destination and per format inline — defeats the entire point of this problem.
Synchronous logging to a slow network destination on the hot path — silently turns "add a log line" into "add unbounded latency."
An unbounded async queue "to never lose a message" — trades a visible, boring backpressure problem for an invisible, catastrophic OOM risk under sustained overload.
Global mutable static fields for configuration instead of a proper Singleton registry — works by accident, but has no clear lifecycle, no clean way to swap for tests, and no encapsulation.
Formatter logic embedded inside each Appender — duplicates formatting code across every destination instead of sharing one LogFormatter.
AB. Design Trade-Offs
Decision
Option A
Option B
Trade-off
Sync vs. async appenders
Synchronous, simple, in-order
Async via Decorator + queue
Sync is simplest and never loses ordering guarantees; async protects app latency but adds an overflow policy to reason about
Queue overflow policy
Drop-oldest/newest
Block briefly
Dropping preserves app latency but loses data; blocking preserves data but reintroduces the latency async was meant to avoid
Registry pattern
Singleton LoggerFactory
Full dependency injection everywhere
Singleton matches how every real framework does it and avoids ceremony; DI is more "pure" but impractical at the call-site density logging requires
AC. Interviewer Evaluation
Weak candidate: one class, hardcoded console output, string-concatenation formatting inline.
Average candidate: has Appender and Formatter interfaces, but logs synchronously everywhere with no thought to blocking I/O.
Strong candidate: clean Strategy (formatter) + Decorator (async) split, explicit overflow policy, and can justify Singleton here specifically because of logging's call-site ubiquity.
Exceptional candidate: all of the above, plus draws the explicit contrast with why Singleton was rejected in other problems in this set, and names a lock-free ring buffer as the real-world high-throughput answer without over-building it live.
AD. Interview Simulation
Interviewer: Design a logging framework.
Candidate: Quick scoping question — should logging ever block the calling thread, say if a network destination is slow?
Interviewer: Good question. It should not block meaningfully.
Candidate: Then I'll separate "what gets written" (a LogFormatter strategy) from "where it goes" (a LogAppender interface), and wrap any I/O-bound appender in an AsyncAppenderDecorator that enqueues onto a bounded queue and lets a background thread do the actual write.
Interviewer: What happens if that queue fills up?
Candidate: That needs an explicit policy — drop the oldest record, drop the new one, or block briefly. An unbounded queue would "solve" it by risking an out-of-memory crash instead, which is worse than dropping a few log lines.
Interviewer: You used a Singleton for the logger registry — didn't you argue against Singletons in the parking lot problem?
Candidate: Intentionally, yes — the difference is call-site density and statefulness. A parking lot has real state you want isolated and testable per-instance; a logger registry is called from every layer of the codebase, is nearly stateless, and every production logging framework I know of uses exactly this pattern. Singleton isn't wrong by default — it's wrong when it hides meaningful state that testing needs to control.
LogLevel enum, LogMessage, LogAppender interface (Console/File/Database), Logger as a Singleton, explicit thread-safety requirement stated
Correctly identifies Singleton as appropriate here — validated and expanded on in M above
No formatter abstraction (formatting appears baked into appenders); thread-safety is stated as a requirement but the specific mechanism isn't detailed
The Singleton-is-fine judgment call — one of the only reference solutions in this set where the default pattern choice is fully endorsed rather than critiqued
Real-world frameworks (Log4j / java.util.logging / Python logging) — general prior knowledge, not a single URL
Named-logger hierarchies, appender chains, level inheritance from parent loggers
Battle-tested at massive scale; the async-appender pattern here mirrors Log4j's AsyncAppender directly
Production frameworks add hierarchical level inheritance this document intentionally simplifies away for interview scope
The registry-by-name pattern and per-appender level override, both adopted directly here
AF. Interview Cheat Sheet
Problem
Logging Framework
Difficulty
Intermediate (pattern showcase)
Core requirements
Levels, multiple simultaneous destinations, pluggable formatting, non-blocking under load
Key clarifications
Per-logger levels? Sync or async by default? Multiple appenders at once? Rotation needed? Overflow behavior?
Decorator for async, not a parallel "AsyncAppender" strategy — and correctly defending Singleton for the registry
Likely-changing behavior
New destinations, new formats, delivery-guarantee strength, filtering granularity
Concurrency concern
Bounded queue + explicit overflow policy; single consumer thread per destination for write ordering
Top follow-ups
Hot-path latency, message loss under load, log rotation, Singleton justification, correlation-ID propagation
Common mistakes
Synchronous slow appenders on the hot path; unbounded queues; formatting logic duplicated per appender; global mutable statics instead of a real registry
What NOT to over-engineer
Don't build a lock-free ring buffer live — name it as the real-world high-throughput answer and move on
AG. Full Implementation
A complete, compilable reference implementation of every class in the diagram above (composition, inheritance, interface realization, and dependency-injected strategies all preserved per section I), with a runnable demo exercising the primary flow — in Java, C++, JavaScript, and TypeScript.
// Logging Framework — full LLD implementation with demo.
// Single-file Java translation of the Mermaid class diagram in section J.
//
// Note on concurrency: this problem's difficulty badge ("Intermediate — Pattern Showcase") does not
// call out concurrency, so no real locks/threads are used here. AsyncAppenderDecorator's queue-drain
// is exposed as an explicit flush() rather than a background java.lang.Thread, to keep the demo
// single-threaded and deterministic while still faithfully modeling the enqueue/overflow/drain logic.
import java.io.*;
import java.util.*;
public class p6 {
// ---------- LogLevel ----------
enum LogLevel { DEBUG, INFO, WARN, ERROR, FATAL } // ordinal() gives natural severity ordering
// ---------- LogRecord ----------
/** Immutable snapshot of one log event. Does not decide whether/where it gets written. */
static final class LogRecord {
final long timestamp;
final LogLevel level;
final String message;
final String loggerName;
final String threadName;
final Map<String, Object> context;
final String exceptionInfo; // nullable summary of an attached Throwable
LogRecord(LogLevel level, String message, String loggerName, Map<String, Object> context, String exceptionInfo) {
this.timestamp = System.currentTimeMillis();
this.level = level; this.message = message; this.loggerName = loggerName;
this.threadName = Thread.currentThread().getName();
this.context = context == null ? Collections.emptyMap() : context;
this.exceptionInfo = exceptionInfo;
}
}
// ---------- LogFormatter (Strategy: mutually exclusive rendering choices) ----------
interface LogFormatter { String format(LogRecord record); }
static final class PlainTextFormatter implements LogFormatter {
public String format(LogRecord r) {
StringBuilder sb = new StringBuilder();
sb.append('[').append(r.timestamp).append("] ").append(r.level).append(' ')
.append('[').append(r.loggerName).append("] [").append(r.threadName).append("] ")
.append(r.message);
if (!r.context.isEmpty()) sb.append(' ').append(r.context);
if (r.exceptionInfo != null) sb.append(" EXCEPTION: ").append(r.exceptionInfo);
return sb.toString();
}
}
static final class JsonFormatter implements LogFormatter {
public String format(LogRecord r) {
StringBuilder sb = new StringBuilder("{");
sb.append("\"timestamp\":").append(r.timestamp).append(',');
sb.append("\"level\":\"").append(r.level).append("\",");
sb.append("\"logger\":\"").append(r.loggerName).append("\",");
sb.append("\"thread\":\"").append(r.threadName).append("\",");
sb.append("\"message\":\"").append(escape(r.message)).append("\",");
sb.append("\"context\":{");
boolean first = true;
for (Map.Entry<String, Object> e : r.context.entrySet()) {
if (!first) sb.append(',');
sb.append('"').append(e.getKey()).append("\":\"").append(escape(String.valueOf(e.getValue()))).append('"');
first = false;
}
sb.append('}');
if (r.exceptionInfo != null) sb.append(",\"exception\":\"").append(escape(r.exceptionInfo)).append('"');
sb.append('}');
return sb.toString();
}
private String escape(String s) { return s.replace("\"", "\\\""); }
}
// ---------- LogAppender (Strategy-like injected destinations) ----------
interface LogAppender {
void append(LogRecord record);
LogLevel getMinLevel();
}
static final class ConsoleAppender implements LogAppender {
private final LogLevel minLevel;
private final LogFormatter formatter; // dependency, not hardcoded
ConsoleAppender(LogLevel minLevel, LogFormatter formatter) { this.minLevel = minLevel; this.formatter = formatter; }
public void append(LogRecord record) { System.out.println(formatter.format(record)); }
public LogLevel getMinLevel() { return minLevel; }
}
static final class FileAppender implements LogAppender {
private final LogLevel minLevel;
private final LogFormatter formatter;
private final String path;
FileAppender(LogLevel minLevel, LogFormatter formatter, String path) {
this.minLevel = minLevel; this.formatter = formatter; this.path = path;
}
public void append(LogRecord record) {
try (BufferedWriter w = new BufferedWriter(new FileWriter(path, true))) {
w.write(formatter.format(record));
w.newLine();
} catch (IOException e) {
System.err.println("FileAppender write failed: " + e.getMessage());
}
}
public LogLevel getMinLevel() { return minLevel; }
}
/**
* Decorator: wraps ANY appender to move its I/O off the calling thread. Applies to
* console/file/network alike — a cross-cutting wrapper, not a rival implementation of "how to write".
*/
static final class AsyncAppenderDecorator implements LogAppender {
private final LogAppender wrapped;
private final int capacity;
private final Deque<LogRecord> queue = new ArrayDeque<>();
AsyncAppenderDecorator(LogAppender wrapped, int capacity) { this.wrapped = wrapped; this.capacity = capacity; }
public void append(LogRecord record) {
if (queue.size() >= capacity) {
queue.pollFirst(); // overflow policy: drop-oldest to keep the caller non-blocking
}
queue.offerLast(record); // non-blocking enqueue
}
/** Simulates the background worker thread draining the queue and doing the real (slow) I/O. */
void flush() {
LogRecord r;
while ((r = queue.pollFirst()) != null) {
wrapped.append(r); // the actual write logic is delegated to the wrapped appender
}
}
public LogLevel getMinLevel() { return wrapped.getMinLevel(); }
}
// ---------- Logger ----------
/** Per-name entry point: owns a level threshold + a list of appenders. */
static final class Logger {
private final String name;
private LogLevel level;
private final List<LogAppender> appenders = new ArrayList<>(); // composition
Logger(String name, LogLevel level) { this.name = name; this.level = level; }
void setLevel(LogLevel level) { this.level = level; }
void addAppender(LogAppender appender) { appenders.add(appender); }
void log(LogLevel level, String message, Map<String, Object> context, String exceptionInfo) {
if (level.ordinal() < this.level.ordinal()) return; // cheap early exit, no formatting done
LogRecord record = new LogRecord(level, message, name, context, exceptionInfo);
for (LogAppender appender : appenders) {
if (level.ordinal() >= appender.getMinLevel().ordinal()) { // per-appender re-check
appender.append(record);
}
}
}
void debug(String msg) { log(LogLevel.DEBUG, msg, null, null); }
void info(String msg) { log(LogLevel.INFO, msg, null, null); }
void warn(String msg) { log(LogLevel.WARN, msg, null, null); }
void error(String msg, Throwable t) { log(LogLevel.ERROR, msg, null, describe(t)); }
void fatal(String msg, Throwable t) { log(LogLevel.FATAL, msg, null, describe(t)); }
private String describe(Throwable t) { return t == null ? null : t.getClass().getSimpleName() + ": " + t.getMessage(); }
}
// ---------- LoggerFactory (Singleton registry) ----------
static final class LoggerFactory {
private static final LoggerFactory INSTANCE = new LoggerFactory();
private final Map<String, Logger> registry = new HashMap<>();
private LogLevel globalDefaultLevel = LogLevel.INFO;
private LoggerFactory() {}
static Logger getLogger(String name) {
return INSTANCE.registry.computeIfAbsent(name, n -> new Logger(n, INSTANCE.globalDefaultLevel));
}
static void setGlobalDefaultLevel(LogLevel level) { INSTANCE.globalDefaultLevel = level; }
}
// ---------- Demo ----------
public static void main(String[] args) throws Exception {
String logFile = "p6-demo.log";
new File(logFile).delete(); // start clean
Logger appLogger = LoggerFactory.getLogger("app.orders");
appLogger.setLevel(LogLevel.DEBUG);
appLogger.addAppender(new ConsoleAppender(LogLevel.INFO, new PlainTextFormatter())); // console: INFO+
FileAppender fileAppender = new FileAppender(LogLevel.DEBUG, new JsonFormatter(), logFile); // file: DEBUG+ as JSON
AsyncAppenderDecorator asyncFile = new AsyncAppenderDecorator(fileAppender, 100);
appLogger.addAppender(asyncFile);
appLogger.debug("Loading order pipeline"); // filtered out of console (INFO min) but queued for file
appLogger.info("Order OR-1 received");
try {
throw new IllegalStateException("payment gateway timeout");
} catch (Exception e) {
appLogger.error("Order OR-1 failed", e);
}
appLogger.warn("Retrying order OR-1");
appLogger.fatal("Out of memory processing OR-1", new OutOfMemoryError("heap exhausted"));
System.out.println("--- flushing async file appender ---");
asyncFile.flush(); // simulate the background worker draining the queue
System.out.println("--- file contents (" + logFile + ") ---");
try (BufferedReader r = new BufferedReader(new FileReader(logFile))) {
String line;
while ((line = r.readLine()) != null) System.out.println(line);
}
new File(logFile).delete(); // cleanup after demo
}
}
// Logging Framework — full LLD implementation with demo.
// Single-file C++ translation of the Mermaid class diagram in section J.
//
// Note on concurrency: this problem's difficulty badge ("Intermediate — Pattern Showcase") does not
// call out concurrency, so no real mutexes/threads are used here. AsyncAppenderDecorator's queue-drain
// is exposed as an explicit flush() rather than a background std::thread, to keep the demo
// single-threaded and deterministic while still faithfully modeling the enqueue/overflow/drain logic.
#include <iostream>
#include <fstream>
#include <sstream>
#include <string>
#include <vector>
#include <deque>
#include <map>
#include <memory>
#include <chrono>
#include <stdexcept>
#include <cstdio>
// ---------- LogLevel ----------
enum class LogLevel { DEBUG = 0, INFO = 1, WARN = 2, ERROR = 3, FATAL = 4 };
std::string levelName(LogLevel l) {
switch (l) {
case LogLevel::DEBUG: return "DEBUG";
case LogLevel::INFO: return "INFO";
case LogLevel::WARN: return "WARN";
case LogLevel::ERROR: return "ERROR";
case LogLevel::FATAL: return "FATAL";
}
return "?";
}
// ---------- LogRecord ----------
// Immutable snapshot of one log event. Does not decide whether/where it gets written.
struct LogRecord {
long long timestamp;
LogLevel level;
std::string message;
std::string loggerName;
std::string threadName;
std::map<std::string, std::string> context; // ordered by key for deterministic output
std::string exceptionInfo; // empty => none
LogRecord(LogLevel level, std::string message, std::string loggerName,
std::map<std::string, std::string> context, std::string exceptionInfo)
: level(level), message(std::move(message)), loggerName(std::move(loggerName)),
threadName("main"), context(std::move(context)), exceptionInfo(std::move(exceptionInfo)) {
timestamp = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()).count();
}
};
// ---------- LogFormatter (Strategy: mutually exclusive rendering choices) ----------
class LogFormatter {
public:
virtual ~LogFormatter() = default;
virtual std::string format(const LogRecord& record) = 0;
};
class PlainTextFormatter : public LogFormatter {
public:
std::string format(const LogRecord& r) override {
std::ostringstream os;
os << "[" << r.timestamp << "] " << levelName(r.level) << " [" << r.loggerName << "] ["
<< r.threadName << "] " << r.message;
if (!r.context.empty()) {
os << " {";
bool first = true;
for (auto& [k, v] : r.context) { if (!first) os << ", "; os << k << "=" << v; first = false; }
os << "}";
}
if (!r.exceptionInfo.empty()) os << " EXCEPTION: " << r.exceptionInfo;
return os.str();
}
};
class JsonFormatter : public LogFormatter {
static std::string escape(const std::string& s) {
std::string out;
for (char c : s) { if (c == '"') out += "\\\""; else out += c; }
return out;
}
public:
std::string format(const LogRecord& r) override {
std::ostringstream os;
os << "{\"timestamp\":" << r.timestamp << ",\"level\":\"" << levelName(r.level) << "\","
<< "\"logger\":\"" << r.loggerName << "\",\"thread\":\"" << r.threadName << "\","
<< "\"message\":\"" << escape(r.message) << "\",\"context\":{";
bool first = true;
for (auto& [k, v] : r.context) { if (!first) os << ","; os << "\"" << k << "\":\"" << escape(v) << "\""; first = false; }
os << "}";
if (!r.exceptionInfo.empty()) os << ",\"exception\":\"" << escape(r.exceptionInfo) << "\"";
os << "}";
return os.str();
}
};
// ---------- LogAppender (injected destinations) ----------
class LogAppender {
public:
virtual ~LogAppender() = default;
virtual void append(const LogRecord& record) = 0;
virtual LogLevel getMinLevel() const = 0;
};
class ConsoleAppender : public LogAppender {
LogLevel minLevel;
std::shared_ptr<LogFormatter> formatter; // dependency, not hardcoded
public:
ConsoleAppender(LogLevel minLevel, std::shared_ptr<LogFormatter> formatter)
: minLevel(minLevel), formatter(std::move(formatter)) {}
void append(const LogRecord& record) override { std::cout << formatter->format(record) << "\n"; }
LogLevel getMinLevel() const override { return minLevel; }
};
class FileAppender : public LogAppender {
LogLevel minLevel;
std::shared_ptr<LogFormatter> formatter;
std::string path;
public:
FileAppender(LogLevel minLevel, std::shared_ptr<LogFormatter> formatter, std::string path)
: minLevel(minLevel), formatter(std::move(formatter)), path(std::move(path)) {}
void append(const LogRecord& record) override {
std::ofstream out(path, std::ios::app);
if (!out) { std::cerr << "FileAppender write failed\n"; return; }
out << formatter->format(record) << "\n";
}
LogLevel getMinLevel() const override { return minLevel; }
};
// Decorator: wraps ANY appender to move its I/O off the calling thread. Applies to
// console/file/network alike — a cross-cutting wrapper, not a rival implementation of "how to write".
class AsyncAppenderDecorator : public LogAppender {
std::shared_ptr<LogAppender> wrapped;
size_t capacity;
std::deque<LogRecord> queue;
public:
AsyncAppenderDecorator(std::shared_ptr<LogAppender> wrapped, size_t capacity)
: wrapped(std::move(wrapped)), capacity(capacity) {}
void append(const LogRecord& record) override {
if (queue.size() >= capacity) queue.pop_front(); // overflow policy: drop-oldest
queue.push_back(record); // non-blocking enqueue
}
// Simulates the background worker thread draining the queue and doing the real (slow) I/O.
void flush() {
while (!queue.empty()) {
wrapped->append(queue.front()); // the actual write logic is delegated to the wrapped appender
queue.pop_front();
}
}
LogLevel getMinLevel() const override { return wrapped->getMinLevel(); }
};
// ---------- Logger ----------
// Per-name entry point: owns a level threshold + a list of appenders.
class Logger {
std::string name;
LogLevel level;
std::vector<std::shared_ptr<LogAppender>> appenders; // composition
static std::string describe(const std::exception* e, const std::string& typeName) {
if (!e) return "";
return typeName + ": " + e->what();
}
public:
Logger(std::string name, LogLevel level) : name(std::move(name)), level(level) {}
void setLevel(LogLevel l) { level = l; }
void addAppender(std::shared_ptr<LogAppender> appender) { appenders.push_back(std::move(appender)); }
void log(LogLevel lvl, const std::string& message, const std::map<std::string, std::string>& context,
const std::string& exceptionInfo) {
if ((int)lvl < (int)level) return; // cheap early exit, no formatting done
LogRecord record(lvl, message, name, context, exceptionInfo);
for (auto& appender : appenders) {
if ((int)lvl >= (int)appender->getMinLevel()) { // per-appender re-check
appender->append(record);
}
}
}
void debug(const std::string& msg) { log(LogLevel::DEBUG, msg, {}, ""); }
void info(const std::string& msg) { log(LogLevel::INFO, msg, {}, ""); }
void warn(const std::string& msg) { log(LogLevel::WARN, msg, {}, ""); }
void error(const std::string& msg, const std::exception& e, const std::string& typeName) {
log(LogLevel::ERROR, msg, {}, describe(&e, typeName));
}
void fatal(const std::string& msg, const std::exception& e, const std::string& typeName) {
log(LogLevel::FATAL, msg, {}, describe(&e, typeName));
}
};
// ---------- LoggerFactory (Singleton registry) ----------
class LoggerFactory {
std::map<std::string, std::shared_ptr<Logger>> registry;
LogLevel globalDefaultLevel = LogLevel::INFO;
LoggerFactory() = default;
static LoggerFactory& instance() { static LoggerFactory f; return f; }
public:
static std::shared_ptr<Logger> getLogger(const std::string& name) {
auto& f = instance();
auto it = f.registry.find(name);
if (it != f.registry.end()) return it->second;
auto logger = std::make_shared<Logger>(name, f.globalDefaultLevel);
f.registry[name] = logger;
return logger;
}
static void setGlobalDefaultLevel(LogLevel level) { instance().globalDefaultLevel = level; }
};
// ---------- Demo ----------
int main() {
const std::string logFile = "p6-demo.log";
std::remove(logFile.c_str()); // start clean
auto appLogger = LoggerFactory::getLogger("app.orders");
appLogger->setLevel(LogLevel::DEBUG);
appLogger->addAppender(std::make_shared<ConsoleAppender>(LogLevel::INFO, std::make_shared<PlainTextFormatter>()));
auto fileAppender = std::make_shared<FileAppender>(LogLevel::DEBUG, std::make_shared<JsonFormatter>(), logFile);
auto asyncFile = std::make_shared<AsyncAppenderDecorator>(fileAppender, 100);
appLogger->addAppender(asyncFile);
appLogger->debug("Loading order pipeline"); // filtered out of console (INFO min) but queued for file
appLogger->info("Order OR-1 received");
try {
throw std::runtime_error("payment gateway timeout");
} catch (const std::exception& e) {
appLogger->error("Order OR-1 failed", e, "runtime_error");
}
appLogger->warn("Retrying order OR-1");
try {
throw std::bad_alloc();
} catch (const std::exception& e) {
appLogger->fatal("Out of memory processing OR-1", e, "bad_alloc");
}
std::cout << "--- flushing async file appender ---\n";
asyncFile->flush(); // simulate the background worker draining the queue
std::cout << "--- file contents (" << logFile << ") ---\n";
std::ifstream in(logFile);
std::string line;
while (std::getline(in, line)) std::cout << line << "\n";
in.close();
std::remove(logFile.c_str()); // cleanup after demo
return 0;
}
// Logging Framework — full LLD implementation with demo.
// Single-file Node/JS translation of the Mermaid class diagram in section J.
//
// Note on concurrency: this problem's difficulty badge ("Intermediate — Pattern Showcase") does not
// call out concurrency, so no real synchronization primitives are used here (Node is single-threaded
// anyway). AsyncAppenderDecorator's queue-drain is exposed as an explicit flush() rather than a
// setImmediate/microtask loop, to keep the demo deterministic while faithfully modeling the
// enqueue/overflow/drain logic described in the hot-path pseudocode.
'use strict';
const fs = require('fs');
// ---------- LogLevel ----------
const LogLevel = Object.freeze({ DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3, FATAL: 4 });
const LogLevelName = Object.freeze({ 0: 'DEBUG', 1: 'INFO', 2: 'WARN', 3: 'ERROR', 4: 'FATAL' });
// ---------- LogRecord ----------
// Immutable snapshot of one log event. Does not decide whether/where it gets written.
class LogRecord {
constructor(level, message, loggerName, context, exceptionInfo) {
this.timestamp = Date.now();
this.level = level;
this.message = message;
this.loggerName = loggerName;
this.threadName = 'main'; // JS has no real threads; kept for structural fidelity with the diagram
this.context = context || {};
this.exceptionInfo = exceptionInfo || null;
}
}
// ---------- LogFormatter (Strategy: mutually exclusive rendering choices) ----------
class LogFormatter {
format(record) { throw new Error('not implemented'); } // eslint-disable-line no-unused-vars
}
class PlainTextFormatter extends LogFormatter {
format(r) {
let s = `[${r.timestamp}] ${LogLevelName[r.level]} [${r.loggerName}] [${r.threadName}] ${r.message}`;
const keys = Object.keys(r.context);
if (keys.length > 0) s += ` {${keys.map((k) => `${k}=${r.context[k]}`).join(', ')}}`;
if (r.exceptionInfo) s += ` EXCEPTION: ${r.exceptionInfo}`;
return s;
}
}
class JsonFormatter extends LogFormatter {
format(r) {
const obj = {
timestamp: r.timestamp, level: LogLevelName[r.level], logger: r.loggerName,
thread: r.threadName, message: r.message, context: r.context,
};
if (r.exceptionInfo) obj.exception = r.exceptionInfo;
return JSON.stringify(obj);
}
}
// ---------- LogAppender (injected destinations) ----------
class LogAppender {
append(record) { throw new Error('not implemented'); } // eslint-disable-line no-unused-vars
getMinLevel() { throw new Error('not implemented'); }
}
class ConsoleAppender extends LogAppender {
constructor(minLevel, formatter) { super(); this.minLevel = minLevel; this.formatter = formatter; }
append(record) { console.log(this.formatter.format(record)); }
getMinLevel() { return this.minLevel; }
}
class FileAppender extends LogAppender {
constructor(minLevel, formatter, path) { super(); this.minLevel = minLevel; this.formatter = formatter; this.path = path; }
append(record) {
try {
fs.appendFileSync(this.path, this.formatter.format(record) + '\n');
} catch (e) {
console.error('FileAppender write failed:', e.message);
}
}
getMinLevel() { return this.minLevel; }
}
// Decorator: wraps ANY appender to move its I/O off the calling thread. Applies to
// console/file/network alike — a cross-cutting wrapper, not a rival implementation of "how to write".
class AsyncAppenderDecorator extends LogAppender {
constructor(wrapped, capacity) { super(); this.wrapped = wrapped; this.capacity = capacity; this.queue = []; }
append(record) {
if (this.queue.length >= this.capacity) this.queue.shift(); // overflow policy: drop-oldest
this.queue.push(record); // non-blocking enqueue
}
// Simulates the background worker thread draining the queue and doing the real (slow) I/O.
flush() {
while (this.queue.length > 0) {
this.wrapped.append(this.queue.shift()); // the actual write logic is delegated to the wrapped appender
}
}
getMinLevel() { return this.wrapped.getMinLevel(); }
}
// ---------- Logger ----------
// Per-name entry point: owns a level threshold + a list of appenders.
class Logger {
constructor(name, level) {
this.name = name;
this.level = level;
this.appenders = []; // composition
}
setLevel(level) { this.level = level; }
addAppender(appender) { this.appenders.push(appender); }
log(level, message, context, exceptionInfo) {
if (level < this.level) return; // cheap early exit, no formatting done
const record = new LogRecord(level, message, this.name, context, exceptionInfo);
for (const appender of this.appenders) {
if (level >= appender.getMinLevel()) { // per-appender re-check
appender.append(record);
}
}
}
debug(msg) { this.log(LogLevel.DEBUG, msg, null, null); }
info(msg) { this.log(LogLevel.INFO, msg, null, null); }
warn(msg) { this.log(LogLevel.WARN, msg, null, null); }
error(msg, err) { this.log(LogLevel.ERROR, msg, null, this._describe(err)); }
fatal(msg, err) { this.log(LogLevel.FATAL, msg, null, this._describe(err)); }
_describe(err) { return err ? `${err.name}: ${err.message}` : null; }
}
// ---------- LoggerFactory (Singleton registry) ----------
class LoggerFactory {
static _registry = new Map();
static _globalDefaultLevel = LogLevel.INFO;
static getLogger(name) {
if (!LoggerFactory._registry.has(name)) {
LoggerFactory._registry.set(name, new Logger(name, LoggerFactory._globalDefaultLevel));
}
return LoggerFactory._registry.get(name);
}
static setGlobalDefaultLevel(level) { LoggerFactory._globalDefaultLevel = level; }
}
// ---------- Demo ----------
function main() {
const logFile = 'p6-demo.log';
if (fs.existsSync(logFile)) fs.unlinkSync(logFile); // start clean
const appLogger = LoggerFactory.getLogger('app.orders');
appLogger.setLevel(LogLevel.DEBUG);
appLogger.addAppender(new ConsoleAppender(LogLevel.INFO, new PlainTextFormatter())); // console: INFO+
const fileAppender = new FileAppender(LogLevel.DEBUG, new JsonFormatter(), logFile); // file: DEBUG+ as JSON
const asyncFile = new AsyncAppenderDecorator(fileAppender, 100);
appLogger.addAppender(asyncFile);
appLogger.debug('Loading order pipeline'); // filtered out of console (INFO min) but queued for file
appLogger.info('Order OR-1 received');
try {
throw new Error('payment gateway timeout');
} catch (e) {
appLogger.error('Order OR-1 failed', e);
}
appLogger.warn('Retrying order OR-1');
appLogger.fatal('Out of memory processing OR-1', new RangeError('heap exhausted'));
console.log('--- flushing async file appender ---');
asyncFile.flush(); // simulate the background worker draining the queue
console.log(`--- file contents (${logFile}) ---`);
console.log(fs.readFileSync(logFile, 'utf8').trimEnd());
fs.unlinkSync(logFile); // cleanup after demo
}
if (require.main === module) {
main();
}
module.exports = {
LogLevel, LogRecord, LogFormatter, PlainTextFormatter, JsonFormatter,
LogAppender, ConsoleAppender, FileAppender, AsyncAppenderDecorator, Logger, LoggerFactory,
};
// Logging Framework — full LLD implementation with demo.
// Single-file TypeScript translation of the Mermaid class diagram in section J.
//
// Note on concurrency: this problem's difficulty badge ("Intermediate — Pattern Showcase") does not
// call out concurrency, so no real synchronization primitives are used here (Node is single-threaded
// anyway). AsyncAppenderDecorator's queue-drain is exposed as an explicit flush() rather than a
// setImmediate/microtask loop, to keep the demo deterministic while faithfully modeling the
// enqueue/overflow/drain logic described in the hot-path pseudocode.
import * as fs from 'fs';
// ---------- LogLevel ----------
enum LogLevel { DEBUG = 0, INFO = 1, WARN = 2, ERROR = 3, FATAL = 4 }
// ---------- LogRecord ----------
// Immutable snapshot of one log event. Does not decide whether/where it gets written.
class LogRecord {
readonly timestamp: number = Date.now();
readonly threadName: string = 'main'; // TS has no real threads; kept for structural fidelity with the diagram
constructor(
public readonly level: LogLevel, public readonly message: string, public readonly loggerName: string,
public readonly context: Record<string, string>, public readonly exceptionInfo: string | null,
) {}
}
// ---------- LogFormatter (Strategy: mutually exclusive rendering choices) ----------
interface LogFormatter {
format(record: LogRecord): string;
}
class PlainTextFormatter implements LogFormatter {
format(r: LogRecord): string {
let s = `[${r.timestamp}] ${LogLevel[r.level]} [${r.loggerName}] [${r.threadName}] ${r.message}`;
const keys = Object.keys(r.context);
if (keys.length > 0) s += ` {${keys.map((k) => `${k}=${r.context[k]}`).join(', ')}}`;
if (r.exceptionInfo) s += ` EXCEPTION: ${r.exceptionInfo}`;
return s;
}
}
class JsonFormatter implements LogFormatter {
format(r: LogRecord): string {
const obj: Record<string, unknown> = {
timestamp: r.timestamp, level: LogLevel[r.level], logger: r.loggerName,
thread: r.threadName, message: r.message, context: r.context,
};
if (r.exceptionInfo) obj.exception = r.exceptionInfo;
return JSON.stringify(obj);
}
}
// ---------- LogAppender (injected destinations) ----------
interface LogAppender {
append(record: LogRecord): void;
getMinLevel(): LogLevel;
}
class ConsoleAppender implements LogAppender {
constructor(private readonly minLevel: LogLevel, private readonly formatter: LogFormatter) {}
append(record: LogRecord): void { console.log(this.formatter.format(record)); }
getMinLevel(): LogLevel { return this.minLevel; }
}
class FileAppender implements LogAppender {
constructor(
private readonly minLevel: LogLevel, private readonly formatter: LogFormatter, private readonly path: string,
) {}
append(record: LogRecord): void {
try {
fs.appendFileSync(this.path, this.formatter.format(record) + '\n');
} catch (e) {
console.error('FileAppender write failed:', (e as Error).message);
}
}
getMinLevel(): LogLevel { return this.minLevel; }
}
// Decorator: wraps ANY appender to move its I/O off the calling thread. Applies to
// console/file/network alike — a cross-cutting wrapper, not a rival implementation of "how to write".
class AsyncAppenderDecorator implements LogAppender {
private readonly queue: LogRecord[] = [];
constructor(private readonly wrapped: LogAppender, private readonly capacity: number) {}
append(record: LogRecord): void {
if (this.queue.length >= this.capacity) this.queue.shift(); // overflow policy: drop-oldest
this.queue.push(record); // non-blocking enqueue
}
// Simulates the background worker thread draining the queue and doing the real (slow) I/O.
flush(): void {
while (this.queue.length > 0) {
this.wrapped.append(this.queue.shift()!); // the actual write logic is delegated to the wrapped appender
}
}
getMinLevel(): LogLevel { return this.wrapped.getMinLevel(); }
}
// ---------- Logger ----------
// Per-name entry point: owns a level threshold + a list of appenders.
class Logger {
private readonly appenders: LogAppender[] = []; // composition
constructor(public readonly name: string, private level: LogLevel) {}
setLevel(level: LogLevel): void { this.level = level; }
addAppender(appender: LogAppender): void { this.appenders.push(appender); }
log(level: LogLevel, message: string, context: Record<string, string> | null, exceptionInfo: string | null): void {
if (level < this.level) return; // cheap early exit, no formatting done
const record = new LogRecord(level, message, this.name, context ?? {}, exceptionInfo);
for (const appender of this.appenders) {
if (level >= appender.getMinLevel()) { // per-appender re-check
appender.append(record);
}
}
}
debug(msg: string): void { this.log(LogLevel.DEBUG, msg, null, null); }
info(msg: string): void { this.log(LogLevel.INFO, msg, null, null); }
warn(msg: string): void { this.log(LogLevel.WARN, msg, null, null); }
error(msg: string, err: Error): void { this.log(LogLevel.ERROR, msg, null, this.describe(err)); }
fatal(msg: string, err: Error): void { this.log(LogLevel.FATAL, msg, null, this.describe(err)); }
private describe(err: Error | null): string | null { return err ? `${err.name}: ${err.message}` : null; }
}
// ---------- LoggerFactory (Singleton registry) ----------
class LoggerFactory {
private static registry = new Map<string, Logger>();
private static globalDefaultLevel: LogLevel = LogLevel.INFO;
static getLogger(name: string): Logger {
if (!LoggerFactory.registry.has(name)) {
LoggerFactory.registry.set(name, new Logger(name, LoggerFactory.globalDefaultLevel));
}
return LoggerFactory.registry.get(name)!;
}
static setGlobalDefaultLevel(level: LogLevel): void { LoggerFactory.globalDefaultLevel = level; }
}
// ---------- Demo ----------
function main(): void {
const logFile = 'p6-demo.log';
if (fs.existsSync(logFile)) fs.unlinkSync(logFile); // start clean
const appLogger = LoggerFactory.getLogger('app.orders');
appLogger.setLevel(LogLevel.DEBUG);
appLogger.addAppender(new ConsoleAppender(LogLevel.INFO, new PlainTextFormatter())); // console: INFO+
const fileAppender = new FileAppender(LogLevel.DEBUG, new JsonFormatter(), logFile); // file: DEBUG+ as JSON
const asyncFile = new AsyncAppenderDecorator(fileAppender, 100);
appLogger.addAppender(asyncFile);
appLogger.debug('Loading order pipeline'); // filtered out of console (INFO min) but queued for file
appLogger.info('Order OR-1 received');
try {
throw new Error('payment gateway timeout');
} catch (e) {
appLogger.error('Order OR-1 failed', e as Error);
}
appLogger.warn('Retrying order OR-1');
appLogger.fatal('Out of memory processing OR-1', new RangeError('heap exhausted'));
console.log('--- flushing async file appender ---');
asyncFile.flush(); // simulate the background worker draining the queue
console.log(`--- file contents (${logFile}) ---`);
console.log(fs.readFileSync(logFile, 'utf8').trimEnd());
fs.unlinkSync(logFile); // cleanup after demo
}
main();
export {
LogLevel, LogRecord, LogFormatter, PlainTextFormatter, JsonFormatter,
LogAppender, ConsoleAppender, FileAppender, AsyncAppenderDecorator, Logger, LoggerFactory,
};