Skip to content
FollowtheDamnCode

LLD: Logging Framework

Loggers, levels, handlers, and formatters — a small framework that shows Singleton, Chain of Responsibility, Strategy, and Observer working together.

Big idea

A logging framework separates four questions: who is logging (a named logger, arranged in a hierarchy like app.db), is this important enough (levels and filters), where does it go (handlers: console, file, network), and what does it look like (formatters). Each answer is its own swappable piece — which is why the design is a tour of patterns.

Requirements

Functional

  • get_logger("app.db") returns the same logger every time for a name.
  • Levels: DEBUG < INFO < WARNING < ERROR; a logger ignores messages below its level.
  • Loggers form a hierarchy by dotted name; a record goes to the logger's handlers and then propagates to its parents' handlers.
  • Handlers have their own levels and formatters; several per logger.

Non-functional: logging must never crash the app; cheap when a level is disabled; safe across threads.

Out of scope: config files, log rotation, async shipping (extensions).

Entities

LogManager (registry, a deliberate Singleton), Logger (name, level, handlers, parent), Level, LogRecord (time, level, logger name, message), Handler (interface: level, formatter, emit) with ConsoleHandler, MemoryHandler, FileHandler; Formatter (a Strategy).

Think of it like

A newsroom. Reporters (loggers) file stories tagged by desk (sports.football) and urgency (level). Editors (handlers) each subscribe to what they care about — the website editor takes everything from INFO up, the alerts editor only ERRORs — and each formats it their own way. A story filed at the football desk also goes up to the sports desk's editors unless someone says stop.

Class diagram

Loggers, handlers, and formatters

Key flow: one call, two destinations

A warning logged on app.db

Code skeleton

A hierarchy, levels, two formats, and propagation
import json

LEVELS = {"DEBUG": 10, "INFO": 20, "WARNING": 30, "ERROR": 40}

class TextFormatter:
    def format(self, r):
        return f"[{r['level']:<7}] {r['logger']}: {r['message']}"

class JsonFormatter:
    def format(self, r):
        return json.dumps(r, sort_keys=True)


class Handler:
    def __init__(self, level="DEBUG", formatter=None):
        self.level, self.formatter = LEVELS[level], formatter or TextFormatter()
    def handle(self, record):
        if LEVELS[record["level"]] >= self.level:
            try:
                self.emit(self.formatter.format(record))
            except Exception as err:          # logging must never crash the app
                print(f"(handler failed: {err})")

class ConsoleHandler(Handler):
    def emit(self, line): print(line)

class MemoryHandler(Handler):
    def __init__(self, *a, **k):
        super().__init__(*a, **k); self.lines = []
    def emit(self, line): self.lines.append(line)


class Logger:
    def __init__(self, name, parent):
        self.name, self.parent = name, parent
        self.level, self.handlers, self.propagate = None, [], True
    def effective_level(self):                # inherit the nearest ancestor's level
        node = self
        while node.level is None:
            node = node.parent
        return node.level
    def log(self, level, message):
        if LEVELS[level] < self.effective_level():
            return                            # cheap early exit when disabled
        record = {"t": LogManager.tick(), "level": level, "logger": self.name, "message": message}
        node = self
        while node:                           # chain up the hierarchy
            for h in node.handlers:
                h.handle(record)
            node = node.parent if node.propagate else None
    def info(self, m): self.log("INFO", m)
    def warning(self, m): self.log("WARNING", m)
    def debug(self, m): self.log("DEBUG", m)


class LogManager:                             # one registry per process
    _loggers, _clock = {}, 0
    root = Logger("root", None)
    root.level = LEVELS["WARNING"]

    @classmethod
    def get_logger(cls, name):
        if name not in cls._loggers:
            parent_name = name.rsplit(".", 1)[0] if "." in name else None
            parent = cls.get_logger(parent_name) if parent_name else cls.root
            cls._loggers[name] = Logger(name, parent)
        return cls._loggers[name]

    @classmethod
    def tick(cls):
        cls._clock += 1
        return cls._clock


LogManager.root.handlers.append(ConsoleHandler(level="INFO"))
app = LogManager.get_logger("app")
app.level = LEVELS["INFO"]
db = LogManager.get_logger("app.db")
audit = MemoryHandler(level="WARNING", formatter=JsonFormatter())
db.handlers.append(audit)

print("same logger:", LogManager.get_logger("app.db") is db)
db.debug("connection pool size 10")           # below app's INFO level: dropped
db.info("connected")
db.warning("slow query 2.3s")
LogManager.get_logger("app.http").info("GET /health 200")
print("audit:", audit.lines)
output
same logger: True
[INFO   ] app.db: connected
[WARNING] app.db: slow query 2.3s
[INFO   ] app.http: GET /health 200
audit: ['{"level": "WARNING", "logger": "app.db", "message": "slow query 2.3s", "t": 2}']

Run and edit it in the app.

app.db had no level of its own, so it inherited INFO from app; the warning reached both its own JSON handler and, by propagation, the root console.

Watch it

Which pattern does the Formatter play in this design?

Why check the level before building the log record?

Extensions and trade-offs

  • Async handlers — a queue plus a background writer so slow destinations don't block requests (a Proxy in front of the real handler).
  • Filters — per-handler predicates chained before emit (Chain of Responsibility).
  • Structured context — attach request IDs to every record (context variables), essential for tracing.
  • Configuration — build the logger tree from a file at startup (Builder).
  • Handler fan-out — handlers are subscribers of a logger (Observer); propagation is a chain up the tree.
  • Testing — a MemoryHandler is the test double; avoid assertions against global state by resetting the registry.

Key terms

  • Logger hierarchy — Loggers named with dots form a tree; children inherit levels and propagate records to parents.
  • Handler — The component that sends a log record to a destination.
  • Formatter — Turns a record into text (or JSON) for a handler.
  • Propagation — Passing a record up to ancestor loggers' handlers after the logger's own.
  • Effective level — The level a logger uses, inherited from the nearest ancestor that sets one.