当前位置:首页 > 文章列表 > 文章 > python教程 > Python logging 怎样为异常记录 traceback 但避免重复输出

Python logging 怎样为异常记录 traceback 但避免重复输出

来源:17golang原创 2026-10-24 22:25:17 0浏览 收藏

值班时最容易误判的一类日志问题是:异常明明只发生了一次,终端却连续出现两份甚至三份 traceback。通常不需要牺牲 traceback 来“消重”。正确做法是让异常在一个明确的处理器里用 logger.exception() 记录一次,再把 logger 层级、handler 数量和 propagate 配置整理成一条可解释的输出路径。

先记住这三个判断
  • logger.exception() 本身是 ERROR 级别,并会附加当前异常信息,只应在异常处理器中调用。
  • 同一异常既调用 logger.exception() 又调用 logger.error(..., exc_info=True),会得到两条记录。
  • 子 logger 和父 logger 同时挂 handler,且子 logger 保持传播时,同一个 LogRecord 也可能被输出多次。

先判断重复来自调用还是 logger 层级

Python logging 把 logger、handler、filter 和 formatter 分开设计。异常处理器负责创建记录,handler 负责把记录送到控制台或文件;logger 的层级传播则决定同一条记录是否继续交给父级 handler。排查时先问“记录创建了几次”,再问“这条记录经过了几个输出点”。

Python logging 异常处理器生成 LogRecord 后沿子 logger、父 logger 到达多个 handler 的关系说明图
图1:Python logging 的异常记录、层级传播与输出 handler 关系说明图,不是运行截图。

如果输出中 traceback 的首行、业务消息和时间都重复一致,优先检查 handler 或传播;如果每份消息上下文不同,才继续检查异常处理器是否被多个层级重复调用。官方文档也说明,给一个 logger 和它的祖先同时挂 handler,可能让同一条记录发出多次。

异常处理器里只选一种 traceback 写法

最小写法是把记录动作放在 except 块内:

import logging

logger = logging.getLogger(__name__)

def load_profile(user_id: str) -> dict:
    try:
        # 这里模拟一个可能失败的外部读取动作。
        return read_profile_from_store(user_id)
    except OSError:
        # exception 会以 ERROR 级别记录当前异常的 traceback。
        logger.exception("读取用户资料失败 user_id=%s", user_id)
        # 记录后按业务约定返回空结果,避免把同一异常再次记录。
        return {}

logger.exception() 是 logger.error() 的便捷形式,区别在于它会把异常信息加入日志,而且官方要求它只在异常处理器中使用。如果你需要动态指定级别,可以改成下面这种写法,但同一个 except 分支不要两种都调用:

try:
    result = parse_payload(raw_payload)
except ValueError:
    # exc_info=True 让 error 记录当前异常;这里不再额外调用 logger.exception。
    logger.error("请求体解析失败", exc_info=True)
    result = None  # 让调用方得到明确的失败结果。

重复输出时先数 handler,再看 propagate

模块级 logger 通常使用 logging.getLogger(__name__),消息会向父 logger 传播。最常见的重复配置是:应用 logger 挂了一个 FileHandler,root logger 又挂了一个 StreamHandler,而应用 logger 仍然允许传播。此时一条记录同时写文件并打印终端并不一定是错误;真正的问题是你是否又在同一目的地配置了第二个 handler,或是否把两份内容误当成两次异常。

def describe_logger_tree(logger: logging.Logger) -> list[tuple[str, str]]:
    # 只读取当前 logger 的直接 handler,便于排查重复挂载。
    rows = [(logger.name, type(handler).__name__) for handler in logger.handlers]
    # propagate 为真时,输出还可能来自父级;这里把信号单独列出。
    rows.append((logger.name, f"propagate={logger.propagate}"))
    return rows

检查表可以这样用:

现象优先检查处理方向
同一消息同时出现在文件和终端是否本来就配置了两个目的地如果这是预期,保留;否则删掉不需要的 handler
终端出现两份完全相同消息root 与子 logger 是否都挂了 StreamHandler只在一个层级保留控制台 handler
每次请求重载后多一份初始化函数是否反复 addHandler把配置移到应用启动处,或先清理已有 handler
子模块日志多一份子 logger 的 propagate 是否仍为 True集中由父级处理,或在子 logger 明确设置 False

用集中配置固定唯一输出路径

如果应用需要同时写控制台和文件,可以把两个目的地都声明在一次配置中,再决定是否让应用 logger 继续向 root 传播。下面的模板把 handler 绑定到名为 service 的 logger,并用 propagate=False 切断 root 的第二条路径;这样 traceback 仍保留,但不会因为 root 还有同类 handler 而重复。

