33

I need to log plenty of data while running my system code. What logging packages I can use to have an efficient, asynchronous logging? Is the standard Python logging package (https://docs.python.org/2/library/logging.html) asynchronous by default?

4
  • 2
    No it's not. But it easy to write your own handler that drops the message into a Queue where it is picked up by an other thread. Commented Aug 23, 2017 at 14:49
  • @KlausD. Can you maybe explain more or suggest some link to read about it? Commented Aug 23, 2017 at 15:30
  • If it's linux you can use syslog or syslog-ng directly which is quick to use. Commented Aug 23, 2017 at 19:49
  • related? docs.python.org/3/howto/logging-cookbook.html#blocking-handlers Commented Sep 15, 2024 at 10:12

2 Answers 2

29

Async code can use the usual logging features without resorting to special async modules or wrappers. Code like this is possible.

import logging


async def do_some_async_stuff(self):
    logging.getLogger(__name__).info("Started doing stuff...")

    logging.getLogger(__name__).warn("Things went awry...")

The concern here is whether submitting log entries will incur some delay while the entries are written to file, depriving the asynchronous system the opportunity to run other tasks during the lapse. This can happen if a blocking handler that writes to file is added directly somewhere along the logging hierarchy.

There's a simple solution for this provided by the standard logging module: use a non-blocking handler that enqueues its messages to the desired blocking handler running in its own private thread.

Pureism aside, there's no hard-bound rule that precludes the use of the QueueHandler for providing async code that logs with a non-blocking log handler, used together with a blocking handler hosted in a QueueListener.

The solution below is entirely compatible with coroutines that call up the logging loggers and submit entries in typical fashion - wrappers with calls to .run_in_executor() aren't needed. Async code won't experience any blocking behavior from the logging system.

For example, a QueueHandler can be set up as the root handler

import queue
from logging.handlers import QueueHandler

log_queue     = queue.Queue()
queue_handler = QueueHandler(log_queue)  # Non-blocking handler.

root = logging.getLogger()
root.addHandler(queue_handler)           # Attached to the root logger.

And the blocking handler you want can be put inside a QueueListener:

from logging.handlers import QueueListener
from logging.handlers import RotatingFileHandler

rot_handler    = RotatingFileHandler(...)   # The blocking handler.
queue_listener = QueueListener(log_queue, 
                               rot_handler) # Sitting comfortably in its
                                            # own thread, isolated from
                                            # async code.
queue_listener.start()

Then configure the handler nested in the listener with whatever log entry formatting you need.

I personally like the rotating file handler because it limits the size and number of log files produced, deleting the oldest when a new backup is created.

Sign up to request clarification or add additional context in comments.

7 Comments

For a fully working example, you need to import QueueListener from logging.handlers as well.
You're correct, thank you @TroyDaniels
Threads suffer from the GIL in Cpython. They do not sit very comfortably. The ideal solution would be to have an async logging module in the std library that awaits the emit method of the handler
Not anymore (3.14).
Go on, you still have a few million answers and comments to update now
This solution isn't purist. It does present a hybrid async/threaded solution to logging. However, it may be more performant than using async executors to interface with the logging objects, as they also involve threading and the GIL.
For reference, this is the solution proposed in the logging module's documentation
22

You can execute logging.info() message using a pool of n worker, uing concurrent.futures.ThreadPoolExecutor, n should be always equals to one :

import concurrent.futures 
import logging

executor = concurrent.futures.ThreadPoolExecutor(max_workers=1) 

def info(self, msg, *args):
    executor.submit(logging.info, msg, *args)

4 Comments

Can you maybe explain what your code is supposed to do and how it relates mine question?
The code example shows, how to organize a kind of async queue of the "info" function calls as you requested. Very important here to have count of workers equals to one, that will give you exact order of the calls. In the case of n more then one you can have unexpected behavior - and order of the lines in the result log will be wrong. BR, Andrew
Note that if you change the values you are logging after you call info, it is indeterminate which version will be logged.
This solution requires changing existing / 3rd party code. Using QueueListener (see other answer above) is basically this w/o needing to change existing code and is thus a much better alternative.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.