Python 日志想显示真正调用方,可以调 stacklevel

16 次浏览4 条回复

封装一层日志 helper 后,日志默认显示的是 helper 那一行,排查调用方不太方便。Python 3.8+ 可以把 stacklevel 往上调:

import logging

logging.basicConfig(
    level=logging.INFO,
    format="%(levelname)s %(filename)s:%(lineno)d %(message)s",
)
log = logging.getLogger(__name__)

def notice(message):
    log.info(message, stacklevel=2)

def load_config():
    notice("loading config")

load_config()

这样记录的文件和行号会指向 load_config,而不是 notice。如果 helper 外面还有一层封装,就按实际调用栈把数值再加上去。环境:Python 3.8+,只用标准库 logging。

这个对日志 helper 很实用。封装不止一层时,可以让 helper 把深度参数透传,避免每个调用点手算:

def notice(message, *, stacklevel=1):
    log.info(message, stacklevel=stacklevel + 1)

外层再按实际多包了一层传 stacklevel=2。这样改包装层时,日志里的文件和行号不容易又指回 helper。

Nathan03Lv1#1

这个对日志 helper 很实用。封装不止一层时,可以让 helper 把深度参数透传,避免每个调用点手算:

def notice(message, *, stacklevel=1):
    log.info(message, stacklevel=stacklevel + 1)

外层再按实际多包了一层传 stacklevel=2。这样改包装层时,日志里的文件和行号不容易又指回 helper。

补一个容易踩的点:stacklevel 是按调用栈的栈帧计数,不是按业务函数名计数。中间加了装饰器或条件封装后,固定写 2 可能又偏一层。测试时可以检查日志记录的 funcName,通常比死盯行号更稳定。

uitfLv1#2

补一个容易踩的点:stacklevel 是按调用栈的栈帧计数,不是按业务函数名计数。中间加了装饰器或条件封装后,固定写 2 可能又偏一层。测试时可以检查日志记录的 funcName,通常比死盯行号更稳定。

对,funcName 比行号稳定。这个行为也可以用 pytest 的 caplog 固定下来,不用解析终端输出:

def test_notice_points_to_caller(caplog):
    with caplog.at_level(logging.INFO):
        load_config()

    record = next(r for r in caplog.records if r.message == "loading config")
    assert record.funcName == "load_config"

如果 helper 支持多层包装,再补一个装饰器场景的测试,改包装层时比较容易发现 stacklevel 偏了一层。

ZiorLv1#3

对,funcName 比行号稳定。这个行为也可以用 pytest 的 caplog 固定下来,不用解析终端输出:

def test_notice_points_to_caller(caplog):
    with caplog.at_level(logging.INFO):
        load_config()

    record = next(r for r in caplog.records if r.message == "loading config")
    assert record.funcName == "load_config"

如果 helper 支持多层包装,再补一个装饰器场景的测试,改包装层时比较容易发现 stacklevel 偏了一层。

装饰器场景里还有个细节:即使加了 functools.wraps,包装函数那一层栈帧也还在,wraps 只是复制元数据,不会替你调整 stacklevel。所以回归测试最好同时覆盖直调和带装饰器两条路径;funcName 用来断言调用方,行号留给格式展示会稳一点。