当前位置:首页 > 文章列表 > 文章 > python教程 > Python 日志重复输出时怎么检查 handler 传播关系

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

来源:17golang原创 2026-09-08 12:07:56 0浏览 收藏

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删除
Go x509.SystemCertPool 返回空池时怎么兼容不同系统Go x509.SystemCertPool 返回空池时怎么兼容不同系统
上一篇
Go x509.SystemCertPool 返回空池时怎么兼容不同系统
Java HttpClient 收到 404 时为什么 future 仍然正常完成
下一篇
Java HttpClient 收到 404 时为什么 future 仍然正常完成
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    25次使用
  • SuperCLUE中文大模型评测基准:功能、能力维度与应用指南
    SuperCLUE
    SuperCLUE是权威的中文大语言模型综合评测基准,涵盖语言理解、知识应用、AI Agent智能体及安全性等12项核心能力。通过多轮对话与客观测试,定期发布榜单与技术报告,为模型研发、优化及行业选型提供科学依据。
    178次使用
  • C-Eval中文评测基准:大语言模型多学科能力评估指南
    C-Eval
    深入了解C-Eval中文评估套件,涵盖52个学科与4级难度。本文详解其功能特点、Zero-shot/Few-shot使用方法及代码示例,助您全面评测LLM中文理解与泛化能力。
    113次使用
  • AI Prompt Library:免费AI提示词库,助力ChatGPT高效创作与营销
    AI Prompt Library
    探索AI Prompt Library免费资源库,涵盖营销、写作及多场景AI提示词。兼容ChatGPT、Claude等工具,一键复制优化输出,提升工作效率。
    40次使用
  • Generrated:DALL·E 2/3 AI绘画提示词灵感库与图像对比平台
    Generrated
    Generrated汇集9300+张DALL·E生成图像及对应提示词,支持查看完整图集、对比DALL·E 2与3版本差异,是AI绘图新手学习Prompt设计与获取创作灵感的实用工具。
    21次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议隐私政策
返回登录
  • 重置密码