Design a Logging Framework | LLD

By Prajwal Haniya

Techletter #105 | December 28, 2024

Requirements

  1. The logging framework should support different log levels, such as DEBUG, INFO, WARNING, ERROR, and FATAL.
  2. It should allow logging messages with a timestamp, log level, and message content.
  3. The framework should support multiple output destinations, such as console, file, and database.
  4. It should provide a configuration mechanism to set the log level and output destination.
  5. The logging framework should be thread-safe to handle concurrent logging from multiple threads.
  6. It should be extensible to accommodate new log levels and output destinations in the future.

logging-image

Enum (log_level.py)

from enum import Enum

class LogLevel(Enum):
    DEBUG = 1
    INFO = 2
    WARNING = 3
    ERROR = 4
    FATAL = 5

log_appender.py

from abc import ABC, abstractmethod

class LogAppender(ABC):
    @abstractmethod
    def append(self, log_message):
        pass

file_appender.py

from log_appender import LogAppender

class FileAppender(LogAppender):
    def __init__(self, file_path):
        self.file_path = file_path

    def append(self, log_message):
        with open(self.file_path, "a") as file:
            file.write(str(log_message) + "\n")

console_appender.py

from log_appender import LogAppender

class ConsoleAppender(LogAppender):
    def append(self, log_message):
        print(log_message)

logger_config.py

class LoggerConfig:
    def __init__(self, log_level, log_appender):
        self.log_level = log_level
        self.log_appender = log_appender

    def get_log_level(self):
        return self.log_level

    def set_log_level(self, log_level):
        self.log_level = log_level

    def get_log_appender(self):
        return self.log_appender

    def set_log_appender(self, log_appender):
        self.log_appender = log_appender

log_message.py

import time

class LogMessage:
    def __init__(self, level, message):
        self.level = level
        self.message = message
        self.timestamp = int(time.time() * 1000)

    def get_level(self):
        return self.level

    def get_message(self):
        return self.message

    def get_timestamp(self):
        return self.timestamp

    def __str__(self):
        return f"[{self.level}] {self.timestamp} - {self.message}"

main.py

from enum.log_level import LogLevel

class Logger:
    _instance = None

    def __init__(self):
        if Logger._instance is not None:
            raise Exception("This class is a singleton")
        else:
            Logger._instance = self
            self.config = LoggerConfig(LogLevel.INFO, ConsoleAppender())

    @staticmethod
    def get_instance():
        if Logger._instance is None:
            Logger()
        return Logger._instance

    def set_config(self, config):
        self.config = config

    def log(self, level, message):
        if level.value >= self.config.get_log_level().value:
            log_message = LogMessage(level, message)
            self.config.get_log_appender().append(log_message)

    def debug(self, message):
        self.log(LogLevel.DEBUG, message)

    def info(self, message):
        self.log(LogLevel.INFO, message)

    def warning(self, message):
        self.log(LogLevel.WARNING, message)

    def error(self, message):
        self.log(LogLevel.ERROR, message)

    def fatal(self, message):
        self.log(LogLevel.FATAL, message)