python logging messages and exit codes

Everyone knows that an application exit code should change based on the success, error or maybe warnings that happened during execution.

Lately i came along some python code that was structured the following way:

#!/usr/bin/python3
import sys
import logging

def warnme():
    # something bad happens
    logging.warning("warning")
    sys.exit(2)

def evil():
    # something evil happens
    logging.error("error")
    sys.exit(1)

def main():
    logging.basicConfig(
        level=logging.DEBUG,
    )   

    [..]

the situation was a little bit more complicated, some functions in other modules also exited the application, so sys.exit() calls were distributed in lots of modules an files.

Exiting the application in some random function of another module is something i dont consider nice coding style, because it makes it hard to track down errors.

I expect:

  • exit code 0 on success
  • exit code 1 on errors
  • exit code 2 on warnings
  • warnings or errors shall be logged in the function where they actually happen: the logging module will show the function name with a better format option: nice for debugging.
  • one function that exits accordingly, preferrably main()

How to do better?

As the application is using the logging module, we have a single point to collect warnings and errors that might happen accross all modules. This works by passing a custom handler to the logging module which tracks emitted messages.

Heres an small example:

#!/usr/bin/python3
import sys
import logging

class logCount(logging.Handler):
    class LogType:
        def __init__(self):
            self.warnings = 0
            self.errors = 0

    def __init__(self):
        super().__init__()
        self.count = self.LogType()

    def emit(self, record):
        if record.levelname == "WARNING":
            self.count.warnings += 1
        if record.levelname == "ERROR":
            self.count.errors += 1
            
def infome():
    logging.info("hello world")

def warnme():
    logging.warning("help, an warning")

def evil():
    logging.error("yikes")

def main():
    EXIT_WARNING = 2
    EXIT_ERROR = 1
    counter = logCount()
    logging.basicConfig(
        level=logging.DEBUG,
        handlers=[counter, logging.StreamHandler(sys.stderr)],
    )
    infome()
    warnme()
    evil()
    if counter.count.errors != 0:
        raise SystemExit(EXIT_ERROR)
    if counter.count.warnings != 0:
        raise SystemExit(EXIT_WARNING)

if __name__ == "__main__":
    main()
python3 count.py ; echo $?
INFO:root:hello world
WARNING:root:help, an warning
ERROR:root:yikes
1

This also makes easy to define something like:

  • hey, got 2 warnings, change exit code to error?
  • got 3 warnings, but no –strict passed, ingore those, exit with success!
  • etc..
Written on March 16, 2022