Python logging 为什么重复输出:propagate、handler 和层级配置的排查方法
开发环境里最烦人的日志问题之一,是同一条 INFO 在终端出现两遍,甚至一条带时间、一条不带时间。很多人先去搜业务代码,怀疑某个函数被调用了两次;但在 Python logging 里,更常见的原因是子 logger 把记录交给自己的 handler 后,又沿层级传给了父 logger。
你开发时大概率碰到过这类场景:代码里明明只写了一次日志打印,终端却连续输出好几条完全相同的日志,排查半天业务逻辑也没找到重复调用,问题其实出在logging模块的层级传播和handler挂载规则上。
排查这类重复输出问题,优先按两个方向验证:第一检查当前日志记录是否同时被子logger和父/根logger的handler捕获,第二检查日志配置初始化函数有没有被多次执行重复添加handler。
- 先看同一条记录是否同时命中了子 logger 和根 logger 的 handler。
- 子 logger 已经有专用 handler 时,通常把
propagate设为False。 - 全局配置必须只初始化一次,重复添加 StreamHandler 也会造成成倍输出。
- 修复后要同时核对 logger 的 handlers、父级关系和实际输出行数。
先用一段最小代码复现重复日志
下面的代码故意把同一个消息交给两个层级。app.db 有自己的终端处理器,根 logger 也有一个处理器;默认情况下,子 logger 处理完还会继续向上冒泡。
import logging
root = logging.getLogger()
root.setLevel(logging.INFO)
root.addHandler(logging.StreamHandler())
db_logger = logging.getLogger("app.db")
db_logger.setLevel(logging.INFO)
db_logger.addHandler(logging.StreamHandler())
db_logger.info("连接池已准备")
你会看到两行相同的“连接池已准备”。这不代表 info() 被执行了两次,而是同一条 LogRecord 先被 app.db 的 handler 输出,再因为 propagate=True 交给根 logger 的 handler 输出。

重复输出的关键:logger、handler 和 propagate 各管什么
排查时不要把三个概念混成一个。logger 负责接收日志记录并决定级别,handler 负责把记录写到终端或文件,propagate 只决定记录是否继续交给父 logger 的 handler。
logger 名称决定层级关系
logging.getLogger("app.db") 的父级不是一个手动保存的对象,而是名称层级中的 app,再向上才是根 logger。只要没有显式设置,记录就可能一路传到根 logger。
handler 才是真正写出一行文字的地方
同一个 logger 上挂两个 StreamHandler,也会出现两行;父子 logger 各挂一个,也会出现两行。判断重复来源,最有效的是直接打印处理器数量,而不是猜业务调用链。
logger = logging.getLogger("app.db")
print("name=", logger.name)
print("handlers=", logger.handlers)
print("propagate=", logger.propagate)
print("parent=", logger.parent.name or "root")
三步排查:先数 handler,再看传播,最后查初始化
第一步:确认是谁挂了 handler
如果项目使用了多个模块,先把目标 logger 和根 logger 的处理器列出来。logger.handlers 只包含当前 logger 的处理器,不包含父级;这正好能帮助你区分“自己重复挂载”和“向上冒泡”两类问题。
def show_logging_path(name: str) -> None:
logger = logging.getLogger(name)
current = logger
while current is not None:
print(
current.name or "root",
"handlers=", len(current.handlers),
"propagate=", getattr(current, "propagate", False),
)
current = current.parent
show_logging_path("app.db")
第二步:明确只保留一条输出路径
如果应用希望所有日志都由根 logger 统一格式化,子 logger 不要再添加输出 handler,只设置级别,然后保持 propagate=True。如果数据库模块必须写到独立文件,则保留子 logger 的 handler,并设置 propagate=False,避免它再回到根 logger。
第三步:检查配置函数是否重复进入
模块被重复导入通常不会重复执行同一个模块体,但配置函数可能在Web启动、测试夹具和热重载入口分别调用。最稳妥的做法是让配置函数具备幂等性:配置前先清理自己管理的 handler,或用明确的初始化标记。
def configure_db_logger() -> logging.Logger:
logger = logging.getLogger("app.db")
logger.setLevel(logging.INFO)
logger.propagate = False
if not logger.handlers:
handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter("DB %(levelname)s %(message)s"))
logger.addHandler(handler)
return logger
这里的 if not logger.handlers 只适用于这个 logger 的 handler 确实由当前配置函数独占的场景。如果测试需要重新配置输出目标,应该显式移除旧 handler,而不是无条件追加新 handler。

