Python logging propagate 为真时为何出现重复日志
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.worker 是 app 的子 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 去执行输出逻辑,最终就会出现同一条日志被多次打印的重复现象。

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()。

一份不容易误判的检查清单
- 先打印目标 logger 和所有祖先的
handlers,确认是否挂了同类或不同类 handler。 - 再看传播链上第一个
propagate=False,它决定记录在哪里停止向上提供。 - 确认配置函数是否被重复调用,尤其检查是否每次调用都创建新的 StreamHandler。
- 最后用唯一消息和 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 去重机制。先明确唯一输出边界,再决定是否需要过滤器。
Go []rune 转回字符串时为什么长度发生变化
- 上一篇
- Go []rune 转回字符串时为什么长度发生变化
- 下一篇
- Go reflect.TypeFor 如何在泛型函数中获取类型
-
- 文章 · python教程 | 2小时前 | csv · python · DictWriter ·
- Python csv.DictWriter 缺少字段时如何设置 extrasaction
- 274浏览 收藏
-
- 文章 · python教程 | 3小时前 |
- Python pathlib Path.resolve 遇到不存在路径如何处理
- 278浏览 收藏
-
- 文章 · python教程 | 4小时前 |
- Python asyncio.timeout_at 如何用绝对截止时间包住任务
- 470浏览 收藏
-
- 文章 · python教程 | 8小时前 | 资源管理 · python · memoryview · 缓冲区协议 · 性能编程 · Python memoryview memoryview.release Python 缓冲区协议 bytearray BufferError mmap 资源释放
- Python memoryview 使用后如何释放底层资源
- 275浏览 收藏
-
- 文章 · python教程 | 9小时前 |
- Python array 类型码不匹配时怎样安全转换数值
- 311浏览 收藏
-
- 文章 · python教程 | 10小时前 |
- Python mmap 修改文件后如何保证变更刷回磁盘
- 415浏览 收藏
-
- 文章 · python教程 | 12小时前 | 网络编程 · Socket · python · Python socket 端口绑定 SO_REUSEADDR
- Python socket 设置 SO_REUSEADDR 后端口仍无法绑定怎么办
- 154浏览 收藏
-
- 文章 · python教程 | 13小时前 | 并发 · python · 线程安全 · http.server · Python http.server threading.Lock ThreadingHTTPServer
- Python http.server 共享状态时如何避免线程请求互相覆盖
- 344浏览 收藏
-
- 文章 · python教程 | 14小时前 |
- Python urllib.parse.quote_plus 处理空格和加号有什么区别
- 453浏览 收藏
-
- 文章 · python教程 | 16小时前 |
- Python json.loads 解析超大整数时如何限制输入风险
- 212浏览 收藏
-
- 文章 · python教程 | 17小时前 |
- Python zoneinfo 处理夏令时重复小时要看 fold 吗
- 102浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 30次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 131次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 67次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 24次使用
-
- CMMLU
- 深入了解CMMLU中文评估基准,涵盖67个学科主题,提供数据集下载、Zero-shot/Five-shot评估方法及排行榜,助力优化中文语言模型性能。
- 13次使用
-
- go格式“占位符”输入输出 类似python的input
- 2023-01-19 346浏览
-
- Golang如何调用Python代码详解
- 2023-01-07 235浏览
-
- HTTP 的 response 中的响应体和头部是分开发送的吗?
- 2023-01-28 387浏览
-
- B站等视频网站的弹幕用的是 websocket 还是轮询?
- 2023-02-16 447浏览
-
- Linux 下有什么命令行工具以时序显示 CPU 占用率?
- 2023-01-13 360浏览

