Loguru: Python logging made (stupidly) simple

repository·master·Indexed 12 days ago

https://github.com/Delgan/loguru

A Python library designed to simplify logging by providing a powerful API that eliminates standard logging boilerplate. Key features include zero-boilerplate setup, unified API for handlers and formatters, built-in file rotation, retention, and compression, and robust exception handling via @logger.catch. It supports asynchronous, thread-safe, and multiprocess-safe logging, as well as structured logging with bind() and contextualize(). Compatible with Python 3.5+ and PyPy.

Tokens
26.6K
Snippets
94
Records
107
Agent score
97%

What's inside Loguru

  1. Overview of Loguru features

    master

    Loguru is designed to make Python logging enjoyable and powerful by reducing boilerplate and solving common pain points found in standard logging libraries. Key features include:

    • Zero Boilerplate: Ready to use immediately with from loguru import logger.
    • Unified API: Replaces the need for separate Handlers, Formatters, and Filters with a single function.
    • Advanced File Management: Built-in support for rotation, retention, and compression.
    • Modern Formatting: Uses braces-style string formatting.
    • Robust Exception Handling: Catches exceptions within threads or the main process and provides fully descriptive exceptions.
    • Concurrency Support: Asynchronous, thread-safe, and multiprocess-safe.
    • Rich Output: Pretty logging with colors.
    • Compatibility: Entirely compatible with the standard Python logging module and suitable for both scripts and libraries.
  2. Security considerations when using Loguru

    master

    When using Loguru, be aware of several security risks:

    1. Pickle Deserialization: If transmitting log messages over a network using pickle, ensure the source is trusted or use HMAC signing to verify authenticity to prevent malicious code execution.
    2. String Formatting Vulnerabilities: Avoid logging raw user input directly into format strings. Using logger.debug(message, value) is similar to print(message.format(value)), which can allow attackers to access hidden variables via brace syntax (e.g., {value.__init__.__globals__[SECRET_KEY]}).
    3. Log Injection: Sanitize or escape user-provided strings to prevent attackers from injecting fake log entries (e.g., inserting newline characters \n to create new log lines).
    4. Credential Exposure in Exceptions: By default, Loguru displays variable values when an Exception is logged. In production, disable this to prevent sensitive data from appearing in logs by setting the LOGURU_DIAGNOSE=NO environment variable or using logger.add(..., diagnose=False).
    5. File Permissions: Loguru uses the built-in open() function. To ensure log files are created with restricted permissions, use the opener argument in logger.add().
    import hashlib
    import hmac
    import pickle
    import os
    
    # 1. Secure Pickle handling with HMAC
    def client(connection):
        data = pickle.dumps("Log message")
        digest = hmac.digest(b"secret-shared-key", data, hashlib.sha1)
        connection.send(digest + b" " + data)
    
    def server(connection):
        expected_digest, data = connection.read().split(b" ", 1)
        data_digest = hmac.digest(b"secret-shared-key", data, hashlib.sha1)
        if not hmac.compare_digest(data_digest, expected_digest):
            print("Integrity error")
        else:
            message = pickle.loads(data)
            logger.info(message)
    
    # 2. Preventing credential exposure in logs
    logger.add("out.log", diagnose=False)
    
    # 3. Setting custom file permissions
    def opener(file, flags):
        return os.open(file, flags, 0o600)
    
    logger.add("combined.log", opener=opener)
  3. How to differentiate logs across different modules

    master

    Loguru uses a single global logger instance. Instead of creating new logger objects per module, use the following patterns to differentiate logs:

    1. Filter by Module Name: Use the record["name"] field (which contains the module name) in a filter function to route logs to different files.
    2. Use bind() for precise identification: The logger.bind(**kwargs) method returns a new logger instance tied to specific values in the record["extra"] dictionary. This is the recommended way for advanced use cases.
    # 1. Filtering by module name
    logger.add("module_1.log", filter="module_1")
    
    # 2. Using bind() for specific context
    def is_specific_log(record):
        return record["extra"].get("is_specific") is True
    
    logger.add("specific.log", filter=is_specific_log)
    logger.add("other.log", filter=lambda r: not is_specific_log(r))
    
    specific_logger = logger.bind(is_specific=True)
    specific_logger.info("This goes to specific.log")
    logger.info("This goes to other.log")
    def is_specific_log(record):
        return record["extra"].get("is_specific") is True
    
    logger.add("specific.log", filter=is_specific_log)
    logger.add("other.log", filter=lambda r: not is_specific_log(r))
    
    specific_logger = logger.bind(is_specific=True)
    specific_logger.info("This message will go to 'specific.log' only.")
    
    logger.info("This message will go to 'other.log' only.")
  4. How to create independent loggers with separate handlers

    master

    While Loguru uses a single global logger by default, you can create specialized logging flows using two methods:

    1. Using bind() and filter

    Use bind() to attach extra context to a logger instance, then use a filter on the handler to route logs based on that context. This is useful for splitting logs into different files based on an identifier.

    2. Using copy.deepcopy()

    For a large number of tasks, configuring many filters can be slow. Instead, use copy.deepcopy(logger) to create a truly independent logger object. Adding a handler to a deep-copied logger will not affect the original global logger.

    Note: Always call logger.remove() before deep-copying if the original logger has non-picklable handlers (like the default sys.stderr sink) to avoid errors.

    # Method 1: bind and filter
    from loguru import logger
    
    def task_A():
        logger_a = logger.bind(task="A")
        logger_a.info("Starting task A")
    
    logger.add("file_A.log", filter=lambda record: record["extra"]["task"] == "A")
    
    # Method 2: deepcopy for independent loggers
    import copy
    from loguru import logger
    
    logger.remove()
    for task_id in ["A", "B"]:
        logger_ = copy.deepcopy(logger)
        logger_.add(f"file_{task_id}.log")
        # logger_ is now independent
  5. Structured logging with bind() and contextualize()

    master

    Loguru allows adding context to log messages via the extra record attribute:

    • bind(): Creates a new logger instance with specific key-value pairs attached to the extra dict. Can be used for permanent context or inline binding.
    • contextualize(): A context manager that modifies the context-local state temporarily.
    • patch(): Allows dynamic modification of the record dict for every new message.
    • serialize=True: When passed to add(), converts each log message into a JSON string.
    # JSON serialization
    logger.add(custom_sink_function, serialize=True)
    
    # Permanent binding
    context_logger = logger.bind(ip="192.168.0.1", user="someone")
    context_logger.info("Contextualize your logger easily")
    
    # Inline binding
    context_logger.bind(user="someone_else").info("Inline binding of extra attribute")
    
    # Temporary context
    with logger.contextualize(task=task_id):
        do_something()
        logger.info("End of task")
    
    # Dynamic patching
    logger = logger.patch(lambda record: record["extra"].update(utc=datetime.utcnow()))
    
    # Filtering based on extra context
    logger.add("special.log", filter=lambda record: "special" in record["extra"])
    logger.bind(special=True).info("This message is logged to the special file!")
  6. Why is the level name shown as 'Level NUM' instead of a name?

    master

    When you pass an integer (e.g., from the standard logging library) to logger.log(), Loguru treats it as an anonymous level.

    Because Loguru allows users to define multiple different levels with the same severity number, it cannot guarantee which name corresponds to a specific number. To avoid ambiguity, it displays the severity number (e.g., Level 20) instead of a name when a name is not explicitly provided.

    import logging
    from loguru import logger
    
    # This will output 'Level 20' instead of 'INFO'
    logger.log(logging.INFO, "This is an info message.")
  7. Use LogRecord dicts and advanced sinks

    master

    In Loguru, the equivalent of a LogRecord is a standard Python dict. This dictionary contains all contextual information about the log event.

    When writing a custom sink (a function or object passed to logger.add()), the argument received is a string-like object that contains a .record attribute. This attribute provides access to the full dict of log metadata.

    • Simple Sink: Can treat the input as a plain string.
    • Advanced Sink: Can access message.record to perform logic based on severity, file paths, or custom context (via record["extra"]).

    To extend the record with custom data (similar to setLogRecordFactory), use the patch() method to inject keys into record["extra"].

    from loguru import logger
    import sys
    
    def simple_sink(message):
        # A simple sink can use "message" as a basic string and ignore the "record" attribute.
        print(message, end="")
    
    def advanced_sink(message):
        # An advanced sink can use the "record" attribute to access contextual information.
        record = message.record
    
        if record["level"].no >= 50:
            file_path = record["file"].path
            print(f"Critical error in {file_path}", end="", file=sys.stderr)
        else:
            print(message, end="")
    
    logger.add(simple_sink)
    logger.add(advanced_sink)
  8. Configure Loguru for a library vs an application

    master

    How you use Loguru depends on whether you are building a library or an application:

    For Applications

    • You can add handlers anywhere.
    • It is best practice to configure the logger inside an if __name__ == "__main__": block in your entry point.

    For Libraries

    • Do not add handlers. Adding handlers in a library affects the global logger and will interfere with the user's own logging configuration.
    • Disable your library's logs by default. Use logger.disable("your_package_name") in your package's __init__.py. This prevents your library's logs from cluttering the user's output.
    • Allow users to enable logs. Users can call logger.enable("your_package_name") if they want to see your library's logs.
    • Provide a configuration helper. Optionally, provide a function like configure_logger() that users can call to set up your library's logging explicitly.
    # In mypackage/__init__.py (Library entry point)
    from loguru import logger
    
    # Disable logs for this package by default
    logger.disable("mypackage")
    
    # Do NOT call logger.add() here

    In mypackage/main.py (Application entry point)

    if name == "main": from loguru import logger logger.add("app.log") # Run application logic

  9. Quickstart with Loguru

    master

    Loguru provides a pre-configured logger instance that outputs to stderr by default, allowing you to start logging immediately without boilerplate code.

    from loguru import logger
    
    logger.debug("That's it, beautiful and simple logging!")
  10. Serialize log messages using custom functions

    master

    While serialize=True converts records to JSON, you can implement custom serialization for specific sinks. You can do this by providing a custom function to the sink or by using a custom format function to inject serialized data into the record["extra"] dictionary.

    To avoid redundant serialization calls when using multiple sinks, use logger.patch() to inject the serialized data into the record once.

    # Option 1: Custom sink function
    def serialize(record):
        subset = {"timestamp": record["time"].timestamp(), "message": record["message"]}
        return json.dumps(subset)
    
    def sink(message):
        serialized = serialize(message.record)
        print(serialized)
    
    logger.add(sink)
    
    # Option 2: Custom format function
    def formatter(record):
        record["extra"]["serialized"] = serialize(record)
        return "{extra[serialized]}\n"
    
    logger.add("file.log", format=formatter)
    
    # Option 3: Using patch to reuse serialization across multiple sinks
    def patching(record):
        record["extra"]["serialized"] = serialize(record)
    
    logger = logger.patch(patching)
    logger.add(sys.stderr, format="{extra[serialized]}")
    logger.add("file.log", format="{extra[serialized]}")
  11. Capture stdout, stderr, and warnings

    master

    If you cannot control the source code of an application, you can redirect standard output and error to Loguru.

    Capture stdout/stderr

    Use contextlib.redirect_stdout with a custom stream object that implements write and flush. Important: You must remove the default handler and use sys.__stdout__ for your sink to avoid deadlocks.

    Capture warnings

    You can capture warnings by replacing warnings.showwarning with a custom function that calls logger.opt(depth=2).warning(). Alternatively, you can use warnings.warn as a sink directly.

    # Capture stdout
    import contextlib
    import sys
    from loguru import logger
    
    class StreamToLogger:
        def __init__(self, level="INFO"):
            self._level = level
        def write(self, buffer):
            for line in buffer.rstrip().splitlines():
                logger.opt(depth=1).log(self._level, line.rstrip())
        def flush(self):
            pass
    
    logger.remove()
    logger.add(sys.__stdout__)
    
    stream = StreamToLogger()
    with contextlib.redirect_stdout(stream):
        print("Standard output is sent to added handlers.")
    
    # Capture warnings via sink
    import warnings
    logger.add(warnings.warn, format="{message}", filter=lambda record: record["level"].name == "WARNING")
  12. Correctly implement custom formatting functions

    master

    When passing a callable to the format argument of logger.add(), the function must return a template string containing placeholders (like "{time}"), NOT a pre-formatted message.

    Incorrect (returns formatted string):

    def incorrect_dynamic_format(record):
        return f"{record['time']} - {record['level']} - {record['message']}"

    Correct (returns template string):

    def correct_dynamic_format(record):
        return "{time} - {level} - {message}\n{exception}"

    If you need to pre-format data: Save the result in the record["extra"] dictionary and then reference it in the template.

    Escaping curly braces: If your format string contains literal curly braces (e.g., for JSON), escape them by doubling them ({{ and }}).

    def correct_dynamic_format(record):
        return "{time} - {level} - {message}\n{exception}"
    
    logger.add(sys.stderr, format=correct_dynamic_format)