直接继承 logging.Formatter 无法拿到异常信息,因为 record.exc_text 默认为 None,需在 format() 中显式调用 self.formatException(record) 才生成;自定义 Formatter 应判断 record.exc_info 是否存在,手动格式化并注入,避免依赖 %(exc_text)s 占位符。

为什么直接继承 logging.Formatter 无法拿到异常信息
因为默认情况下,Formatter.format() 接收到的 record 对象在没有触发 record.exc_info 时,record.exc_text 是 None,且 record.stack_info 也不会自动填充。即使你写了 logger.error("msg", exc_info=True),异常文本也只在调用 format() 前由 Formatter 内部的 formatException() 处理并写入 record.exc_text —— 但这个过程发生在你自定义的 format() 执行之后。所以如果你没显式调用 self.formatException(record),就看不到异常堆栈。
如何在自定义 Formatter 中安全捕获并格式化异常字段
必须重写 format(),并在其中判断 record.exc_info 是否存在;若存在,手动调用 self.formatException(record) 获取字符串,再注入到你想放的位置(比如追加到消息末尾,或塞进 JSON 字段)。注意:不要直接拼接 record.exc_text,因为它可能为空或未生成。
-
record.exc_info是三元组(type, value, traceback),非空即表示有异常待格式化 - 调用
self.formatException(record)才会真正生成人类可读的堆栈字符串 - 如果用了
%(exc_text)s占位符但没提前生成exc_text,该位置会显示为空字符串 - 若想把异常作为结构化字段(如 JSON 日志),建议单独提取为
record.exc_formatted,再在format()中使用
一个能同时输出行号、函数名、异常详情的实战 Formatter 示例
import logging
import traceback
<p>class RichFormatter(logging.Formatter):
def format(self, record):</p><h1>确保异常文本已生成</h1><pre class="brush:php;toolbar:false;"> if record.exc_info:
record.exc_formatted = self.formatException(record)
else:
record.exc_formatted = ""
# 补充缺失字段(避免 %()s 引发 KeyError)
if not hasattr(record, 'func_name'):
record.func_name = record.funcName
return super().format(record)使用方式
handler = logging.StreamHandler() formatter = RichFormatter( fmt="%(asctime)s | %(levelname)-8s | %(name)s | %(func_name)s:%(lineno)d | %(message)s%(exc_formatted)s", datefmt="%Y-%m-%d %H:%M:%S" ) handler.setFormatter(formatter) logger = logging.getLogger("demo") logger.addHandler(handler) logger.setLevel(logging.DEBUG)
测试
try: 1 / 0 except Exception: logger.error("出错了", exc_info=True)
这段代码的关键在于:不依赖 %(exc_text)s 自动展开,而是主动控制 exc_formatted 的生成时机和内容;同时兼容无异常场景(避免 AttributeError)。
JSON 日志中嵌入异常字段的注意事项
如果用 python-json-logger 或手写 json.dumps() 输出,别把 record.exc_text 直接当字段值——它含换行符和缩进,JSON 会解析失败。应该先用 traceback.format_exception(*record.exc_info) 得到列表,再 "\n".join() 或转成单行(用 repr() 包裹)。
立即学习“Python免费学习笔记(深入)”;
- 推荐做法:在
format()中预处理异常为单行字符串,例如repr("".join(traceback.format_exception(*record.exc_info))) - 避免在
extra参数里传异常对象,Logger.log()不会序列化它 - 如果用了
concurrent.futures或异步任务,exc_info可能在主线程丢失,需显式捕获后传入logger.error(..., exc_info=sys.exc_info())
异常字段不是“开了 exc_info=True 就自动好使”的黑盒,它依赖 formatter 主动触发和清理。最常被忽略的是:没检查 record.exc_info 就直接访问 record.exc_text,结果日志里异常位置永远是空的。


















