Back to Posts
Workspace with laptop, notepad, pen, and coffee mug on desk, illustrating a Python Logging Module Implementation Guide setup for developers.

The Ultimate Guide to Python Logging Essentials

Have you ever been curious about the inner workings of the logging module and how it efficiently gathers, organizes, and sends out records? This post aims to elucidate the components and mechanisms at play within Python’s logging framework, supported by a practical code example to illustrate these concepts in action.

The main components

The logging module has a few key components to make it work.

The record

class LogRecord:
    def __init__(self, name: str, level: LogLevel, msg: str, module_name: str, func_name: str, lineno: int) -> None:
        self.name: str = name
        self.level: LogLevel = level
        self.msg: str = msg
        self.module_name: str = module_name
        self.func_name: str = func_name
        self.lineno: int = lineno

The record is the cornerstone of the logging system, encapsulating all the information pertinent to an event being logged. This includes metadata such as the log message, the severity level (DEBUG, INFO, WARNING, ERROR), and contextual information like the module name, function name, and line number where the log call was made. In our example, the LogRecord class is designed to hold these pieces of information, thereby providing a structured record of each logging event.

The handler

import io

class StreamHandler:
    def __init__(self, stream: Optional[Union[io.StringIO, io.TextIOWrapper]] = None, formatter: Optional['TemplateFormatter'] = None) -> None:
        self.stream: Union[io.StringIO, io.TextIOWrapper] = stream if stream is not None else io.StringIO()
        self.formatter: Optional[TemplateFormatter] = formatter

    def emit(self, record: LogRecord) -> None:
        formatted_record = self.formatter.format(record) if self.formatter else record.msg
        self.stream.write(formatted_record + '\\n')
        self.stream.flush()

Handlers are responsible for dispatching the log records to the designated destinations. A logger can have multiple handlers, each configured to send logs to different outputs, such as the console, files, or external systems. The StreamHandler in our code exemplifies a handler that directs log messages to a stream, which can be standard output, a file, or any object with a write method. Handlers play a crucial role in determining the fate of log records once they’re created.

The formatter

from string import Template

class TemplateFormatter:
    def __init__(self, template_str: str) -> None:
        self.template: Template = Template(template_str)

    def format(self, record: LogRecord) -> str:
        return self.template.substitute(
            name=record.name,
            level=record.level.name,
            message=record.msg,
            module_name=record.module_name,
            func_name=record.func_name,
            lineno=record.lineno
        )

While records provide the raw data of log events, formatters dictate their presentation. A formatter can be attached to any handler to specify the output format of log messages, allowing developers to include or exclude certain details according to their needs. The TemplateFormatter class in our example uses a template string to define a customizable format for log messages, enabling developers to craft the log output’s appearance meticulously.

The logger

import inspect

class LogLevel(Enum):
    DEBUG = 10
    INFO = 20
    WARNING = 30
    ERROR = 40

class Logger:
    def __init__(self, name: str, level: LogLevel = LogLevel.DEBUG) -> None:
        self.name: str = name
        self.level: LogLevel = level
        self.handler: Optional[StreamHandler] = None

    def set_level(self, level: LogLevel) -> None:
        self.level = level

    def add_handler(self, handler: 'StreamHandler') -> None:
        self.handler = handler

    def log(self, level: LogLevel, msg: str) -> None:
        if level.value >= self.level.value and self.handler:
            frame = inspect.currentframe().f_back
            module = inspect.getmodule(frame)
            module_name = module.__name__ if module else 'Unknown'
            record = LogRecord(
                name=self.name,
                level=level,
                msg=msg,
                module_name=module_name,
                func_name=frame.f_code.co_name,
                lineno=frame.f_lineno
            )
            self.handler.emit(record)

    def debug(self, msg: str) -> None:
        self.log(LogLevel.DEBUG, msg)

    def info(self, msg: str) -> None:
        self.log(LogLevel.INFO, msg)

    def warning(self, msg: str) -> None:
        self.log(LogLevel.WARNING, msg)

    def error(self, msg: str) -> None:
        self.log(LogLevel.ERROR, msg)

The interaction among records, handlers, and formatters is orchestrated by the logger, the facade through which the application interacts with the logging system. A logger is configured with a minimum log level and is associated with one or more handlers, each equipped with a formatter. When a log method is called on the logger, it creates a LogRecord if the event’s severity meets the logger’s threshold. This record is then passed to each handler, which, in turn, uses its formatter to prepare the message for output. When creating the LogRecord, it gathers data using inspect to provide as much context as possible.

Putting it together

import io
from typing import Optional, Union
import sys
from enum import Enum
import inspect
from string import Template

