Repository: streamlink/streamlink (python)

Issue:
logging with custom formatter doesn't print function name

### Checklist

- [x] This is a bug report and not [a different kind of issue](https://github.com/streamlink/streamlink/issues/new/choose)
- [x] [I have read the contribution guidelines](https://github.com/streamlink/streamlink/blob/master/CONTRIBUTING.md#contributing-to-streamlink)
- [x] [I have checked the list of open and recently closed bug reports](https://github.com/streamlink/streamlink/issues?q=is%3Aissue+label%3A%22bug%22)
- [x] [I have checked the commit log of the master branch](https://github.com/streamlink/streamlink/commits/master)

### Collaboration

- [x] [I will provide feedback should a pull request be opened with a fix for the problem](https://github.com/streamlink/streamlink/blob/master/CONTRIBUTING.md#pull-request-feedback)

### Streamlink version

streamlink 8.1.2

### Description

When importing the streamlink module into a project and trying to use a custom logging formatter that uses funcName, the log entries just show None.

```
import logging

rootlogger = logging.getLogger()

formatter = logging.Formatter(
    '%(asctime)s %(name)s %(funcName)s %(levelname)s %(message)s')
console_handler = logging.StreamHandler()
console_handler.setFormatter(formatter)
rootlogger.addHandler(console_handler)
rootlogger.setLevel(logging.DEBUG)


def testfunc():
    rootlogger.info('test2')


testfunc()
import streamlink # fmt: off
testfunc()
```

### Debug/trace log

```text
> python logtest.py
2026-01-20 02:35:10,398 root testfunc INFO test2
2026-01-20 02:35:10,713 root None info test2
```


Patch:
diff --git a/src/streamlink/logger.py b/src/streamlink/logger.py
index ef41fab0006..f3f797d5ed6 100644
--- a/src/streamlink/logger.py
+++ b/src/streamlink/logger.py
@@ -176,7 +176,7 @@ def _log_record_factory(name, level, fn, lno, msg, args, exc_info, func=None, si
         # noinspection PyTypeChecker
         return WarningLogRecord(name, level, fn, lno, msg, args, exc_info, func, sinfo)
 
-    return _log_record_factory_default(name, level, fn, lno, msg, args, exc_info, func=None, sinfo=None, **kwargs)
+    return _log_record_factory_default(name, level, fn, lno, msg, args, exc_info, func, sinfo, **kwargs)
 
 
 # borrowed from stdlib and modified, so that `WarningMessage` gets passed as `msg` to the `WarningLogRecord`


Test patch:
diff --git a/tests/test_logger.py b/tests/test_logger.py
index 193601dcdae..cfe063c4b1f 100644
--- a/tests/test_logger.py
+++ b/tests/test_logger.py
@@ -174,6 +174,48 @@ def test_custom_module_name(self, caplog: pytest.LogCaptureFixture, log: logging
             ("test_logger", levelname, "bar"),
         ]
 
+    def test_logrecord(self, caplog: pytest.LogCaptureFixture, log: logging.Logger):
+        def get_lineno():
+            frame = currentframe()
+            assert frame
+            assert frame.f_back
+            return getframeinfo(frame.f_back).lineno
+
+        caplog.set_level(1, "streamlink")
+        log.info("test %(a)s %(b)s", {"a": "foo", "b": "bar"}, exc_info=ValueError("err"), stack_info=True)
+        lineno = get_lineno() - 1
+
+        assert [
+            (
+                r.exc_text,
+                r.filename,
+                r.funcName,
+                r.levelname,
+                r.levelno,
+                r.lineno,
+                r.message,
+                r.module,
+                r.name,
+                r.pathname,
+                r.stack_info is not None and len(r.stack_info) > 0,
+            )
+            for r in caplog.records
+        ] == [
+            (
+                "ValueError: err",
+                "test_logger.py",
+                "test_logrecord",
+                "info",
+                20,
+                lineno,
+                "test foo bar",
+                "test_logger",
+                "test",
+                __file__,
+                True,
+            ),
+        ]
+
     @pytest.mark.parametrize(
         ("level", "expected"),
         [
@@ -378,8 +420,27 @@ def test_capture_logrecord(
         lineno = self._warn([("Test warning", UserWarning)])
         path = Path(__file__)
         assert recwarn.list == []
-        assert [(r.name, r.levelname, r.pathname, r.filename, r.module, r.lineno, r.message) for r in caplog.records] == [
-            ("warnings", "userwarning", __file__, path.name, path.stem, lineno, f"Test warning\n  {__file__}:{lineno}"),
+        assert [
+            (
+                r.name,
+                r.levelname,
+                r.pathname,
+                r.filename,
+                r.module,
+                r.lineno,
+                r.message,
+            )
+            for r in caplog.records
+        ] == [
+            (
+                "warnings",
+                "userwarning",
+                __file__,
+                path.name,
+                path.stem,
+                lineno,
+                f"Test warning\n  {__file__}:{lineno}",
+            ),
         ]
 
     @pytest.mark.parametrize("log", [{"capture_warnings": True}], indirect=["log"])