1. Flexible logging

Python’s standard logging module was conceived some time ago. It can be difficult to configure and it lacks the ability to easily communicate structured data.

The Turberfield logger module is designed for use in both web-native applications and offline tooling.

It is flexible and easily customisable.

from turberfield.utils.logger import LogManager

logger = LogManager().get_logger("root")
logger.info("Hello, World!")
2022-05-25 18:40:00.766978|    INFO|root| Hello, World!

The main feature of these Loggers is that you can set them to format any arbitrary data you might want to send them.

A logger has a frame which is a list of the formats of all the fields it can present to a LogAdapter.

print(logger.frame)
['{now}', '{level.name:>8}', '{logger.name}', ' {0}']

This frame can be modified at run time to add extra fields to the log. Unlike the standard logging module, it doesn’t matter if that data is missing:

import http

logger.frame += ["{status.name}"]
logger.info("Situation report", status=http.HTTPStatus.OK)
2022-05-25 18:53:51.767937|    INFO|root| Situation report|OK

You can supply a customised LogAdapter to perform filtering or reformatting on an endpoint-specific basis.

import re
import platform

from turberfield.utils.logger import LogAdapter

class Alarmist(LogAdapter):

    patterns = [
        (re.compile("NOTE"), (234, 0, 255)),
        (re.compile("INFO"), (0, 255, 255)),
        (re.compile("ERROR"), (234, 255, 0)),
        (re.compile("WARNING"), (255, 106, 0)),
        (re.compile("CRITICAL"), (255, 0, 106)),
    ]

    def colour_field(self, field, word):
        if "level" in field:
            r, g, b = next(
                (c for r, c in self.patterns if r.search(word)),
                (200, 200, 200)
            )
            return f"[38;2;{r};{g};{b}m{word}"
        else:
            return word

    def render(self, entry):
        if platform.system().lower() == "windows":
            return super().render(entry)

        return "|".join(
            self.colour_field(f, w)
            for f, w in zip(entry.origin.frame, entry.tokens)
        )

Once you have configured a logger, you can clone it as the basis of another.

root_logger = log_manager.get_logger("root")
logger = root_logger.clone("main")
logger.set_route(logger.Level.INFO, Alarmist(), sys.stderr)

logger.info("Hello, World!")
logger.warning("Stay safe out there!")

This example is included in the module. Run python -m turberfield.utils.logger to see that these log entries are now in colour on the terminal.

By default, logging goes to sys.stderr. You can log to a file by passing the necessary path.

logger.set_route(logger.Level.DEBUG, LogAdapter(), pathlib.Path("logger.log"))
logger.error("I didn't mean that.")
logger.debug("It doesn't hurt to check.")
logger.note("Whistle a happy tune!")
logger.critical(
    "We've run out of disk space!",
    status=http.HTTPStatus.INSUFFICIENT_STORAGE
)
$ cat logger.log
2022-05-25 19:11:55.260938|   ERROR|main| I didn't mean that.|
2022-05-25 19:11:55.261088|   DEBUG|main| It doesn't hurt to check.|
2022-05-25 19:11:55.261178|    NOTE|main| Whistle a happy tune!|
2022-05-25 19:11:55.261241|CRITICAL|main| We've run out of disk space!|INSUFFICIENT_STORAGE