两种常见架构,选对 handler 归属
| 目标 | 子 logger | propagate | 结果 |
|---|---|---|---|
| 统一终端格式 | 不挂输出 handler | True | 由根 logger 集中输出 |
| 模块单独写文件 | 挂文件 handler | False | 只走模块自己的输出 |
| 临时调试 | 临时 handler | 按需 | 调试结束后移除 handler |
几个容易误判的坑
把 level 当成去重开关
把子 logger 的级别调成 WARNING 只能过滤低级别记录,不能阻止已经通过过滤的记录继续冒泡。去重首先看 handler 归属和 propagate。
只看 logger.handlers,漏掉根 logger
子 logger 的处理器列表为空,不代表没有输出路径。它可能完全依赖父级;排查必须沿 parent 逐级检查,尤其是根 logger。
测试环境反复配置但没有清理
如果每个测试都向同一个 logger 追加一个处理器,单测越跑输出的日志行数越多。测试结束时移除本次创建的 handler,并关闭它;不要用全局清理破坏其他测试的日志设置。
修复后的验收方式
修复不是把终端里的两行看成一行就结束。给日志内容加一个唯一标识,在一次调用中记录它,然后核对:目标 logger 的 handler 数量、父级是否仍然存在输出 handler、propagate 值,以及捕获到的行数是否为 1。
logger = configure_db_logger()
logger.info("probe-logging-001")
# 预期:终端只出现一行 probe-logging-001
# 若仍出现两行,继续沿 logger.parent 检查 handler
相关问题
为什么 basicConfig 有时不生效?
默认情况下,根 logger 已经有 handler 时,basicConfig() 不会再次替换配置。调试启动顺序时,先看根 logger 当前是否已有处理器,再决定是否在明确场景下使用强制重配置。
关闭 propagate 会不会让日志丢失?
如果子 logger 自己没有可用 handler,关闭传播会让记录没有输出。关闭前必须确认子 logger 已经挂好目标 handler,并且级别和格式都符合预期。
文件 handler 和终端 handler 可以同时保留吗?
可以,但要明确这是“同一条记录写两个目的地”,不是重复故障。若只想终端或文件二选一,就让记录只经过对应的输出路径。
把日志配置收口,重复问题就不再靠猜
Python logging 的重复输出大多能归结为两件事:同一层级挂了多个 handler,或者子 logger 输出后又通过 propagate 传给了父级。把配置入口收口,明确 handler 的归属,再用唯一标识核对实际行数,通常比在业务函数里到处加打印更快。
Java StringJoiner 怎么拼带前后缀的列表:空集合、嵌套分隔和流式收集
- 上一篇
- Java StringJoiner 怎么拼带前后缀的列表:空集合、嵌套分隔和流式收集
- 下一篇
- Linux 服务启动慢怎么定位:After、Requires 与启动时间线的核对方法
-
- 文章 · python教程 | 19分钟前 |
- Python 读取大 CSV 怎么避免内存峰值:分块迭代、类型推断与失败行处理
- 392浏览 收藏
-
- 文章 · python教程 | 2小时前 |
- Python 读取 CSV 用 csv、pandas 还是 polars:按文件规模与类型约束选
- 484浏览 收藏
-
- 文章 · python教程 | 3小时前 |
- Python 日志配置怎么选:basicConfig、dictConfig 与分层输出的取舍
- 368浏览 收藏
-
- 文章 · python教程 | 4小时前 |
- Python 日志旋转后为什么还写旧文件:handler 重载与进程句柄检查
- 204浏览 收藏
-
- 文章 · python教程 | 6小时前 | 并发 · 日志 · 消息队列 · python · 运维 · Python 背压 QueueHandler QueueListener 日志队列
- Python QueueHandler 为什么会丢日志:队列背压、QueueListener 与优雅退出
- 473浏览 收藏
-
- 文章 · python教程 | 7小时前 |
- Python 3.14 compression.zstd 怎么从内存压缩落到文件:level、open 与可选模块边界
- 406浏览 收藏
-
- 文章 · python教程 | 8小时前 |
- Python asyncio.TaskGroup 取消后为什么还有异常:ExceptionGroup 拆解与重试边界
- 285浏览 收藏
-
- 文章 · python教程 | 8小时前 |
- Python AsyncExitStack 怎么管理异步资源:多连接清理、异常传播与退出顺序
- 257浏览 收藏
-
- 文章 · python教程 | 10小时前 |
- Python tomllib 读取配置怎么处理缺失键:类型校验与默认值
- 374浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- ljg-skills
- ljg-skills 是李继刚开源的 AI 技能与提示词集合,面向大模型使用者整理了一批可复用的 prompt、角色设定和任务技能模板,适合用于学习提示词设计、搭建个人 AI 工作流和沉淀团队常用智能体能力。
- 5229次使用
-
- MELO音乐
- MELO音乐是一站式AI视频与音乐制作助手,对标suno, udio的高品质体验。提供伴奏生成、原创写词、无损导出、哼唱识曲、混音变声等全套音频与短视频编辑工具。无论是流行Kpop、电音说唱、民谣古风、摇滚儿歌还是商用轻音乐,MELO为你免费谱曲,轻松做同款!
- 4739次使用
-
- UniScribe
- UniScribe 是一款 AI 音视频转文字与内容整理工具,支持上传音频、视频文件或粘贴 YouTube 链接,自动生成转写文本、摘要、思维导图和关键问题,并支持多格式导出,适合会议记录、课程学习、访谈整理和内容创作复盘。
- 4687次使用
-
- 剧云
- 剧云是专业中文剧本创作平台,安全稳定运行十余年,集成AI编剧、剧本医生审核、人物小传、剧情关系图、大纲编写、多人协作、Word导入导出、版权管控功能,数据安全防护,轻松高效创作剧本。
- 4944次使用
-
- 万象有声
- 万象有声,一个专为有声创作者打造的新一代智能有声内容创作平台。平台提供专业的智能拆章、智能画本编辑、AI配音、AI生成音效、后期制作、智能对轨、智能审听等有声创作全流程工具,可以帮助创作者高效、低成本创作出引人入胜的有声作品。立即体验,让有声书制作更简单!
- 4903次使用
-
- golang xorm 自定义日志记录器之使用zap实现日志输出、切割日志(最新)
- 2023-02-24 432浏览
-
- Go语言常用的打log方式详解
- 2023-02-24 485浏览
-
- Go常用技能日志log包创建使用示例
- 2023-01-28 105浏览
-
- Go学习笔记之Zap日志的使用
- 2023-01-19 360浏览
-
- Go实现整合Logrus实现日志打印
- 2023-01-07 229浏览

