Code Walkthrough

micropython_587px-Felling_a_gumtree_c1884-1917_Powerhouse_Museum.jpg
A mountain ash being felled using springboards, c. 1884–1917, Australia; Tyrrell Photographic Collection, Powerhouse Museum; Public Domain

Levels

This module is nice in that it doesn't require any other libraries other than the built-in time module.

There is a list that defines the levels: the value and a name. That's used to convert values to names, as well as create a global variable for each level. They can be used directly as, for example, logging.ERROR.

Download: file
import time

levels = [(0,  'NOTSET'),
          (10, 'DEBUG'),
          (20, 'INFO'),
          (30, 'WARNING'),
          (40, 'ERROR'),
          (50, 'CRITICAL')]

for value, name in levels:
    globals()[name] = value

def level_for(value):
    """Convert a numberic level to the most appropriate name.

    :param value: a numeric level

    """
    for i in range(len(LEVELS)):
        if value == LEVELS[i][0]:
            return LEVELS[i][1]
        elif value < LEVELS[i][0]:
            return LEVELS[i-1][1]
    return LEVELS[0][1]

Getting a Logger

To get hold of a logger, you use the getLogger function. You pass it the name of the logger you want to create or retrieve. This way you can ask for a logger anywhere in your code. Specifying the same name will get you the same logger.

Download: file
logger_cache = dict()

def getLogger(name):
    """Create or retrieve a logger by name.

    :param name: the name of the logger to create/retrieve

    """
    if name not in logger_cache:
        logger_cache[name] = Logger()
    return logger_cache[name]

Logger

The core of the module is the Logger class. By default loggers use a PrintHandler (which we'll look at below) that simply uses print to output the messages. To change that to a different handler use the addHandler method. The method is called addHandler to be closer to CPython's logger. It works slightly differently in that it actually adds an additional handler the the logger rather than replacing it.

Logger as  a level property that allows you to get and set the cuttoff priority level. Messages with a level below the one set are ignored.

Finally, there is the log method that is the core of the class. This takes the level to log at, a format string, and arguments to be inserted into the format string. The % operator is used (passing it the supplied arguments) to create the message.

Download: file
class Logger(object):
    """Provide a logging api."""

    def __init__(self):
        """Create an instance.

        :param handler: what to use to output messages. Defaults to a PrintHandler.

        """
        self._level = NOTSET
        self._handler = PrintHandler()

    def setLevel(self, value):
        """Set the logging cuttoff level.

        :param value: the lowest level to output

        """
        self._level = value

    def addHandler(hldr):
        """Sets the handler of this logger to the specified handler.
        *NOTE* this is slightly different from the CPython equivalent which adds
        the handler rather than replaceing it.

        :param hldr: the handler

        """
        self._handler = hldr
        
    def log(self, level, format_string, *args):
        """Log a message.

        :param level: the priority level at which to log
        :param format_string: the core message string with embedded formatting directives
        :param args: arguments to ``format_string.format()``, can be empty

        """
        if level >= self._level:
            self._handler.emit(level, format_string % args)

Finally, there is a convenience method for logging at each level.

Download: file
    def debug(self, format_string, *args):
        """Log a debug message.

        :param format_string: the core message string with embedded formatting directives
        :param args: arguments to ``format_string.format()``, can be empty

        """
        self.log(DEBUG, format_string, *args)

    def info(self, format_string, *args):
        """Log a info message.

        :param format_string: the core message string with embedded formatting directives
        :param args: arguments to ``format_string.format()``, can be empty

        """
        self.log(INFO, format_string, *args)

    def warning(self, format_string, *args):
        """Log a warning message.

        :param format_string: the core message string with embedded formatting directives
        :param args: arguments to ``format_string.format()``, can be empty

        """
        self.log(WARNING, format_string, *args)

    def error(self, format_string, *args):
        """Log a error message.

        :param format_string: the core message string with embedded formatting directives
        :param args: arguments to ``format_string.format()``, can be empty

        """
        self.log(ERROR, format_string, *args)

    def critical(self, format_string, *args):
        """Log a critical message.

        :param format_string: the core message string with embedded formatting directives
        :param args: arguments to ``format_string.format()``, can be empty

        """
        self.log(CRITICAL, format_string, *args)

Handlers

We skipped over that part of the file. And what is that PrintHandler we saw in the constructor?

Looking at Logger's log method above, we see that the handler object is used to emit (i.e. send out) the message. The format_string and args are combined using the % operator and the result is sent, along with the level, to the emit method of the handler.

Here's the builtin PrintHandleralong with the LoggingHandler abstract base class*.

LoggingHandler provides a method, format, which takes the level and message to be logged and returns the string to be output, built from a timestamp, the name of the level, and the message.

It also contains a placeholder for the emit method which raises a NotImplementedError as this method must be implemented by subclasses.

Download: file
class LoggingHandler(object):
    """Abstract logging message handler."""

    def format(self, level, msg):
        """Generate a timestamped message.

        :param level: the logging level
        :param msg: the message to log

        """
        return '{0}: {1} - {2}'.format(time.monotonic(), level_for(level), msg)

    def emit(self, level, msg):
        """Send a message where it should go.
        Place holder for subclass implementations.
        """
        raise NotImplementedError()

PrintHandler subclasses LoggingHandler and provides an implementation of emit which uses LoogingHandler's format method to create the string to be output and prints it. This handler is bundled into the logging module since this is usually what you will need.

Download: file
class PrintHandler(LoggingHandler):
    """Send logging messages to the console by using print."""

    def emit(self, level, msg):
        """Send a message to teh console.

        :param level: the logging level
        :param msg: the message to log

        """
        print(self.format(level, msg))

*An abstract base class is not meant to be directly instantiated, rather it is to be subclassed.

This guide was first published on Mar 18, 2019. It was last updated on Mar 18, 2019. This page (Code Walkthrough) was last updated on Nov 21, 2019.