Skip to content

py311: incorrect logging.LogRecord.module value (and related attrs) when using a custom log-level method #97941

@bastimeyer

Description

@bastimeyer

Bug report

#28287 has introduced changes to the logging.LogRecord's module, pathname and filename attributes when a custom log-level method gets used due to how the logging.Logger.findCaller() and logging.currentframe() methods were changed and how "internal" call stack frames are handled now. This change doesn't seem intentional to me, so I decided to open this bug report.

Related: streamlink/streamlink#4862

Your environment

  • CPython versions tested on: CPython 3.10.7 and CPython 3.11-rc2
  • Operating system and architecture: Arch Linux (x86_64) (irrelevant to the issue)

Reproduction

Consider the following example which defines a custom trace() logging method on a custom Logger subclass that gets set as the default logger class.

myproject/__init__.py

myproject/__main__.py

import myproject.logger
from myproject.content import content

content()

myproject/logger.py

import logging

TRACE = 5

logging.addLevelName(TRACE, "trace")

class MyLoggingClass(logging.getLoggerClass()):
    def trace(self, message, *args, **kwargs):
        if self.isEnabledFor(TRACE):
            self._log(TRACE, message, args, **kwargs)

logging.setLoggerClass(MyLoggingClass)

handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter("[{module}][{name}][{levelname}] {message}", style="{"))

root = logging.getLogger("myproject")
root.addHandler(handler)
root.setLevel(TRACE)

myproject/content.py

import logging
from myproject.logger import TRACE

log = logging.getLogger(__name__)

def content():
    log.trace("foo")
    log.log(TRACE, "bar")

Result

$ python3.10 -m myproject
[content][myproject.content][trace] foo
[content][myproject.content][trace] bar
$ python3.11 -m myproject
[logger][myproject.content][trace] foo
[content][myproject.content][trace] bar

As you can see, on py311 the custom logger.trace() method causes the log record to have a different module (and pathname + filename), but not any of the default log-level methods like the generic log() method.

From what it looks like, the call stack that's being read and iterated now stops at the first "non internal frame". Since the default log-level methods are considered "internal", everything's working fine. The custom trace() method however is not "internal" and is thus used as the source of the log call instead of the example's content() function.

This "internal frame" implementation is not quite right, because the trace() method is calling the private _log() method, and not any of the public log-level methods where it would make sense to stop on those "internal" call stack frames. It should also ignore call stack frames which are calling the private _log() method, so users can define custom log-level methods like in the example shown above.

The solution for now is defining two trace() methods, one for py<3.11 and one for py>=3.11, where the py311 one sets the stacklevel keyword on the _log() call to 2 (instead of the default value of 1), so the loop skips one additional call stack frame when checking for "internal" call stack frames, which would be the custom trace() method. This is a bad and very unintuitive workaround though.

Thanks for taking a look at this.

Metadata

Metadata

Assignees

Labels

invalidstdlibStandard Library Python modules in the Lib/ directory

Projects

Status

Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions