登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  python教程

Python 日志重复输出时怎么检查 handler 传播关系

来源:17golang原创

时间:2026-09-08 12:07:56 471浏览 收藏

Python 日志重复输出,通常不是同一行代码执行了两次,而是同一个 LogRecord 同时经过了子 logger 的 handler 和祖先 logger(最常见是 root logger)的 handler。先看 logger.handlers,再沿着 parent 检查 propagate,很快就能找到重复路径。LoggerAdapter 只负责给已有 logger 注入上下文,不会自动创建一条新的 handler 链。

要点速览
  • 子 logger 和 root logger 同时挂 handler,是重复输出的首要嫌疑。
  • 统一输出优先只配置 root handler;子模块必须独立输出时,再用 propagate=False 截断传播。
  • LoggerAdapter 适合传递 request_id 等字段,但 Formatter 要求的字段必须始终存在。

先把重复输出还原成 logger 拓扑

假设业务 logger 名为 service.api。它的父级可能是 service,再向上到 root。记录先由当前 logger 的直接 handler 处理;当 propagate 为真时,祖先 logger 的 handler 也会看到这条记录。注意,传播到祖先时并不会重新按祖先 logger 的级别做一次完整筛选,因此不要把“root 的 level 较高”当作去重方案。

'Python
图1:查看 LoggerAdapter 产生的 LogRecord、子 logger handler 与 root handler 的静态关系,先定位重复输出的两条处理路径。

排查时不要只调用 hasHandlers():它会把祖先 handler 也算进去。下面的小函数专门列出每一级 logger 的直接 handler、传播开关和 Formatter,输出结果比肉眼翻配置文件更可靠。

import logging

def show_logging_chain(logger_name: str) -> None:
    # 沿父级链检查直接挂载的 handler,不把祖先 handler 混在一起
    logger = logging.getLogger(logger_name)
    while logger is not None:
        print(f'name={logger.name!r} propagate={logger.propagate}')
        for index, handler in enumerate(logger.handlers):
            # 用对象编号区分同类型但实际不同的 handler
            formatter = handler.formatter
            pattern = formatter._fmt if formatter else '(默认格式)'
            print(f'  handler[{index}]={type(handler).__name__} '
                  f'id={id(handler)} format={pattern!r}')
        if logger is logging.getLogger():
            break
        logger = logger.parent

show_logging_chain('service.api')

如果 service.api 和 root 都列出一个 StreamHandler,且中间每一级的 propagate 都是 True,重复输出就已经解释清楚了。若只有一个 handler,继续检查是否有配置函数被重复调用,或是否存在两个输出目标本来就使用了相同格式。

三种配置方案怎么选

修复的关键不是盲目删除 handler,而是先确定“这条记录应该由谁负责输出”。下面三种结构足够覆盖大多数单进程服务。

'Python
图2:对比三种 logging 配置结构,选择 handler 的唯一归属,并把上下文字段交给 LoggerAdapter。
方案handler 归属适用场景容易踩的坑
root-only只放在 root多个模块统一输出到控制台或文件子 logger 又加 handler 就会重复
子 logger 独立子 logger 自己负责某模块需要独立文件或独立格式必须设置 propagate=False
集中配置配置入口统一创建应用启动时一次性声明所有输出热重载或重复初始化时要避免再 add

一般应用先选 root-only:库和业务模块都用 logging.getLogger(__name__),启动入口只配置一次。若 service.audit 必须单独写审计文件,则给它配置自己的 handler,并明确设置 service.audit.propagate = False,否则审计记录还会落到 root 的普通日志里。

LoggerAdapter 怎样保留上下文

上下文需求不应该通过“每个请求新建一个 handler”解决。LoggerAdapter 包装已有 logger,把 extra 字典合并进 LogRecord;它仍然遵守被包装 logger 的 handler 和传播关系。

import logging

logger = logging.getLogger('service.api')
adapter = logging.LoggerAdapter(logger, {'request_id': 'req-42'})

# Formatter 使用的 request_id 必须由每次日志记录提供
handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter(
    '%(levelname)s request=%(request_id)s %(message)s'))
logger.addHandler(handler)
logger.propagate = False  # 这个模块独立输出时,截断 root 的重复处理

adapter.info('读取用户资料')

这里的边界有两个:第一,Formatter 写了 request_id,普通的 logger.info() 若没有这个字段可能在格式化阶段报错;第二,extra 不能覆盖 logging 已经使用的保留字段。若项目中有普通 logger 和 Adapter 并存,建议把字段改成 Formatter 可选的默认值,或让两类调用共享一个明确的上下文注入约定。

把检查固化成可维护清单

  1. 配置入口只执行一次,尤其检查热重载、测试 fixture 和模块导入副作用。
  2. 每个 logger 只明确一个 handler 归属:root-only 或子 logger 独立,不要两边都挂同类输出。
  3. 需要请求字段时使用 LoggerAdapter 或统一的过滤器,不要复制 handler。
  4. 修改后重新打印 logger 链,确认直接 handler 数量、propagate 和 Formatter 字段都符合预期。

常见问题

为什么 logger.handlers 是空的,日志仍然会输出?

因为祖先 logger 可能有 handler。沿 parent 链继续看,并区分直接 handler 与 hasHandlers() 的汇总结果。

把子 logger 的 level 调成 ERROR 能去重吗?

不能。级别控制和 handler 数量是两件事;重复的 ERROR 仍会沿传播链到达多个 handler。应调整 handler 归属或关闭传播。

可以直接把所有子 logger 的 propagate 都设为 False 吗?

不建议。这样会切断统一 root 配置,子 logger 若没有自己的 handler 反而可能丢失输出。只对确实拥有独立输出目标的边界设置 False。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>