Logging in Python with Loguru

Author

Andres Monge

Published

December 18, 2024

Logging is an essential part of any application, providing insights into its runtime behavior. Python’s built-in logging module is powerful but can be cumbersome to configure. Enter Loguru, a third-party logging library that simplifies logging with a more intuitive API and rich features.

Why Loguru?

Loguru offers several advantages over the standard logging module:

  • Simpler API: No need for handlers, formatters, or loggers. Just import and log.
  • Rich Formatting: Easily customize log formats with colors, timestamps, and more.
  • Exception Handling: Automatically captures and logs exceptions with tracebacks.
  • Asynchronous Logging: Supports asynchronous logging out of the box.

Customizing Loguru

Loguru allows you to customize the log format, level, and output destinations. Here’s how to configure it:

Custom Log Format

You can define a custom format for your logs using Loguru’s formatting syntax. For example, to include the log level, timestamp, and message:

Code
from loguru import logger
import sys

logger.remove()  # Remove default handler
logger.add(sys.stdout, format="{time} | {level} | {message}")
logger.info("Custom format logging!")
2025-03-11T17:33:55.161680+0100 | INFO | Custom format logging!

Log Levels

Loguru supports the standard log levels: TRACE, DEBUG, INFO, WARNING, ERROR, and CRITICAL. You can set the log level globally or per handler:

Code
logger.add(sys.stdout, level="DEBUG")
logger.debug("This is a debug message.")
2025-03-11T17:33:55.212477+0100 | DEBUG | This is a debug message.
2025-03-11 17:33:55.212 | DEBUG    | __main__:<module>:2 - This is a debug message.

Adding Handlers

You can add multiple handlers to log to different destinations, such as files, with different formats or levels:

Code
logger.add("file.log", level="INFO", rotation="10 MB")
logger.info("This log will go to both the console and a file.")
2025-03-11T17:33:55.258561+0100 | INFO | This log will go to both the console and a file.
2025-03-11 17:33:55.258 | INFO     | __main__:<module>:2 - This log will go to both the console and a file.

Advanced Features

Exception Logging

Loguru automatically captures exceptions and logs them with tracebacks. However, you can trim the traceback logs to make them more concise:

Code
try:
    1 / 0
except ZeroDivisionError:
    logger.opt(exception=(None, False, False)).error("An error occurred!")
--- Logging error in Loguru Handler #15 ---
Record was: {'elapsed': datetime.timedelta(seconds=257, microseconds=729688), 'exception': (type=None, value=False, traceback=False), 'extra': {}, 'file': (name='1334436161.py', path='/tmp/ipykernel_66122/1334436161.py'), 'function': '<module>', 'level': (name='ERROR', no=40, icon='❌'), 'line': 4, 'message': 'An error occurred!', 'module': '1334436161', 'name': '__main__', 'process': (id=66122, name='MainProcess'), 'thread': (id=130782869056384, name='MainThread'), 'time': datetime(2025, 3, 11, 17, 33, 55, 292986, tzinfo=datetime.timezone(datetime.timedelta(seconds=3600), 'CET'))}
Traceback (most recent call last):
  File "/tmp/ipykernel_66122/1334436161.py", line 2, in <module>
    1 / 0
    ~~^~~
