Repository: streamlink/streamlink

Issue:
logger: custom trace() log level has incorrect module name on Python 3.11
Python 3.11 has changed how the call stack gets read when creating a new log record:
https://github.com/python/cpython/commit/5ca6d7469be53960843df39bb900e9c3359f127f

This causes the log record's module name to be set to Streamlink's `logger` module where the custom `trace()` method is defined, which is incorrect.

Example:
https://github.com/streamlink/streamlink/actions/runs/3168863166/jobs/5160366442#step:5:285
https://github.com/streamlink/streamlink/pull/4861#issuecomment-1264654242

Is this worth of a Python but report, or do we need to change how the custom `trace()` method is defined?

- `Logger._log()`
  https://github.com/python/cpython/blob/v3.11.0rc2/Lib/logging/__init__.py#L1610-L1634
- `Logger.findCaller()`
  https://github.com/python/cpython/blob/v3.11.0rc2/Lib/logging/__init__.py#L1561-L1593

Since there's no way to pass a custom call stack to the `Logger._log()` method, the `stacklevel` needs to be increased from 1 to 2 on py311 from what it looks like, so that it skips the `logger` module at the beginning of the call stack:

```py
class StreamlinkLogger(_BaseLoggerClass):
    if sys.version_info < (3, 11):
        def trace(self, message, *args, **kws):
            if self.isEnabledFor(TRACE):
                self._log(TRACE, message, args, **kws)
    else:
        def trace(self, message, *args, **kws):
            if self.isEnabledFor(TRACE):
                kws["stacklevel"] = 2
                self._log(TRACE, message, args, **kws)
```



Discussion:

Resolving patch:
diff --git a/src/streamlink/logger.py b/src/streamlink/logger.py
--- a/src/streamlink/logger.py
+++ b/src/streamlink/logger.py
@@ -3,6 +3,7 @@
 from datetime import datetime
 from logging import CRITICAL, DEBUG, ERROR, INFO, WARNING
 from pathlib import Path
+from sys import version_info
 from threading import Lock
 from typing import IO, List, Optional, TYPE_CHECKING, Union
 
@@ -46,9 +47,18 @@
 
 
 class StreamlinkLogger(_BaseLoggerClass):
-    def trace(self, message, *args, **kws):
-        if self.isEnabledFor(TRACE):
-            self._log(TRACE, message, args, **kws)
+    # fix module name that gets read from the call stack in the logging module
+    # https://github.com/python/cpython/commit/5ca6d7469be53960843df39bb900e9c3359f127f
+    if version_info >= (3, 11):
+        def trace(self, message, *args, **kws):
+            if self.isEnabledFor(TRACE):
+                # increase the stacklevel by one and skip the `trace()` call here
+                kws["stacklevel"] = 2
+                self._log(TRACE, message, args, **kws)
+    else:
+        def trace(self, message, *args, **kws):
+            if self.isEnabledFor(TRACE):
+                self._log(TRACE, message, args, **kws)
 
 
 class StringFormatter(logging.Formatter):


Test patch:
diff --git a/tests/test_logger.py b/tests/test_logger.py
--- a/tests/test_logger.py
+++ b/tests/test_logger.py
@@ -87,6 +87,17 @@ def test_trace_no_output(self, log: logging.Logger, output: StringIO):
         log.trace("test")  # type: ignore[attr-defined]
         assert output.getvalue() == ""
 
+    # https://github.com/streamlink/streamlink/issues/4862
+    def test_trace_module_name(self, caplog: pytest.LogCaptureFixture, log: logging.Logger):
+        caplog.set_level(1)
+        log = logging.getLogger(self.__class__.__module__)
+        log.trace("foo")  # type: ignore[attr-defined]
+        log.log(logger.TRACE, "bar")
+        assert [(record.module, record.levelname, record.message) for record in caplog.records] == [
+            ("test_logger", "trace", "foo"),
+            ("test_logger", "trace", "bar"),
+        ]
+
     def test_debug_out_at_trace(self, log: logging.Logger, output: StringIO):
         log.setLevel("trace")
         log.debug("test")