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

Python logging propagate 为真时为何出现重复日志

来源:17golang原创

时间:2026-09-15 06:39:14 306浏览 收藏

Python 日志出现“一条消息打印两遍”,通常不是 propagate 把同一条记录复制成了两个对象,而是同一个 LogRecord 依次交给了子 logger 和祖先 logger 上的多个 handler。最常见的组合是:业务 logger 自己挂了一个 StreamHandler,同时 propagate=True 又把记录送到 root logger 的 StreamHandler。

要点速览
  • 重复输出先查 handler 的挂载位置,不要先改日志级别。
  • propagate=True 会把记录交给祖先 logger 的 handler,但不会再次按祖先 logger 的 level/filter 过滤。
  • 集中式日志只保留高位 handler;子 logger 需要独立输出时,配合 propagate=False 切断上送。

先判断:到底是哪两个 handler 在输出

logger 的名字按点号形成层级。例如 app.workerapp 的子 logger,最终还会接近 root logger。重复日志排查要把“谁产生日志”和“谁真正写出日志”分开看。下面的检查函数不会改变配置,只打印当前 logger 的直接 handler、传播开关和祖先链。

import logging

def inspect_logger(name: str) -> None:
    # 只检查配置结构,不发送新的日志记录。
    logger = logging.getLogger(name)
    print(f"logger={logger.name!r} propagate={logger.propagate}")
    print("direct handlers:", [type(item).__name__ for item in logger.handlers])

    # 沿父级向上查找,定位可能重复处理同一记录的 handler。
    parent = logger.parent
    while parent is not None:
        print(f"ancestor={parent.name or 'root'} handlers="
              f"{[type(item).__name__ for item in parent.handlers]}")
        if not parent.propagate:
            break
        parent = parent.parent

inspect_logger("app.worker")

如果子 logger 和 root 都出现了 StreamHandler,且中间没有 propagate=False,重复输出就有了明确解释。注意 hasHandlers() 只能告诉你“当前链路能找到 handler”,不能替代对每一层 handlers 的逐层检查。

logging 模块默认的日志传播机制是子 Logger 产生的日志事件,会沿着父级 Logger 链路逐层向上传递,每一层父 Logger 只要挂载了 Handler 就会尝试输出这条日志。当 propagate 属性设为 True 时,子 Logger 打印的日志除了会经过自身绑定的 Handler 输出,还会被逐层交给上层所有未设置过 Handler 级别的父 Logger 去执行输出逻辑,最终就会出现同一条日志被多次打印的重复现象。
Python logging 子 logger、父 logger、root logger 与 StreamHandler FileHandler 的传播关系示意图
图1:Python logging 层级与 handler 归属示意图,propagate=True 让同一记录可被子级和祖先级 handler 分别处理。

propagate=True 为什么会把同一条日志送到多处

propagate 的默认值是 True。当 app.worker 记录一条 ERROR 时,先会提供给它自己直接挂载的 handler;如果允许传播,记录还会继续提供给 app、root 等祖先 logger 的 handler,直到传播被切断。这里的“传播”是把记录交给祖先 handler,不是把日志级别再从头计算一次。

配置组合可见结果适合场景
子 logger 无 handler,root 有一个通常只输出一次应用统一收集日志
子 logger 有 handler,propagate=True,root 也有同一记录可能输出多次一般应避免
子 logger 有 handler,propagate=False只在子 logger 的边界输出模块需要独立文件或格式

因此,单纯把 root 的级别从 INFO 调到 WARNING,并不能可靠修复重复。祖先 logger 的 level/filter 不会替代 handler 去重;要修的是 handler 的归属边界。

怎么改才不会重复:只保留一个输出边界

集中式配置最简单:业务和库 logger 不直接添加 handler,统一交给 root 或某个应用顶层 logger。下面的示例把输出责任留给 root,子 logger 只负责产生记录。

import logging

root = logging.getLogger()
root.setLevel(logging.INFO)

# 演示时先清理旧 handler,避免重复运行配置函数叠加输出。
root.handlers.clear()
root.addHandler(logging.StreamHandler())

worker = logging.getLogger("app.worker")
worker.info("任务已进入队列")  # 只由 root 的 handler 输出

如果某个库或模块必须写入独立文件,则反过来:只给它挂 FileHandler,并设置 propagate=False。这表示它的记录停在自己的输出边界,不再送到 root。生产代码还应让配置函数具备幂等性,避免每次初始化都再次 addHandler()

Python logging 日志传播链路示意图
logging propagate 为 True 时的日志向上传递流程
Python logging 集中式输出与 propagate=False 独立输出边界示意图
图2:避免重复日志的配置边界示意图,集中式和独立式输出分别保留唯一的 handler 归属链。

一份不容易误判的检查清单

  1. 先打印目标 logger 和所有祖先的 handlers,确认是否挂了同类或不同类 handler。
  2. 再看传播链上第一个 propagate=False,它决定记录在哪里停止向上提供。
  3. 确认配置函数是否被重复调用,尤其检查是否每次调用都创建新的 StreamHandler。
  4. 最后用唯一消息和 handler 身份验证输出次数,不要用提高 level 的方式掩盖结构问题。

如果应用统一输出,推荐“子 logger 不挂 handler、root 或应用顶层只挂一次”;如果模块要独立输出,推荐“模块挂自己的 handler,同时 propagate=False”。两种方案都可以,但同一条记录不要同时拥有两个输出责任。

相关问题

把 propagate 设置为 False 会丢日志吗?

只有当当前 logger 及其子链没有可用 handler 时,才可能看不到输出。设置前要确认目标 logger 自己已经有负责写出的 handler。

为什么只调用 logging.basicConfig 仍然重复?

basicConfig() 只负责快速配置 root,而且 root 已有 handler 时默认不会重复配置。重复往往来自业务 logger 手动添加 handler,或初始化函数反复创建 handler。

能不能给父 logger 再加一个过滤器解决?

过滤器可以做业务筛选,但不是 handler 去重机制。先明确唯一输出边界,再决定是否需要过滤器。

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