ZeroDivisionError: division by zero

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/aemonge/.virtualenvs/summary-dspy-addl/lib/python3.12/site-packages/loguru/_handler.py", line 147, in emit
    formatter_record["exception"] = "".join(lines)
                                    ^^^^^^^^^^^^^^
  File "/home/aemonge/.virtualenvs/summary-dspy-addl/lib/python3.12/site-packages/loguru/_better_exceptions.py", line 573, in format_exception
    yield from self._format_exception(value, tb, is_first=True, from_decorator=from_decorator)
  File "/home/aemonge/.virtualenvs/summary-dspy-addl/lib/python3.12/site-packages/loguru/_better_exceptions.py", line 454, in _format_exception
    frames, final_source = self._extract_frames(
                           ^^^^^^^^^^^^^^^^^^^^^
  File "/home/aemonge/.virtualenvs/summary-dspy-addl/lib/python3.12/site-packages/loguru/_better_exceptions.py", line 216, in _extract_frames
    if is_valid(tb.tb_frame):
                ^^^^^^^^^^^
AttributeError: 'bool' object has no attribute 'tb_frame'
--- End of logging error ---
--- Logging error in Loguru Handler #16 ---
Record was: {'elapsed': datetime.timedelta(seconds=257, microseconds=729688), 'exception': (type=None, value=False, traceback=False), 'extra': {}, 'file': (name='1334436161.py', path='/tmp/ipykernel_66122/1334436161.py'), 'function': '<module>', 'level': (name='ERROR', no=40, icon='❌'), 'line': 4, 'message': 'An error occurred!', 'module': '1334436161', 'name': '__main__', 'process': (id=66122, name='MainProcess'), 'thread': (id=130782869056384, name='MainThread'), 'time': datetime(2025, 3, 11, 17, 33, 55, 292986, tzinfo=datetime.timezone(datetime.timedelta(seconds=3600), 'CET'))}
Traceback (most recent call last):
  File "/tmp/ipykernel_66122/1334436161.py", line 2, in <module>
    1 / 0
    ~~^~~
ZeroDivisionError: division by zero

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/aemonge/.virtualenvs/summary-dspy-addl/lib/python3.12/site-packages/loguru/_handler.py", line 147, in emit
    formatter_record["exception"] = "".join(lines)
                                    ^^^^^^^^^^^^^^
  File "/home/aemonge/.virtualenvs/summary-dspy-addl/lib/python3.12/site-packages/loguru/_better_exceptions.py", line 573, in format_exception
    yield from self._format_exception(value, tb, is_first=True, from_decorator=from_decorator)
  File "/home/aemonge/.virtualenvs/summary-dspy-addl/lib/python3.12/site-packages/loguru/_better_exceptions.py", line 454, in _format_exception
    frames, final_source = self._extract_frames(
                           ^^^^^^^^^^^^^^^^^^^^^
  File "/home/aemonge/.virtualenvs/summary-dspy-addl/lib/python3.12/site-packages/loguru/_better_exceptions.py", line 216, in _extract_frames
    if is_valid(tb.tb_frame):
                ^^^^^^^^^^^
AttributeError: 'bool' object has no attribute 'tb_frame'
--- End of logging error ---
--- Logging error in Loguru Handler #17 ---
Record was: {'elapsed': datetime.timedelta(seconds=257, microseconds=729688), 'exception': (type=None, value=False, traceback=False), 'extra': {}, 'file': (name='1334436161.py', path='/tmp/ipykernel_66122/1334436161.py'), 'function': '<module>', 'level': (name='ERROR', no=40, icon='❌'), 'line': 4, 'message': 'An error occurred!', 'module': '1334436161', 'name': '__main__', 'process': (id=66122, name='MainProcess'), 'thread': (id=130782869056384, name='MainThread'), 'time': datetime(2025, 3, 11, 17, 33, 55, 292986, tzinfo=datetime.timezone(datetime.timedelta(seconds=3600), 'CET'))}
Traceback (most recent call last):
  File "/tmp/ipykernel_66122/1334436161.py", line 2, in <module>
    1 / 0
    ~~^~~
ZeroDivisionError: division by zero

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/aemonge/.virtualenvs/summary-dspy-addl/lib/python3.12/site-packages/loguru/_handler.py", line 147, in emit
    formatter_record["exception"] = "".join(lines)
                                    ^^^^^^^^^^^^^^
  File "/home/aemonge/.virtualenvs/summary-dspy-addl/lib/python3.12/site-packages/loguru/_better_exceptions.py", line 573, in format_exception
    yield from self._format_exception(value, tb, is_first=True, from_decorator=from_decorator)
  File "/home/aemonge/.virtualenvs/summary-dspy-addl/lib/python3.12/site-packages/loguru/_better_exceptions.py", line 454, in _format_exception
    frames, final_source = self._extract_frames(
                           ^^^^^^^^^^^^^^^^^^^^^
  File "/home/aemonge/.virtualenvs/summary-dspy-addl/lib/python3.12/site-packages/loguru/_better_exceptions.py", line 216, in _extract_frames
    if is_valid(tb.tb_frame):
                ^^^^^^^^^^^
AttributeError: 'bool' object has no attribute 'tb_frame'
--- End of logging error ---

Asynchronous Logging

Loguru supports asynchronous logging, which can improve performance in I/O-bound applications:

Code
logger.add("async_log.log", enqueue=True)
logger.info("This log is written asynchronously.")
2025-03-11T17:33:55.371616+0100 | INFO | This log is written asynchronously.
2025-03-11 17:33:55.371 | INFO     | __main__:<module>:2 - This log is written asynchronously.

Colorful Logs: Easier to Read

Colorful logs are not just visually appealing; they make logs easier to read and debug. Loguru supports colorized logs out of the box, allowing you to differentiate log levels at a glance. For example:

Code
logger.add(sys.stdout, colorize=True, format="<green>{time}</green> | <level>{level}</level> | <cyan>{message}</cyan>")
logger.info("This is a colorful info log!")
2025-03-11T17:33:55.414127+0100 | INFO | This is a colorful info log!
2025-03-11 17:33:55.414 | INFO     | __main__:<module>:2 - This is a colorful info log!
2025-03-11T17:33:55.414127+0100 | INFO | This is a colorful info log!

Sending Logs to a Remote Server

You can use Loguru to send logs to a remote server via an API. Here’s a quick example using the requests library:

Code
import requests
from loguru import logger

def send_log_to_server(message):
    url = "https://example.com/api/logs"
    payload = {"message": message}
    response = requests.post(url, json=payload)
    return response.status_code

logger.add(send_log_to_server, level="INFO")
logger.info("This log will be sent to a remote server.")
2025-03-11T17:33:55.472785+0100 | INFO | This log will be sent to a remote server.
2025-03-11 17:33:55.472 | INFO     | __main__:<module>:11 - This log will be sent to a remote server.
2025-03-11T17:33:55.472785+0100 | INFO | This log will be sent to a remote server.

Saving Logs to a File

Loguru makes it easy to save logs to a file. You can specify the file path, rotation, and retention policies. Here’s an example:

Code
logger.add("logs/app.log", rotation="10 MB", retention="30 days")
logger.info("This log will be saved to a file.")
2025-03-11T17:33:56.044803+0100 | INFO | This log will be saved to a file.
2025-03-11 17:33:56.044 | INFO     | __main__:<module>:2 - This log will be saved to a file.
2025-03-11T17:33:56.044803+0100 | INFO | This log will be saved to a file.

Final Code

Here’s a complete example of a custom logger using Loguru, with advanced features like custom formatting, exception handling, and asynchronous logging:

Code
import os
import sys
from typing import Any, NoReturn

from loguru import logger as loguru_logger

LOG_LEVEL = os.getenv("LOG_LEVEL", "DEBUG")

def format_record(record: Any) -> str:
    """
    Format the log record.

    Parameters
    ----------
    record : Any
        The log record.

    Returns
    -------
    str
    """
    # Precompute the level name with the colon
    level_with_colon = f"{record['level'].name}"

    # Format the message
    msg = f"<level>{level_with_colon:<9}</level> "
    if record["level"].name in ["TRACE", "INFO", "ERROR"]:
        msg += f"<italic>{record['time']:DD/MMM/YY HH:mm:ss.SSS}</italic> - "

    if record["level"].name in ["TRACE", "DEBUG", "WARNING"]:
        msg += f"<underline>{record['name']}:{record['line']}</underline> - "

    # Use the color tags specified in your level definitions
    color_tags = {
        "TRACE": "cyan",
        "DEBUG": "blue",
        "INFO": "green",
        "WARNING": "magenta",
        "ERROR": "yellow",
        "CRITICAL": "red",
    }
    color_tag = color_tags.get(record["level"].name, "")

    msg += f"<{color_tag}>{record['message']}</{color_tag}>\n"

    return msg


loguru_logger.remove() 
loguru_logger.add(
    sys.stdout,
    format=format_record,
    level=LOG_LEVEL,
    colorize=True,
)


loguru_logger.level("TRACE", color="<cyan>")
loguru_logger.level("DEBUG", color="<blue>")
loguru_logger.level("INFO", color="<green>")
loguru_logger.level("WARNING", color="<magenta>")
loguru_logger.level("ERROR", color="<yellow>")
loguru_logger.level("CRITICAL", color="<red>")


class FancyLogError(Exception):
    """Exception from FancyLogger."""


class FancyLogger:
    """
    A example of a Fancy.

    Attributes
    ----------
    log_level : str
        The current logging level.
    """

    log_level: str = LOG_LEVEL

    def includes(self, level: str) -> bool:
        """
        Check if the current logging level is less or equal than the specified level.

        Parameters
        ----------
        level : str
            The level to compare against.

        Returns
        -------
        bool
            True if the current level is less severe, False otherwise.
        """
        level = level.upper()
        current_level = loguru_logger.level(self.log_level).no
        specified_level: int = loguru_logger.level(level).no
        return current_level <= specified_level

    @staticmethod
    def trace(*args: object, **kwargs: object) -> None:
        """
        Log a trace message, with file, line and date-time.

        Parameters
        ----------
        *args: object
            The message(s) to log.
        **kwargs: object
            The kwargs sent to loguru.

        Returns
        -------
        None
        """
        msg = " ".join(map(str, args))
        loguru_logger.opt(depth=1).trace(msg, **kwargs)

    @staticmethod
    def debug(*args: object, **kwargs: object) -> None:
        """
        Log a debug message, with file and line.

        Parameters
        ----------
        *args: object
            The message(s) to log.
        **kwargs: object
            The kwargs sent to loguru.

        Returns
        -------
        None
        """
        msg = " ".join(map(str, args))
        loguru_logger.opt(depth=1).debug(msg, **kwargs)

    @staticmethod
    def info(*args: object, **kwargs: object) -> None:
        """
        Log a info message, with date-time.

        Parameters
        ----------
        *args: object
            The message(s) to log.
        **kwargs: object
            The kwargs sent to loguru.

        Returns
        -------
        None
        """
        msg = " ".join(map(str, args))
        loguru_logger.info(msg, **kwargs)

    @staticmethod
    def warning(*args: object, **kwargs: object) -> None:
        """
        Log a warning message, with line and file.

        Parameters
        ----------
        *args: object
            The message(s) to log.
        **kwargs: object
            The kwargs sent to loguru.

        Returns
        -------
        None
        """
        msg = " ".join(map(str, args))
        loguru_logger.opt(depth=1).warning(msg, **kwargs)

    @staticmethod
    def error(*args: object, **kwargs: object) -> None:
        """
        Log an error message and raises an exception, with date-time.

        Parameters
        ----------
        *args: object
            The message(s) to log.
        **kwargs: object
            The kwargs sent to loguru.

        Returns
        -------
        None

        Raises
        ------
        FancyLogError
        """
        import traceback

        msg = " ".join(map(str, args))
        loguru_logger.error(msg, **kwargs)
        traceback.print_exc()
        raise FancyLogError

    @staticmethod
    def critical(*args: object, **kwargs: object) -> NoReturn:
        """
        Log a critical message and exits with code 1.

        Parameters
        ----------
        *args: object
            The message(s) to log.
        **kwargs: object
            The kwargs sent to loguru.

        Returns
        -------
        NoReturn
        """
        msg = " ".join(map(str, args))
        loguru_logger.critical(msg, **kwargs)
        sys.exit(1)


logging = FancyLogger()

Conclusion

Loguru simplifies logging in Python with its intuitive API and powerful features. Whether you need basic logging or advanced customization, Loguru has you covered. The final code provided demonstrates how to create a custom logger with Loguru, complete with custom formatting, exception handling, and asynchronous logging.