'What's the worst level log that I just logged?

I've added logs to a Python 2 application using the logging module. Now I want to add a closing statement at the end, dependent on the worst thing logged.

If the worst thing logged had the INFO level or lower, print "SUCCESS!"

If the worst thing logged had the WARNING level, write "SUCCESS!, with warnings. Please check the logs"

If the worst thing logged had the ERROR level, write "FAILURE".

Is there a way to get this information from the logger? Some built in method I'm missing, like logging.getWorseLevelLogSoFar?

My current plan is to replace all log calls (logging.info et al) with calls to wrapper functions in a class that also keeps track of that information.

I also considered somehow releasing the log file, reading and parsing it, then appending to it. This seems worse than my current plan.

Are there other options? This doesn't seem like a unique problem.

I'm using the root logger and would prefer to continue using it, but can change to a named logger if that's necessary for the solution.



Solution 1:[1]

As you're only using the root logger, you could attach a filter to it which keeps track of the level:

import argparse
import logging
import random

LEVELS = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']

class LevelTrackingFilter(logging.Filter):
    def __init__(self):
        self.level = logging.NOTSET

    def filter(self, record):
        self.level = max(self.level, record.levelno)
        return True

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('maxlevel', metavar='MAXLEVEL', default='WARNING',
                        choices=LEVELS,
                        nargs='?', help='Set maximum level to log')
    options = parser.parse_args()
    maxlevel = getattr(logging, options.maxlevel)

    logger = logging.getLogger()
    logger.addHandler(logging.NullHandler())  # needs Python 2.7
    filt = LevelTrackingFilter()
    logger.addFilter(filt)

    for i in range(100):
        level = getattr(logging, random.choice(LEVELS))
        if level > maxlevel:
            continue
        logger.log(level, 'message')
    if filt.level <= logging.INFO:
        print('SUCCESS!')
    elif filt.level == logging.WARNING:
        print('SUCCESS, with warnings. Please check the logs.')
    else:
        print('FAILURE')

if __name__ == '__main__':
    main()

Solution 2:[2]

As you said yourself, I think writing a wrapper function would be the neatest and fastest approach. The problem would be that you need a global variable, if you're not working within a class

global worst_log_lvl = logging.NOTSET

def write_log(logger, lvl, msg):
    logger.log(lvl, msg)
    if lvl > worst_log_lvl:
        global worst_log_lvl
        worst_log_lvl = lvl

or make worst_log_lvl a member of a custom class, where you emulate the signature of logging.logger, that you use instead of the actual logger

 class CustomLoggerWrapper(object):
     def __init__(self):
         # setup of your custom logger
         self.worst_log_lvl = logging.NOTSET

     def debug(self):
         pass

     # repeat for other functions like info() etc.

Solution 3:[3]

There's a "good" way to get this done automatically by using context filters.

TL;DR I've built a package that has the following contextfilter baked in. You can install it with pip install ofunctions.logger_utils then use it with:

from ofunctions import logger_utils

logger = logger_utils.logger_get_logger(log_file='somepath', console=True)
logger.error("Oh no!")
logger.info("Anyway...")

# Now get the worst called loglevel (result is equivalent to logging.ERROR level in this case)

worst_level = logger_utils.get_worst_logger_level(logger)

Here's the long solution which explains what happens under the hood:

Let's built a contextfilter class that can be injected into logging:

class ContextFilterWorstLevel(logging.Filter):
    """
    This class records the worst loglevel that was called by logger
    Allows to change default logging output or record events
    """

    def __init__(self):
        self._worst_level = logging.INFO
        if sys.version_info[0] < 3:
            super(logging.Filter, self).__init__()
        else:
            super().__init__()

    @property
    def worst_level(self):
        """
        Returns worst log level called
        """
        return self._worst_level

    @worst_level.setter
    def worst_level(self, value):
        # type: (int) -> None
        if isinstance(value, int):
            self._worst_level = value

    def filter(self, record):
        # type: (str) -> bool
        """
        A filter can change the default log output
        This one simply records the worst log level called
        """
        # Examples
        # record.msg = f'{record.msg}'.encode('ascii', errors='backslashreplace')
        # When using this filter, something can be added to logging.Formatter like '%(something)s'
        # record.something = 'value'
        if record.levelno > self.worst_level:
            self.worst_level = record.levelno
        return True

Now inject this filter into you logger instance

logger = logging.getLogger()
logger.addFilter(ContextFilterWorstLevel())

logger.warning("One does not simply inject a filter into logging")

Now we can iter over present filters and extract the worst called loglevel like this:

    for flt in logger.filters:
        if isinstance(flt, ContextFilterWorstLevel):
            print(flt.worst_level)

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Vinay Sajip
Solution 2 meetaig
Solution 3 Orsiris de Jong