Custom log handlers in Python
python
Recently at work I had to create a custom log handler in Python to join two logs together. It wasn’t a complex task but it did require me to have a look at the source code for how Python defines the built-in handlers (e.g. FileHandler).
Here’s the code in question:
class LogHandler(logging.Handler):
def __init__(self, logger):
self.logger = logger
def emit(record):
try:
msg = self.format(record)
logger_method = getattr(self.logger, record.levelname.lower())
logger_method(msg)
except Exception:
self.handleError(record)
Lets break it down.
Lines 1 - 4 are pretty boring Python, the most interesting bit is that we’re inheriting from logging.Handler
which gives us access to methods that are useful in a handler (e.g. lines 7 where we use logging.Handler.format
to format the record.
The emit
method, lines 5 - 11, is the interesting part of this and where the magic happens. Part of the contract for implementing a handler is that it must have an emit
method. This isn’t enforced like in an ABC, instead if you don’t, an exception will be thrown wherever the emit
method is called. I don’t have an actual source but looking at the git blame for logging.Handler
it was added in 2002 and the PEP for abstract base classes (PEP 3119) wasn’t created until 2007, so ABC’s weren’t around when logging.Handler
was created.
In our emit
method, we first format the log message using the format
method provided by Handler
. This allows customising how the message is formatted. To do this you use Formatter
objects. Handler
s have the setFormatter
method that allows you to set this. By using the format
method on line 7, we keep our handler generic allowing the user to decide on how log messages should be formatted.
Lines 8 and 9 use Python’s dynamic nature to get the correct log level method off the logger we’re wrapping. record.levelname
gives us the level of the log message as a string (e.g. “INFO”), we lower it an use that name to get the method. We use the fact that in Python, methods are just attributes so we can retrieve them with getattr
. If you’re using MyPy, the type of logger_method
is Callable[[str],
None
]
. To make this more concrete, if LogHandler.emit
is called with a record where the levelname
attribute is “INFO”, self.logger.info
will be called. Whereas, for “DEBUG”, self.logger.debug
will be called.
Finally, the whole thing is wrapped in a try-except
block that catches all Exception
s and then calls Handler.handleError
on the record. This is a pattern I found in Python’s built-in handlers and, like Handler.format
, allows us to respect whatever behaviour the user configured for errors in logging.
There you go! A relatively simple block of code but I hope it’s a good example of how to create custom log handlers. The important bits to remember in general are to wrap in the try-except
block with Handler.handleError
in the error case, and use Handler.format
to format the message. Everything else will be specific to what you want your log handler to do.