logging.exception()是唯一推荐的异常记录入口,它自动绑定当前sys.exc_info()并强制级别为error,必须在except块内调用;配合raise ... from e可保留原始栈;多线程/协程需用loggeradapter或contextvars保证上下文隔离。

logging.exception() 是唯一推荐的异常记录入口
遇到异常时直接用 logging.error() 打印字符串,会丢掉完整栈帧——这是最常踩的坑。真正该用的是 logging.exception(),它自动绑定当前 sys.exc_info(),把 traceback 原样塞进日志行末尾,且强制级别为 ERROR。
常见错误现象:logging.error("failed: " + str(e)) 只输出错误消息,没有文件名、行号、调用链;更糟的是在 except 块外调用,sys.exc_info() 已清空,exception() 什么也打不出来。
- 必须在
except块内调用,且不能跨函数传递异常对象再 log - 不要手动拼接
traceback.format_exc(),除非你明确要截断或重写格式 - 如果想加额外上下文(如用户 ID、请求 ID),用
exc_info=True参数配合extra=...,而不是绕开exception()
Formatter 中 % (asctime)s 和 %(exc_text)s 的实际行为
很多人以为配置了 %(exc_text)s 就能自动打印异常,其实它只在日志级别 ≥ ERROR 且 exc_info 非空时才生效——而 logging.exception() 正是靠这个机制触发的。
典型误配:%(exc_text)s 写在 formatter 里,但 handler 接收的是 logging.info() 日志,结果永远为空。另外,%(asctime)s 默认不带毫秒,线上排障时容易分不清先后顺序。
立即学习“Python免费学习笔记(深入)”;
- 务必确保日志 handler 的 level ≤ ERROR,否则
exception()发出的日志被过滤掉 - 用
%(asctime)s.%(msecs)03d替代纯%(asctime)s,避免时间戳精度丢失 -
%(exc_text)s不会自动缩进,若需对齐,得在自定义 Formatter 子类中处理,别指望默认格式
捕获后重新抛出时怎么保留原始栈
业务逻辑里常见“先记录、再包装、再抛出”,比如 except ValueError as e: logging.exception("param invalid"); raise MyError("bad input") from e。这里的关键是用 from e,否则新异常的 traceback 从当前行开始,原始栈彻底丢失。
另一个陷阱:用 raise MyError(...) # 没有 from,日志里能看到原始栈(因为 exception() 在 except 块里),但上层捕获到的只有新异常,查因时得翻两份日志。
- 只要需要向上透传异常,必须显式写
from e或from None -
logging.exception()和raise ... from e是两个独立动作,缺一不可 - 别用
sys.exc_info()手动保存再 later raise,Python 3 的异常链机制更可靠
多线程/协程下 logger 的 name 和上下文隔离
默认 root logger 全局共享,多线程写日志会混在一起;异步场景下更麻烦——asyncio 切换 task 后,logger.name 没变,但 extra 里的 request_id 却可能错乱。
根本问题不在 logging 本身,而在上下文变量没绑定到 logger 实例。有人试图每个线程 new 一个 logger,反而破坏了 handler 复用和等级控制。
- 用
logging.getLogger(__name__)而非logging.getLogger(),按模块隔离 name - 线程安全靠
LoggerAdapter+extra,不是靠新建 logger - 协程中优先用
contextvars.ContextVar存 request_id,Formatter 里动态读取,避免在每次 log 调用时传 extra
事情说清了就结束。异常栈不是装饰,是排障唯一依据;日志不是越全越好,是关键信息不能断链。