class LogLevel(Enum):
    DEBUG = 10
    INFO = 20
    WARNING = 30
    ERROR = 40

class LogRecord:
    def __init__(self, name: str, level: LogLevel, msg: str, module_name: str, func_name: str, lineno: int) -> None:
        self.name: str = name
        self.level: LogLevel = level
        self.msg: str = msg
        self.module_name: str = module_name
        self.func_name: str = func_name
        self.lineno: int = lineno

class Logger:
    def __init__(self, name: str, level: LogLevel = LogLevel.DEBUG) -> None:
        self.name: str = name
        self.level: LogLevel = level
        self.handler: Optional[StreamHandler] = None

    def set_level(self, level: LogLevel) -> None:
        self.level = level

    def add_handler(self, handler: 'StreamHandler') -> None:
        self.handler = handler

    def log(self, level: LogLevel, msg: str) -> None:
        if level.value >= self.level.value and self.handler:
            frame = inspect.currentframe().f_back
            module = inspect.getmodule(frame)
            module_name = module.__name__ if module else 'Unknown'
            record = LogRecord(
                name=self.name,
                level=level,
                msg=msg,
                module_name=module_name,
                func_name=frame.f_code.co_name,
                lineno=frame.f_lineno
            )
            self.handler.emit(record)

    def debug(self, msg: str) -> None:
        self.log(LogLevel.DEBUG, msg)

    def info(self, msg: str) -> None:
        self.log(LogLevel.INFO, msg)

    def warning(self, msg: str) -> None:
        self.log(LogLevel.WARNING, msg)

    def error(self, msg: str) -> None:
        self.log(LogLevel.ERROR, msg)

class StreamHandler:
    def __init__(self, stream: Optional[Union[io.StringIO, io.TextIOWrapper]] = None, formatter: Optional['TemplateFormatter'] = None) -> None:
        self.stream: Union[io.StringIO, io.TextIOWrapper] = stream if stream is not None else io.StringIO()
        self.formatter: Optional[TemplateFormatter] = formatter

    def emit(self, record: LogRecord) -> None:
        formatted_record = self.formatter.format(record) if self.formatter else record.msg
        self.stream.write(formatted_record + '\\n')
        self.stream.flush()

class TemplateFormatter:
    def __init__(self, template_str: str) -> None:
        self.template: Template = Template(template_str)

    def format(self, record: LogRecord) -> str:
        return self.template.substitute(
            name=record.name,
            level=record.level.name,
            message=record.msg,
            module_name=record.module_name,
            func_name=record.func_name,
            lineno=record.lineno
        )

Example usage and explanation

To illustrate these components working in concert, let’s delve into the provided code snippet:

logger = Logger('my_logger', LogLevel.INFO)
template_str = "[${level}] ${name} - ${module_name}.${func_name}:${lineno} - ${message}"
template_formatter = TemplateFormatter(template_str)
console_handler = StreamHandler(stream=sys.stdout, formatter=template_formatter)
logger.add_handler(console_handler)

logger.debug("This debug message will not be shown.")
logger.info("This is an informational message.")
logger.warning("Warning message here.")
logger.error("An error has occurred.")

Here, we initialize a logger named ‘my_logger’ with a minimum level of INFO, meaning only log messages of INFO level and above will be processed. A TemplateFormatter is created with a specific format for log messages. This formatter is attached to a StreamHandler, which is set to output to the console (sys.stdout). The logger is then configured to use this handler for log events.

When executing the log methods (debug, info, warning, error), the logger evaluates whether the message’s level meets or exceeds its configured level. Since the logger’s level is set to INFO, the debug message is ignored, and the rest are processed, formatted, and output to the console, demonstrating how log records are filtered, formatted, and dispatched.

Final thoughts

The logging module in Python is a powerful tool for monitoring and debugging software applications, offering a flexible and modular approach to capturing and managing log events. By understanding the roles and relationships of records, handlers, and formatters, developers can customize the logging behavior to suit their needs, enhancing the observability and maintainability of their applications. The provided code example serves as a practical guide to implementing these concepts, paving the way for effective logging practices in your projects.

Remember, effective logging is as much an art as it is a science. It requires a balance between detail and conciseness, significance and noise. With the insights and techniques discussed in this post, you’re well-equipped to harness the power of Python’s logging module to build more robust, debuggable, and transparent applications.

Improve your code with my 3-part code diagnosis framework

Watch my free 30 minutes code diagnosis workshop on how to quickly detect problems in your code and review your code more effectively.

When you sign up, you'll get an email from me regularly with additional free content. You can unsubscribe at any time.

Recent posts