Python logging with Loguru

March 9, 2022

Logging made simple

Python has built in logging, but it's not always simple to setup for every scenario. Loguru simplifies the Python logging setup process and brings other conveniences. Here I'll demonstrate some benefits of using Loguru for easier logging in simple scenarios like a command line script.

Why logs?

  1. Debugging problems is easier
  2. Monitor or report on your system usage
  3. Control information displayed and logged

Why Loguru?

  1. Faster logging setup
  2. Easier logging configuration
  3. Better management of logging functionality
  4. Avoid unexpected logging issues

pip install loguru (into a virtual environment)
There are many ways to do virtual environments, remember to set one up based on your version of Python3 and package manager preferences. In the example below we create a virtual environment "env" and activate it using the "source" command. Then pip will install loguru into the env.

python3 -m venv env; source env/bin/activate; pip install loguru

Your first log messages to stdout.
loguru will automatically configure a default logger for you. It will print messages to stdout using default formatting. Try logger.debug logger.info and logger.error with a message.

from loguru import logger

logger.debug("debug message to stdout")
logger.info("info message to stdout")
logger.error("error message to stdout")

Logging Exceptions
Decorate your main function with @logger.catch to log all unexpected tracebacks. Below we have an oops function that will crash our program. After investigating the logged traceback, the mistake in the code can be found.

from loguru import logger
import sys

def oops():
    'one' + 1

@logger.catch
def main():
    logger.debug('About to do an oops.')
    oops()

if __name__ == '__main__':
    sys.exit(main())

Adding 'one' + 1 causes a TypeError exception. Ooops! That was all logged by logger.catch because loguru was watching the main function for tracebacks. Notice the traceback information is more detailed than the standard Python traceback. This is because loguru uses the 'better-exceptions' library to display additional info with nice formatting. The main reason for the problem is usually at the bottom of the traceback. Looks like we should have added 1 + 1...

2022-04-18 13:32:12.672 | DEBUG | __main__:main:9 - About to do an oops.
2022-04-18 13:32:12.673 | ERROR | __main__:<module>:13 - An error has been caught in function '<module>', process 'MainProcess' (76367), thread 'MainThread' (4725654976):
Traceback (most recent call last):
> File "loguru_tests.py", line 13, in <module>
sys.exit(main())
│ │ └ <function main at 0x10eda99d8>
│ └ <built-in function exit>
└ <module 'sys' (built-in)>
File "loguru_tests.py", line 10, in main
oops()
└ <function oops at 0x10e2de1e0>
File "loguru_tests.py", line 5, in oops
'one' + 1
TypeError: can only concatenate str (not "int") to str

Configuring multiple 'handlers'
What if you want to write log files to multiple locations? That's when you'll configure multiple handlers each with a different type of sink. A sink is: 'An object in charge of receiving formatted logging messages and propagating them to an appropriate endpoint.' For example, one handler's sink to stdout and another handler's sink to a log file. New handlers can be added using logger.add, but my own preference for configuration is this dict / json structure. This way you configure everything you want in json once, perhaps even loading your config from a config file in the future.

  1. sys.stdout
  2. /tmp/mylogs/{time}.log
{"handlers":
    [          
        {
            "sink": sys.stdout,
            "level": "INFO",
            "format": "<level>{level: <3} {message}</level>"
        },

        {
            "sink": "/tmp/mylogs/{time}.log",
            "level": "DEBUG"
        },
    ],
}

Above you see we have two logging handlers. One to sys.stdout, with the default level of INFO (so we don't spam our users with debug info) and a custom format (to simplify the output printed to our users). And another handler to a log file with more detailed logs with level of DEBUG and default formatting. See the documentation for .add for more arguments and an example of using .add if you prefer that over JSON. Logger.add

Pass your configuration parameters to logger.configure(**config).

config = {
    "handlers": [
        {"sink": sys.stdout, "level": "INFO"},
        {"sink": "/tmp/mylogs/{time}.log", "level": "DEBUG"},
    ],
}
logger.configure(**config)
logger.info("Write this message to STDOUT and /tmp/mylogs/timestamp.log")

Now sprinkle those log messages in your code to make your users and maintainers happy. Checkout additional features like automatic log cleanup and retention policies. Happy logging! See more details on Loguru's GitHub

Return to blog