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.Handlerit 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. Handlers 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 Exceptions 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.