12

I would like to create a Python logging class that can be inherited as a common means of logging configuration, yet seperately control the logging level of the base class from the parent. This is similar to How to use python logging in multiple modules. The answer by Vinay Sajip to use a LogMixin is very close. Below is my slightly modified version.

Most of my classes inherit smaller classes. For example:

filename: LogMixin.py

import logging, logging.config
import yaml
class LogMixin(object):
    __loggerConfigured = False
    @property
    def logger(self):
        if not self.__loggerConfigured:
            with open('log_config.yaml', 'rt') as f:
                config = yaml.load(f.read())
                logging.config.dictConfig(config)
            self.__loggerConfigured = True
        name = '.'.join([self.__class__.__name__])
        return logging.getLogger(name)

filename: Base.py

from LogMixin import LogMixin
class Base(LogMixin):
    def __init__(self):
        self.logger.debug("Debug Base")
    def run_base(self):
        self.logger.debug("Debug Running Base")
        self.logger.info("Info Running Base")
if __name__ == '__main__':
    my_base = Base()
    my_base.run_base()         

filename: Parent.py

from Base import Base
class Parent(Base):
    def __init__(self):
        self.logger.debug("Debug Parent")
    def run_parent(self):
        self.logger.debug("Debug Running Parent")
        self.logger.info("Info Running Parent")

if __name__ == '__main__':
    my_parent = Parent()
    my_parent.run_base()
    my_parent.run_parent()

filename: log_config.yaml

---
version: 1
disable_existing_loggers: False

# Configuring the default (root) logger is highly recommended
root:
    level: WARNING
    handlers: [console]

# Configuration for logger set with logging.getLogger(NAME)
loggers:
    Base:
        level: INFO
        handlers: [console]
        propagate: no
    Parent:
        level: DEBUG
        handlers: [console]
        propagate: no

formatters:
    simple:
        format: "%(asctime)s - %(name)s - %(levelname)s - %(message)s"

handlers:
    console:
        class: logging.StreamHandler
        formatter: simple
        stream: ext://sys.stdout
...

I get the benefits of the common logging configuration. However, I'd like independent control of the log levels for both Base and Parent. With the config file above, I get:

$ python Base.py                 
2015-03-16 00:06:23,716 - Base - INFO - Info Running Base
$ python Parent.py                
2015-03-16 00:06:19,682 - Parent - DEBUG - Debug Parent
2015-03-16 00:06:19,682 - Parent - DEBUG - Debug Running Base
2015-03-16 00:06:19,682 - Parent - INFO - Info Running Base
2015-03-16 00:06:19,682 - Parent - DEBUG - Debug Running Parent
2015-03-16 00:06:19,682 - Parent - INFO - Info Running Parent

I understand why I get this, I only have one logger "Parent". However, in general, I'd rather get the following:

$ python Base.py                 
2015-03-16 00:06:23,716 - Base - INFO - Info Running Base
$ python Parent.py                
2015-03-16 00:06:19,682 - Parent - DEBUG - Debug Parent
2015-03-16 00:06:19,682 - Base - INFO - Info Running Base
2015-03-16 00:06:19,682 - Parent - DEBUG - Debug Running Parent
2015-03-16 00:06:19,682 - Parent - INFO - Info Running Parent

(notice no DEBUG related to Base.py).
Or even better:

$ python Base.py                 
2015-03-16 00:06:23,716 - Base - INFO - Info Running Base
$ python Parent.py                
2015-03-16 00:06:19,682 - Parent - DEBUG - Debug Parent
2015-03-16 00:06:19,682 - Parent.Base - INFO - Info Running Base
2015-03-16 00:06:19,682 - Parent - DEBUG - Debug Running Parent
2015-03-16 00:06:19,682 - Parent - INFO - Info Running Parent

(Notice the name is Parent.Base so I can see the inheritance.) Is this possible with a single simple LogMixin class?

Community
  • 1
  • 1
proximous
  • 617
  • 1
  • 10
  • 28

1 Answers1

5

A metaclass would be more appropriate. When a class is defined it will get its own logger. Name mangling ensures each class uses its own logger.

import logging
import sys

logging.basicConfig(stream=sys.stdout)

class MetaBase(type):
    def __init__(cls, *args):
        super().__init__(*args)

        # Explicit name mangling
        logger_attribute_name = '_' + cls.__name__ + '__logger'

        # Logger name derived accounting for inheritance for the bonus marks
        logger_name = '.'.join([c.__name__ for c in cls.mro()[-2::-1]])

        setattr(cls, logger_attribute_name, logging.getLogger(logger_name))

class Base(metaclass=MetaBase):
    def __init__(self):
        self.__logger.error('init base')

    def func_base(self):
        self.__logger.error('func base')

class Parent(Base):
    def func_parent(self):
        self.__logger.error('func parent')

p = Parent()
p.func_base()
p.func_parent()

Results in:

ERROR:Base:init base
ERROR:Base:func base
ERROR:Base.Parent:func parent

This approach has a few additional benifites over mix in.

  • The logger for each class is created at class definition and accessed via a direct attribute reference. Avoids property and getLogger call
  • Subclasses only need to inherit base, no need to remember to add MixIn

I've simplified the example to demonstrate the key concept. Should work across files and with a config file.

i alarmed alien
  • 9,412
  • 3
  • 27
  • 40
Guy Gangemi
  • 1,533
  • 1
  • 13
  • 25
  • 1
    This is a very elegant solution, and I thank you for the code. – scientificCompNoob Jun 08 '21 at 22:03
  • This is a very elegant solution, and I thank you for the code. I have a small issue I cant seem to sort out. I have several classes that use the "Base" class. This has the unfortunate side effect of several classes having the same logger name. Ive tried to pull the parent class name in the metaclass and also tried setting the logger name in the "Base" class after initialization. Neither seem to work – scientificCompNoob Jun 08 '21 at 22:28
  • @scientificCompNoob I understand you want to have a unique name for each logger but the class name isn't unique across multiple modules. In that case, include cls.__module__ in the logger name in a way that suits your taste. If you want to be a bit extra, I've noticed that all logger names are stored in logging.Logger.manager.loggerDict, you could look in there first and only extend the name if there's a clash. – Guy Gangemi Jun 10 '21 at 00:43