如何避免Python日志中LogRecord对象被篡改?
日志处理器间LogRecord对象篡改的规范解决方法
问题场景
我开发的应用配置了两个日志处理器:
- 控制台输出处理器:需要缩短异常信息
- 文件输出处理器:需要完整异常回溯
但由于LogRecord对象是可变的,且在处理器间传递时未被克隆,先执行的处理器(比如控制台)修改该对象后,会直接影响后续处理器的输出结果。
最小复现示例(Python 3.10.5)
>>> def div0(): ... try: ... 1/0 ... except Exception: ... logging.exception("Oops") # 此时调用div0()会用默认日志处理器打印消息和回溯 >>> class ShortExceptionFormatter(logging.Formatter): ... def formatException(self, ei): ... return repr(ei[1]) >>> consoleHandler = logging.StreamHandler() >>> consoleHandler.setFormatter(ShortExceptionFormatter()) >>> rootLogger = logging.getLogger() >>> rootLogger.addHandler(consoleHandler) # 现在两个处理器都会打印消息和回溯,不符合预期 >>> div0() ERROR:root:Oops Traceback (most recent call last): File "<pyshell#2>", line 3, in div0 ZeroDivisionError: division by zero Oops Traceback (most recent call last): File "<pyshell#2>", line 3, in div0 ZeroDivisionError: division by zero # 移除默认日志处理器后,自定义格式化器可正常工作 >>> defaultHandler = rootLogger.handlers.pop(0) >>> div0() Oops ZeroDivisionError('division by zero') # 但重新添加默认处理器后,其输出会被自定义格式化器篡改 >>> rootLogger.addHandler(defaultHandler) >>> div0() Oops ZeroDivisionError('division by zero') ERROR:root:Oops ZeroDivisionError('division by zero')
核心疑问
是否存在简洁规范的方法来隔离各处理器的记录对象,或避免篡改记录?该问题源于record.exc_text被篡改,但疑问适用于任何可能被处理器、过滤器或其他函数修改的参数。
当前临时解决方案(非理想)
我目前用备份恢复exc_text的方式临时解决,但显然不够优雅:
class ShortExceptionFormatter(logging.Formatter): def format(self, record): exc_text = record.exc_text record.exc_text = '' message = super().format(record) record.exc_text = exc_text return message
规范解决方案
方法1:格式化前克隆LogRecord对象
通过克隆LogRecord,让每个处理器操作独立的对象,彻底避免相互影响。可以自定义包装格式化器,或直接在处理器的emit方法中克隆记录:
import logging from copy import copy class IsolatedFormatter(logging.Formatter): def format(self, record): # 克隆记录,避免修改原对象 cloned_record = copy(record) return super().format(cloned_record) # 控制台处理器使用隔离格式化器 class ShortExceptionFormatter(IsolatedFormatter): def formatException(self, ei): return repr(ei[1]) consoleHandler = logging.StreamHandler() consoleHandler.setFormatter(ShortExceptionFormatter()) # 文件处理器使用默认格式化器(或自定义完整回溯格式化器) fileHandler = logging.FileHandler('app.log') fileHandler.setFormatter(logging.Formatter('%(levelname)s:%(name)s:%(message)s\n%(exc_info)s')) rootLogger = logging.getLogger() rootLogger.addHandler(consoleHandler) rootLogger.addHandler(fileHandler)
方法2:利用exc_info重新生成异常文本
如果不想克隆对象,可以在需要完整回溯的处理器中,跳过已被修改的exc_text,直接使用exc_info重新生成完整异常信息:
class FullTracebackFormatter(logging.Formatter): def format(self, record): # 保存可能已被修改的exc_text,处理后恢复 original_exc_text = record.exc_text # 强制重新生成完整异常文本 record.exc_text = None message = super().format(record) # 恢复原exc_text,不影响其他处理器 record.exc_text = original_exc_text return message # 文件处理器使用该格式化器 fileHandler = logging.FileHandler('app.log') fileHandler.setFormatter(FullTracebackFormatter())
方法3:使用独立Logger实例
为控制台和文件输出分别创建独立的Logger,各自配置不同的处理器和格式化器,避免共享同一个LogRecord对象:
# 控制台Logger console_logger = logging.getLogger('console_logger') console_logger.setLevel(logging.DEBUG) consoleHandler = logging.StreamHandler() consoleHandler.setFormatter(ShortExceptionFormatter()) console_logger.addHandler(consoleHandler) # 文件Logger file_logger = logging.getLogger('file_logger') file_logger.setLevel(logging.DEBUG) fileHandler = logging.FileHandler('app.log') fileHandler.setFormatter(logging.Formatter('%(levelname)s:%(name)s:%(message)s\n%(exc_info)s')) file_logger.addHandler(fileHandler) # 使用时分别调用不同Logger def div0(): try: 1/0 except Exception: console_logger.exception("Oops") file_logger.exception("Oops")
总结
- 克隆
LogRecord是最彻底的隔离方式,适合需要完全独立处理的场景 - 针对
exc_text的恢复方法是轻量的局部解决方案 - 独立Logger实例适合业务上需要区分日志流的场景
内容的提问来源于stack exchange,提问作者Zeroji
相关产品推荐
相关产品推荐

