The Ultimate Guide to Python Logging Essentials
By Alyce Osbourne
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.