0

TLDR

How can I get a custom logging level to accurately find out the pathname?

Problem description

When I try and add my own custom logging levels (e.g. for very verbose program tracing) I want to print the pathname. For most of the standard loggers, it is able to get this correctly, but for my own custom loggers, these all ultimately call logging.log(levelNum, message, *args, **kwargs) with levelNum as my custom level number. This means that only this line is ever printed, not the desired line.

A similar issue was raised here: Python, logging: wrong pathname when extending Logger and in this comment by "Aya" they say:

actually, you can get the correct pathname, but it's much more complicated [...]

I am currently using the formatter approach outlined here, and the adding of the log levels outlined here.

Is there a way to get the desired behaviour?

Some example code

The output from the below is:

/Users/oliver/ClionProjects/testing/venv/bin/python3 /Users/oliver/ClionProjects/testing/misc/misc.py 
writing some logs...
DEBUG: (/Users/oliver/ClionProjects/testing/misc/misc.py:90) something debug message
TRACE: (/Users/oliver/ClionProjects/testing/venv/lib/python3.11/site-packages/haggis/logs.py:211) something trace message
[2023-08-18 19:10:17,539]: WARNING: (/Users/oliver/ClionProjects/testing/misc/misc.py:92) something warning message

Process finished with exit code 0

Notably, the trace line is showing the wrong file path...

The code:

#!/usr/bin/env python3
"""
Some wrapping around the default logging module.
"""

import logging
import sys
from haggis.logs import add_logging_level
from termcolor import colored


class MyFormatter(logging.Formatter):
    """A nice formatter for logging messages."""
    line_formatting = f" {colored('(%(pathname)s:%(lineno)d)', 'light_grey')}"
    timestamp_formatting = f"{colored('[%(asctime)s]: ', 'green')}"
    trace_format = f"{colored('TRACE', 'cyan')}:{line_formatting} %(msg)s"
    debug_format = f"{colored('DEBUG', 'magenta')}:{line_formatting} %(msg)s"
    info_format = f"{colored('INFO', 'blue')}:{line_formatting} %(msg)s"
    print_format = f"%(msg)s"
    warning_format = f"{timestamp_formatting}{colored('WARNING', 'yellow')}:{line_formatting} %(msg)s"
    error_format = f"{timestamp_formatting}{colored('ERROR', 'red')}:{line_formatting} %(msg)s"
    critical_format = f"{timestamp_formatting}{colored('CRITICAL', 'red', attrs=['reverse', 'blink', 'bold'])}: {line_formatting} %(msg)s"

    def __init__(self):
        super().__init__(fmt=f"UNKNOWN: %(msg)s", datefmt=None, style='%')

    def format(self, record):

        # Save the original format configured by the user
        # when the logger formatter was instantiated
        format_orig = self._style._fmt

        # Replace the original format with one customized by logging level
        if record.levelno == logging.TRACE:
            self._style._fmt = MyFormatter.trace_format
        elif record.levelno == logging.DEBUG:
            self._style._fmt = MyFormatter.debug_format
        elif record.levelno == logging.INFO:
            self._style._fmt = MyFormatter.info_format
        elif record.levelno == logging.PRINT:
            self._style._fmt = MyFormatter.print_format
        elif record.levelno == logging.WARNING:
            self._style._fmt = MyFormatter.warning_format
        elif record.levelno == logging.ERROR:
            self._style._fmt = MyFormatter.error_format
        elif record.levelno == logging.CRITICAL:
            self._style._fmt = MyFormatter.critical_format
        else:
            raise NotImplementedError(f"We don't know how to format logging levels: {record.levelno}")

        # Call the original formatter class to do the grunt work
        result = logging.Formatter.format(self, record)

        # Restore the original format configured by the user
        self._style._fmt = format_orig

        return result


class StdOutFilter(logging.Filter):
    def filter(self, rec):
        return rec.levelno <= logging.PRINT


class StdErrFilter(logging.Filter):
    def filter(self, rec):
        stdout_filter = StdOutFilter()
        return not stdout_filter.filter(rec)


def setup_console_output():
    fmt = MyFormatter()
    stdout_handler = logging.StreamHandler(sys.stdout)
    stdout_handler.setFormatter(fmt)
    stdout_handler.addFilter(StdOutFilter())
    logging.root.addHandler(stdout_handler)
    stderr_handler = logging.StreamHandler(sys.stderr)
    stderr_handler.setFormatter(fmt)
    stderr_handler.addFilter(StdErrFilter())
    logging.root.addHandler(stderr_handler)


add_logging_level('TRACE', logging.DEBUG - 5)
add_logging_level('PRINT', logging.WARNING - 5)

if __name__ == "__main__":
    logging.root.setLevel(logging.TRACE)
    setup_console_output()
    print("writing some logs...")
    logging.debug("something debug message")
    logging.trace("something trace message")
    logging.warning("something warning message")
oliversm
  • 1,771
  • 4
  • 22
  • 44

1 Answers1

0

Increase the stacklevel

So after learning more about stack levels when reading about warnings, it seems the logging module also has this functionality.

Inside of the logToRoot function (either in the haggis module or the answer given here) there is a call to:

logging.log(levelNum, message, *args, **kwargs)

Here tell this to refer to the calling stack, by increasing the stack level from its default value of 1 up to 2, via:

logging.log(levelNum, message, *args, **kwargs, stacklevel=2)

After doing this the output matches what is expected: enter image description here produces:

DEBUG: (/Users/oliver/ClionProjects/testing/misc/misc2.py:141) something debug message
TRACE: (/Users/oliver/ClionProjects/testing/misc/misc2.py:142) something trace message
[2023-08-23 11:57:33,845]: WARNING: (/Users/oliver/ClionProjects/testing/misc/misc2.py:143) something warning message
oliversm
  • 1,771
  • 4
  • 22
  • 44