structlog: Structured Logging for Python

repository·main·Indexed 11 days ago

https://github.com/hynek/structlog

A flexible structured logging library for Python that processes log entries as dictionaries through a pipeline of functions. It supports multiple output formats including JSON, logfmt, and pretty console output, and provides features like BoundLoggers for context management, asyncio compatibility, and integration with the standard library's logging module.

Tokens
38K
Snippets
113
Records
155
Agent score
87%

What's inside structlog

  1. Overview of structlog

    main

    structlog is a production-ready structured logging solution for Python. It is designed around the concept of functions that take and return dictionaries, providing a flexible and high-performance way to handle log entries.

    Key features include:

    • Flexibility: You can choose to let structlog handle the output format or forward log entries to an existing system like the standard library's logging module.
    • Output Formats: Out-of-the-box support for JSON, logfmt, and pretty console output (using ConsoleRenderer).
    • Modern Compatibility: Supports asyncio, context variables, and type hints.
    • Simplicity: Uses familiar APIs while maintaining a core logic based on dictionary manipulation.
  2. What is a Bound Logger and how do they work?

    main

    A Bound Logger is an instance of structlog.typing.BindableLogger (typically structlog.BoundLogger or structlog.stdlib.BoundLogger) obtained via structlog.get_logger() or by using bind()/unbind() methods.

    Key concepts:

    • Context: A dictionary of key-value pairs attached to the logger. When logging, the context serves as the base for the Event Dictionary.
    • Immutability: Bound loggers are immutable. You cannot modify the context of an existing logger instance. Instead, you must use .bind(**kwargs) or .unbind(*keys) to create a new logger instance with the updated context.
    • API: The methods available on a bound logger constitute the user's logging API.
  3. What is a bound logger?

    main

    A bound logger is the primary interface in structlog obtained via structlog.get_logger(). It is called 'bound' because it maintains a context dictionary of key-value pairs that are merged into every log entry produced by that specific logger instance.

    A bound logger consists of three components:

    1. Context Dictionary: A collection of key-value pairs that are merged into each log entry. You can inspect this via structlog.get_context().
    2. Processors: A list of functions called on every log entry. Each processor receives the output of the previous one.
    3. Wrapped Logger: The component responsible for the final I/O (output). By default, this is structlog.PrintLogger, but it can be a standard library logging.Logger or any other object.

    Important: Bound loggers do not perform I/O themselves; they manage context and proxy calls to the wrapped logger.

  4. What is an Event Dictionary?

    main

    An Event Dictionary (or event dict) is the central data structure in structlog. It is a dictionary containing all information related to a single log entry.

    Key characteristics:

    • The event key is a special field that holds the name/message of the event.
    • It is composed of the values bound to a Bound Logger's context merged with the key-value pairs passed directly to the logging method call.
    • It is passed through a chain of Processors, which can add, modify, or remove key-value pairs.
    • The final transformed dictionary is eventually passed to the Wrapped Logger for output.
  5. What is a Processor?

    main

    A Processor is a callable used to transform log entries. Processors are organized into a chain where each processor receives the output of the previous one.

    How they work:

    1. A processor receives an event dictionary as an argument.
    2. It performs transformations (adding, modifying, or removing keys).
    3. It returns a new event dictionary.
    4. The final processor in the chain produces the dictionary that is passed to the Wrapped Logger.

    This pattern allows for highly composable and modular log transformations.

  6. What is a Wrapped Logger?

    main

    The Wrapped Logger is the component responsible for the actual output of the log entry. It is the final destination for the event dictionary after it has passed through all processors.

    Common implementations:

    • structlog.PrintLogger: Used for Native Logging (fast, does not depend on the standard library).
    • logging.Logger: Used when integrating with the Python standard library logging module.
  7. Use contextvars instead of thread-local context

    main

    As of structlog 22.1.0, the structlog.threadlocal module is deprecated. You should use the structlog.contextvars module instead. contextvars is a more feature-rich superset that works correctly with thread-local data, asynchronous code, and greenlets.

    Recommended replacements for structlog.threadlocal functions:

    • structlog.contextvars.merge_contextvars
    • structlog.contextvars.clear_contextvars
    • structlog.contextvars.bind_contextvars
    • structlog.contextvars.get_contextvars
    • structlog.contextvars.get_merged_contextvars
  8. Manipulate log entries with processor chains

    main

    A processor is a function that receives an event_dict and returns a modified event_dict. structlog uses processor chains to transform log entries in flight.

    To use a custom processor, you must register it in the structlog.configure(processors=[...]) call.

    import datetime
    import structlog
    
    # Define a custom processor
    def timestamper(_, __, event_dict):
        event_dict["time"] = datetime.datetime.now().isoformat()
        return event_dict
    
    # Configure structlog with the processor chain
    structlog.configure(
        processors=[
            timestamper,
            structlog.processors.KeyValueRenderer()
        ]
    )
    
    structlog.get_logger().info("hi")
    # Output: event='hi' time='2018-01-21T09:37:36.976816'
  9. How log methods process events

    main

    When you call a logging method (e.g., .info(), .error()) on a bound logger, the following sequence occurs:

    1. Context Copy: The logger's context is copied to become the initial event dictionary.
    2. Keyword Merging: Any keyword arguments passed to the method are added to the event dictionary.
    3. Event Assignment: The first positional argument (the message) is added to the dictionary under the key "event".
    4. Processor Chain: The event dictionary is passed through the configured list of processors.
    5. Output: The final processor's return value (either a string or a (args, kwargs) tuple) is passed to the wrapped logger's corresponding method.

    Note on Interpolation: If you pass positional arguments, structlog performs string interpolation on the event message before processing.

    import structlog
    
    logger = structlog.get_logger()
    log = logger.bind(foo="bar")
    
    # This call results in an event dict: {'foo': 'bar', 'number': 42, 'event': 'Hello, world!'}
    log.info("Hello, %s!", "world", number=42)
  10. Use processor pipelines for data manipulation

    main

    Every log entry passes through a processor pipeline. This is a chain of functions where each function receives a dictionary (event_dict), modifies it, and returns the new dictionary for the next processor in the chain.

    Common tasks handled by built-in processors include:

    • Collecting call stack information (structlog.processors.StackInfoRenderer)
    • Formatting exception info (structlog.processors.format_exc_info)
    • Adding timestamps (structlog.processors.TimeStamper)
    def timestamper(logger, log_method, event_dict):
        """Add a timestamp to each log entry."""
        event_dict["timestamp"] = time.time()
        return event_dict
  11. Implement Canonical Log Lines to reduce noise

    main

    To maximize insight and minimize noise, aim for as few log entries per request as possible. This concept is known as Canonical Log Lines.

    structlog facilitates this by allowing you to:

    1. Bind data incrementally: Add context to loggers as a request progresses.
    2. Use context-local loggers: Use loggers that are local to the current execution context (e.g., via contextvars) to ensure data is automatically attached to all logs within a specific scope.
  12. Use asynchronous logging in asyncio applications

    main

    In asyncio applications, you can prevent the processor chain from blocking the event loop by using structlog's non-standard asynchronous methods. These methods are prefixed with a (e.g., ainfo instead of info) and execute processing in a thread pool executor.

    No extra configuration is required, and you can mix synchronous and asynchronous logging methods in the same application.

    Note: This increases the computational cost per log entry but prevents logging from blocking your application. (Added in version 23.1.0)

    import structlog
    
    logger = structlog.get_logger()
    
    # Asynchronous logging (non-blocking)
    await logger.ainfo("event!")
    
    # Regular synchronous logging
    logger.info("event!")