Python dictConfig 集中配置应用 logger、异常记录、控制台 handler、文件 handler、Formatter 和 propagate False 的结构图
图2:集中配置下的 logger、handler 与传播边界结构图,不是运行截图。
from logging.config import dictConfig

LOGGING = {
    "version": 1,
    "disable_existing_loggers": False,
    "formatters": {
        "standard": {
            # %(exc_text)s 由 formatter 在有异常信息时补充 traceback。
            "format": "%(asctime)s %(levelname)s %(name)s %(message)s"
        }
    },
    "handlers": {
        "console": {
            "class": "logging.StreamHandler",
            "level": "INFO",
            "formatter": "standard",
        },
        "file": {
            "class": "logging.FileHandler",
            "level": "ERROR",
            "filename": "service.log",
            "encoding": "utf-8",
            "formatter": "standard",
        },
    },
    "loggers": {
        "service": {
            "level": "INFO",
            "handlers": ["console", "file"],
            # 由 service 自己负责两个目的地,避免再交给 root 重复处理。
            "propagate": False,
        }
    },
}

def configure_logging() -> None:
    # 只在应用启动阶段调用一次,避免热加载时不断新增 handler。
    dictConfig(LOGGING)

这里的两个 handler 是两个明确的输出目的地,不是重复记录:INFO 及以上进入控制台,ERROR 及以上额外进入文件。若你只想让父 logger 统一管理,则可以移除 service 的 handler 配置并保留默认传播;不要同时保留两套策略。

回滚路径:从复杂配置退回最小单点

线上日志突然翻倍时,先不要大范围修改业务异常处理。可以暂时回滚到“一个应用 logger、一个控制台 handler、一次 logger.exception()”的最小路径,再逐项恢复文件、JSON formatter 或第三方 handler。这样能把问题拆成“记录重复”与“输出重复”两类。

import logging

def minimal_logging() -> logging.Logger:
    # basicConfig 只负责 root 的一次性基础配置,适合故障回退。
    logging.basicConfig(
        level=logging.INFO,
        format="%(levelname)s %(name)s %(message)s",
    )
    # 返回模块 logger,让业务代码不直接依赖 root 级函数。
    return logging.getLogger("service")

如果回退后仍重复,问题大概率不在 formatter,而在同一异常分支被多次记录、多个 handler 没有清理,或上层框架已经替你配置了日志。此时保留一份真实 traceback,记录当前 logger 的 handler 列表和传播状态,再恢复正式配置。

告警确认与复盘清单

修复后不要只看“行数变少了”。一次异常应仍然能在预期目的地找到消息、异常类型和 traceback;同时,正常 INFO 日志不应因为文件 handler 的级别设置而意外消失。值班确认可以按下面顺序完成:

  1. 确认异常分支只调用了一次 logger.exception() 或一次带 exc_info=True 的记录方法。
  2. 列出应用 logger 与 root 的直接 handler,确认同一目的地没有重复实例。
  3. 确认 propagate 的选择与 handler 所在层级一致,不要一边集中处理一边让子 logger 再向 root 复制。
  4. 确认日志轮转、文件权限和编码配置没有把同一条记录重新转发。
  5. 在复盘中记录触发异常的业务边界、最终保留的 traceback 位置和回滚方式。

常见问题

logger.error 和 logger.exception 有什么区别? 两者都可以记录 ERROR;logger.exception() 会自动附加当前异常信息,并且应该只在 except 中使用。普通错误消息不需要 traceback 时,用 logger.error() 更清楚。

设置 propagate=False 会不会删除 traceback? 不会。它只阻止记录继续交给父 logger 的 handler;当前 logger 自己挂的 handler 仍可正常格式化并输出异常信息。

为什么 basicConfig 看起来没有生效? 基础配置通常只在 root 尚未有 handler 时起作用。若框架或前一次初始化已经配置过 handler,再调用它不会自动替换现有配置,应把正式配置放到明确的启动阶段。

文件和终端各有一份算重复吗? 不一定。如果两个目的地是有意配置的,这是一次记录的两种投递;只有同一目的地重复出现,或业务告警按记录条数计费时,才需要进一步收敛路径。

小结:保留 traceback 的关键是使用正确的异常记录方式,避免重复输出的关键是只保留一条清晰的 logger-handler 责任链。先判断调用次数,再检查 handler 和 propagate,最后用集中初始化固定配置,排障会比盲目删除 traceback 稳定得多。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Python zipfile.ZipFile 怎样用 ZIP64 写入超大文件Python zipfile.ZipFile 怎样用 ZIP64 写入超大文件
上一篇
Python zipfile.ZipFile 怎样用 ZIP64 写入超大文件
Linux cgroup v2 memory.events 中 high 与 oom 如何区分
下一篇
Linux cgroup v2 memory.events 中 high 与 oom 如何区分
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之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推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    667次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    684次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    731次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    699次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    534次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码