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 较高”当作去重方案。

排查时不要只调用 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,而是先确定“这条记录应该由谁负责输出”。下面三种结构足够覆盖大多数单进程服务。

| 方案 | 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 可选的默认值,或让两类调用共享一个明确的上下文注入约定。
把检查固化成可维护清单
- 配置入口只执行一次,尤其检查热重载、测试 fixture 和模块导入副作用。
- 每个 logger 只明确一个 handler 归属:root-only 或子 logger 独立,不要两边都挂同类输出。
- 需要请求字段时使用 LoggerAdapter 或统一的过滤器,不要复制 handler。
- 修改后重新打印 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。
-
167 收藏
-
Golang · Go问答 | 1星期前 | golang · slog · 日志排查 · 结构化日志 · Go 1.21 · 结构化日志 JSON日志 Go slog.WithGroup slog嵌套字段 日志属性247 收藏
-
167 收藏
-
文章 · python教程 | 2个月前 | logging · Python教程 · 后端开发 · 日志排查 · Python logging 日志重复 propagate addHandler basicConfig324 收藏
-
文章 · linux | 2个月前 | Linux · 服务治理 · 日志排查 · 运维教程 · Linux 服务管理器 journalctl 服务重启 运维排查 RestartSec start-limit-hit408 收藏
-
215 收藏
-
213 收藏
-
264 收藏
-
264 收藏
-
488 收藏
-
文章 · python教程 | 8小时前 | 并发 · 日志 · Python教程 · 多进程 · 排障 · Python Fork multiprocessing 多进程日志 spawn QueueHandler QueueListener300 收藏
-
107 收藏
-
文章 · python教程 | 10小时前 | 异步编程 · Python教程 · 超时处理 · 任务取消 · Python asyncio CancelledError TaskGroup wait_for shield441 收藏
-
475 收藏
-
468 收藏
-
286 收藏
-
270 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习