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:
2025-04-07T16:07:57.255467+0200 | INFO | Custom format logging![BR
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.remove() # Remove default handlerlogger.add(sys.stdout, colorize=True, format="<green>{time}</green> | <level>{level}</level> | <cyan>{message}</cyan>")logger.info("This is a colorful info log!")
[32m2025-04-07T16:07:57.261769+0200[0m | [32mINFO[0m | [36mThis is a colorful info log![0m
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.remove() # Remove default handlerlogger.add(sys.stdout, level="DEBUG")logger.warning("This is a warning message.")
[32m2025-04-07 16:07:57.267[0m | [35mWARNING [0m | [36m__main__[0m:[36m[0m:[36m3[0m - [35mThis is a warning message.[0m
Adding Handlers
You can add multiple handlers to log to different destinations, such as files, with different formats or levels:
Code
logger.remove() # Remove default handlerlogger.add("file.log", level="INFO", rotation="10 MB")logger.info("This log will go to both the console and a file.")
Advanced Features
Loguru supports asynchronous logging, which can improve performance in I/O-bound applications:
Code
logger.remove() # Remove default handlerlogger.add("async_log.log", enqueue=True)logger.info("This log is written asynchronously.")
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 requestsfrom loguru import loggerdef send_log_to_server(message): url ="https://example.com/api/logs" payload = {"message": message} response = requests.post(url, json=payload)return response.status_codelogger.remove() # Remove default handlerlogger.add(send_log_to_server, level="INFO")logger.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.remove() # Remove default handlerlogger.add("logs/app.log", rotation="10 MB", retention="30 days")logger.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 osimport sysfrom typing import Any, NoReturnfrom loguru import logger as loguru_loggerLOG_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}>{LINE_BREAK}"return msgloguru_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_LEVELdef 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).noreturn current_level <= specified_level@staticmethoddef 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)@staticmethoddef 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)@staticmethoddef 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)@staticmethoddef 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)@staticmethoddef 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@staticmethoddef 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()logging.info("This is a info log!")logging.debug("This is a debug log!")logging.trace("This is a trace log!")logging.warning("This is a warning log!")with contextlib.suppress(FancyLogError): logging.error("This is an error log!")with contextlib.suppress(SystemExit): logging.critical("This is a critical log!")
[32mINFO [0m [3m07/Apr/25 16:07:57.816[0m - [32mThis is a info log![0m[BR[34mDEBUG [0m [4m__main__:228[0m - [34mThis is a debug log![0m[BR[35mWARNING [0m [4m__main__:230[0m - [35mThis is a warning log![0m[BR[33mERROR [0m [3m07/Apr/25 16:07:57.817[0m - [33mThis is an error log![0m[BR[31mCRITICAL [0m [31mThis is a critical log![0m[BRNoneType: None
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.
Source Code
---title: "Logging in Python with Loguru"author: "Andres Monge <aemonge>"date: "2024-12-18"format: html: smooth-scroll: true code-fold: true code-tools: true code-copy: true code-annotations: true code-ansi: true ansi-colors: enable: true include-in-header: - text: | <script type="module"> import { AnsiUp } from "/assets/ansi_up.esm.js"; document.addEventListener("DOMContentLoaded", function () { const ansiUp = new AnsiUp({ escape_for_html: false }); const selector = '[data-class-output="code-rendered"] p'; const codeOutputs = document.querySelectorAll(selector); const LINE_BREAKS = ["[BR", "<br />"] codeOutputs.forEach((output) => { const lines = output.innerText.split(LINE_BREAKS[0]); let processedLines = lines.map((line) => ansiUp.ansi_to_html(line)); processedLines = processedLines.map(line => line.replace(/:::$/g, '')); output.innerHTML = processedLines.join(LINE_BREAKS[1]); }); }); </script>execute: output: asis freeze: auto---Logging is an essential part of any application, providing insights into itsruntime behavior. Python's built-in `logging` module is powerful but can becumbersome to configure. Enter **Loguru**, a third-party logging library thatsimplifies 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 LoguruLoguru allows you to customize the log format, level, and output destinations.Here's how to configure it:#### Custom Log FormatYou can define a custom format for your logs using Loguru's formatting syntax.For example, to include the log level, timestamp, and message:```{python}#| class-output: code-renderedLINE_BREAK ="[BR"from loguru import loggerimport sysimport contextliblogger.remove() # Remove default handlerlogger.add(sys.stdout, format="{time} | {level} | {message}"+ LINE_BREAK)logger.info("Custom format logging!")```### Colorful Logs: Easier to ReadColorful logs are not just visually appealing; they make logs easier to readand debug. Loguru supports colorized logs out of the box, allowing you todifferentiate log levels at a glance. For example:```{python}#| class-output: code-renderedlogger.remove() # Remove default handlerlogger.add(sys.stdout, colorize=True, format="<green>{time}</green> | <level>{level}</level> | <cyan>{message}</cyan>")logger.info("This is a colorful info log!")```### Log LevelsLoguru supports the standard log levels: `TRACE`, `DEBUG`, `INFO`, `WARNING`,`ERROR`, and `CRITICAL`. You can set the log level globally or per handler:```{python}#| class-output: code-renderedlogger.remove() # Remove default handlerlogger.add(sys.stdout, level="DEBUG")logger.warning("This is a warning message.")```#### Adding HandlersYou can add multiple handlers to log to different destinations, such as files,with different formats or levels:```{python}#| class-output: code-renderedlogger.remove() # Remove default handlerlogger.add("file.log", level="INFO", rotation="10 MB")logger.info("This log will go to both the console and a file.")```### Advanced FeaturesLoguru supports asynchronous logging, which can improve performance inI/O-bound applications:```{python}#| class-output: code-renderedlogger.remove() # Remove default handlerlogger.add("async_log.log", enqueue=True)logger.info("This log is written asynchronously.")```### Sending Logs to a Remote ServerYou can use Loguru to send logs to a remote server via an API. Here's a quickexample using the `requests` library:```{python}#| class-output: code-renderedimport requestsfrom loguru import loggerdef send_log_to_server(message): url ="https://example.com/api/logs" payload = {"message": message} response = requests.post(url, json=payload)return response.status_codelogger.remove() # Remove default handlerlogger.add(send_log_to_server, level="INFO")logger.info("This log will be sent to a remote server.")```### Saving Logs to a FileLoguru makes it easy to save logs to a file. You can specify the file path,rotation, and retention policies. Here's an example:```{python}#| class-output: code-renderedlogger.remove() # Remove default handlerlogger.add("logs/app.log", rotation="10 MB", retention="30 days")logger.info("This log will be saved to a file.")```### Final CodeHere’s a complete example of a custom logger using Loguru, with advancedfeatures like custom formatting, exception handling, and asynchronous logging:```{python}#| class-output: code-renderedimport osimport sysfrom typing import Any, NoReturnfrom loguru import logger as loguru_loggerLOG_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}>{LINE_BREAK}"return msgloguru_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_LEVELdef 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).noreturn current_level <= specified_level@staticmethoddef 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)@staticmethoddef 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)@staticmethoddef 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)@staticmethoddef 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)@staticmethoddef 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@staticmethoddef 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()logging.info("This is a info log!")logging.debug("This is a debug log!")logging.trace("This is a trace log!")logging.warning("This is a warning log!")with contextlib.suppress(FancyLogError): logging.error("This is an error log!")with contextlib.suppress(SystemExit): logging.critical("This is a critical log!")```### ConclusionLoguru 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.