Design a Logging Framework
You'll learn to
- -Design log levels, appenders, and formatters so new output targets can be added without touching existing code
- -Combine Singleton (logger registry), Strategy (formatter), and Chain of Responsibility (level filtering) in one coherent design
A logging framework is a favorite LLD prompt for one specific reason: a genuinely good design naturally combines three patterns you have already learned, rather than needing any new ones, which makes it an excellent test of whether you can compose patterns instead of just naming them individually.
Step 1: Log Levels and the Filtering Chain
DEBUG, INFO, WARNING, ERROR form a natural severity ordering, and a logger configured at, say, WARNING should suppress DEBUG and INFO messages while allowing WARNING and ERROR through. This maps directly onto Chain of Responsibility from earlier in this course: a chain of level-handlers, each deciding whether a message at its severity should be processed and passed along, or dropped.
from enum import IntEnum
class LogLevel(IntEnum):
DEBUG = 1
INFO = 2
WARNING = 3
ERROR = 4
class LogMessage:
def __init__(self, level: LogLevel, text: str):
self.level = level
self.text = text
class LogHandler(ABC):
def __init__(self, min_level: LogLevel):
self._min_level = min_level
self._next: "LogHandler | None" = None
def set_next(self, handler: "LogHandler") -> "LogHandler":
self._next = handler
return handler
def handle(self, message: LogMessage) -> None:
if message.level >= self._min_level:
self._write(message)
if self._next:
self._next.handle(message)
@abstractmethod
def _write(self, message: LogMessage) -> None: ...Step 2: Appenders as Independent Handlers
Different output destinations (console, file, remote log aggregator) are appenders, each with its own minimum level and its own formatter - a ConsoleAppender might show everything from DEBUG up, while a FileAppender only persists WARNING and above, both wired into the same chain and each receiving every message, filtering independently.
class ConsoleAppender(LogHandler):
def __init__(self, min_level: LogLevel, formatter: "LogFormatter"):
super().__init__(min_level)
self._formatter = formatter
def _write(self, message: LogMessage) -> None:
print(self._formatter.format(message))
class FileAppender(LogHandler):
def __init__(self, min_level: LogLevel, formatter: "LogFormatter", filename: str):
super().__init__(min_level)
self._formatter = formatter
self._filename = filename
def _write(self, message: LogMessage) -> None:
with open(self._filename, "a") as f:
f.write(self._formatter.format(message) + "\n")Step 3: Formatters as a Swappable Strategy
How a message is rendered (plain text, JSON, a specific timestamp format) is independent of where it goes - a clean Strategy split, exactly as covered earlier. A JSONFormatter and a PlainTextFormatter both implement format(message) -> str, and either appender above can be configured with either formatter without any change to the appender itself.
Step 4: A Singleton Logger Registry
class Logger:
_instance: "Logger | None" = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._chain = None
return cls._instance
def configure(self, chain_head: LogHandler) -> None:
self._chain = chain_head
def log(self, level: LogLevel, text: str) -> None:
if self._chain:
self._chain.handle(LogMessage(level, text))
# Application code anywhere just does:
Logger().log(LogLevel.ERROR, "Payment failed")This is a legitimate, defensible use of Singleton - revisit the Singleton chapter's framing: logging is called from deep in nearly every part of a codebase, and threading a Logger instance as a parameter through every function would add real, ongoing friction for comparatively little benefit, unlike most Singleton use cases where dependency injection is clearly better.
Presenting this design as "Chain of Responsibility for level filtering, Strategy for formatting, Singleton for the global entry point" - naming all three explicitly - is exactly the composition skill this capstone-style problem is testing.
Interview Signal is part of Pro
See a real weak answer next to a real strong one for this exact topic.
Quiz is part of Pro
Test what you just read with a short quiz, and bank the XP.