How to extend the logging.Logger Class?

At this stage, I believe that the research I have made so far and the example provided with the intention to wrap up the solution is sufficient to serve as an answer to my question. In general, there are many approaches that may be utilised to wrap a logging solution. This particular question aimed to focus on a solution that utilises logging.Logger class inheritance so that the internal mechanics can be altered, yet the rest of the functionality kept as it is since it is going to be provided by the original logging.Logger class.

Having said that, class inheritance techniques should be used with great care. Many of the facilities provided by the logging module are already sufficient to maintain and run a stable logging workflow. Inheriting from the logging.Logger class is probably good when the goal is some kind of a fundamental change to the way the log data is processed and exported.

To summarise this, I see that there are two approaches for wrapping logging functionality:

1) The Traditional Logging:

This is simply working with the provided logging methods and functions, but wrap them in a module so that some of the generic repetitive tasks are organised in one place. In this way, things like log files, log levels, managing custom Filters, Adapters etc. will be easy.

I am not sure if a class approach can be utilised in this scenario (and I am not talking about a super class approach which is the topic of the second item) as it seems that things are getting complicated when the logging calls are wrapped inside a class. I would like to hear about this issue and I will definitely prepare a question that explores this aspect.

2) The Logger Inheritance:

This approach is based on inheriting from the original logging.Logger class and adding to the existing methods or entirely hijacking them by modifying the internal behaviour. The mechanics are based on the following bit of code:

# Register our logger.
logging.setLoggerClass(OurLogger)
my_logger = logging.getLogger("main")

From here on, we are relying on our own Logger, yet we are still able to benefit from all of the other logging facilities:

# We still need a loggin handler.
ch = logging.StreamHandler()
my_logger.addHandler(ch)

# Confgure a formatter.
formatter = logging.Formatter('LOGGER:%(name)12s - %(levelname)7s - <%(filename)s:%(username)s:%(funcname)s> %(message)s')
ch.setFormatter(formatter)

# Example main message.
my_logger.setLevel(DEBUG)
my_logger.warn("Hi mom!")

This example is crucial as it demonstrates the injection of two data bits username and funcname without using custom Adapters or Formatters.

Please see the xlog.py repo for more information regarding this solution. This is an example that I have prepared based on other questions and bits of code from other sources.


This line

self.logger = logging.getLogger("myApp")

always retrieves a reference to the same logger, so you are adding an additional handler to it every time you instantiate MyLogger. The following would fix your current instance, since you call MyLogger with a different argument both times.

self.logger = logging.getLogger(name)

but note that you will still have the same problem if you pass the same name argument more than once.

What your class needs to do is keep track of which loggers it has already configured.

class MyLogger(object):
    loggers = set()
    def __init__(self, name, format="%(asctime)s | %(levelname)s | %(message)s", level=INFO):
        # Initial construct.
        self.format = format
        self.level = level
        self.name = name

        # Logger configuration.
        self.console_formatter = logging.Formatter(self.format)
        self.console_logger = logging.StreamHandler(sys.stdout)
        self.console_logger.setFormatter(self.console_formatter)

        # Complete logging config.
        self.logger = logging.getLogger(name)
        if name not in self.loggers:
            self.loggers.add(name)
            self.logger.setLevel(self.level)
            self.logger.addHandler(self.console_logger)

This doesn't allow you to re-configure a logger at all, but I leave it as an exercise to figure out how to do that properly.

The key thing to note, though, is that you can't have two separately configured loggers with the same name.


Of course, the fact that logging.getLogger always returns a reference to the same object for a given name means that your class is working at odds with the logging module. Just configure your loggers once at program start-up, then get references as necessary with getLogger.

Tags:

Python

Logging