Dieter Vansteenwegen
Dieter Vansteenwegen

Reputation: 268

Subclassing logging.Logger to add own functionality

I'm writing code for a robotic system that needs to log to different places, depending on type of deployment/time during startup/...

I'd like to have an option to create a basic logger, then add handlers when appropriate.

I have a basic function in place to create a streamhandler:

def setup_logger() -> logging.Logger:
    """Setup logging.
    Returns logger object with (at least) 1 streamhandler to stdout.

    Returns:
        logging.Logger: configured logger object
    """
    logger = logging.getLogger()
    logger.setLevel(logging.DEBUG)
    stream_handler = logging.StreamHandler()  # handler to stdout
    stream_handler.setLevel(logging.ERROR)
    stream_handler.setFormatter(MilliSecondsFormatter(LOG_FMT))
    logger.addHandler(stream_handler)
    return logger

When the system has internet access, I'd like to add a mail handler (separate class, subclassed from logging.handlers.BufferingHandler). (Example below with a simple rotating file handler to simplify)

def add_rotating_file(logger: logging.Logger) -> logging.Logger:
    rot_fil_handler = logging.handlers.RotatingFileHandler(LOGFILE,
                                                           maxBytes=LOGMAXBYTES,
                                                           backupCount=3)
    rot_fil_handler.setLevel(logging.DEBUG)
    rot_fil_handler.setFormatter(MilliSecondsFormatter(LOG_FMT))
    logger.addHandler(rot_fil_handler)
    return logger

Usage would be:

logger = setup_logger()
logger = add_rotating_file(logger)

This looks "wrong" to me. Giving the logger to the function as an argument and then returning it seems weird and I would think I would better create a class, subclassing logging.Logger.

So something like this:

class pLogger(logging.Logger):

    def __init__(self):
        super().__init__()
        self._basic_configuration()

    def _basic_configuration(self):
        self.setLevel(logging.DEBUG)
        stream_handler = logging.StreamHandler()  # handler to stdout
        stream_handler.setLevel(logging.ERROR)
        stream_handler.setFormatter(MilliSecondsFormatter(LOG_FMT))
        self.addHandler(stream_handler)

    def add_rotating_handler(self):
        rot_file_handler = logging.handlers.RotatingFileHandler(LOGFILE,
                                                            maxBytes=LOGMAXBYTES,
                                                            backupCount=3)
        self.addHandler(rot_file_handler)

                                                

However, the super().init() function needs the logger name as an argument and -as far as I know-, the root logger should be created using logging.getLogger(), so without a name.

Another way would be to not subclass anything, but create a self.logger in my class, which seems wrong as well.

I found this stackexchange question which seems related but I can't figure out how to interpret the answer.

What's the "correct" way to do this?

Upvotes: 0

Views: 424

Answers (1)

Vinay Sajip
Vinay Sajip

Reputation: 99365

There's no particular reason I can see for returning the logger from add_rotating_file(), if that's what seems odd to you. And this (having handlers added based on conditions) doesn't seem like a reason to create a logger subclass. There are numerous ways you could arrange some basic handlers and some additional handlers based on other conditions, but it seems simplest to do something like this:

def setup_logger() -> logging.Logger:
    formatter = MilliSecondsFormatter(LOG_FMT)
    logger = logging.getLogger()
    logger.setLevel(logging.DEBUG)
    handler = logging.StreamHandler(sys.stdout)  # default is stderr
    handler.setLevel(logging.ERROR)
    handler.setFormatter(formatter)
    logger.addHandler(handler)
    if internet_is_available:
        handler = MyCustomEmailHandler(...)  # with whatever params you need
        handler.setLevel(...)
        handler.setFormatter(...) # a suitable formatter instance
        logger.addHandler(handler)
    if rotating_file_wanted:
        handler = RotatingFileHandler(LOGFILE,
                                      maxBytes=LOGMAXBYTES,
                                      backupCount=3)
        handler.setLevel(...)
        handler.setFormatter(...) # a suitable formatter instance
        logger.addHandler(handler)
    # and so on for other handlers
    return logger # and you don't even need to do this - you could pass the logger in instead

`

Upvotes: 1

Related Questions