Python程序退出时如何控制未捕获异常的日志记录方式?
Python日志JSON格式化与异常退出的正确处理
我编写了如下Python程序,希望所有日志以JSON格式输出(方便CloudWatch检索),异常回溯信息放在"exception"字段中;同时程序发生异常时要以错误状态退出,且回溯信息不会拆分为多事件:
import logging if __name__ == "__main__": # 配置日志记录器以JSON格式写入日志,并将异常回溯信息放在"exception"字段中 logger = logging.getLogger(__name__) logger.info("worker started") try: # 执行一些工作任务 logger.info("worker finished successfully") except Exception: logger.exception("unknown error occurred") # 此处该如何处理?
我尝试了三种方案,但都有问题:
- 方案一:仅记录日志,程序正常退出(不符合错误退出要求)
except Exception: logger.exception("unknown error occurred") - 方案二:记录日志后
raise,程序错误退出,但会额外输出未格式化的异常(ECS中显示为多行,不符合JSON格式要求)except Exception: logger.exception("unknown error occurred") raise - 方案三:记录日志后用
sys.exit(1),程序错误退出,但调试时无法直接定位到异常发生位置(跳转到sys.exit行而非错误源),且sys.exit的使用不被推荐except Exception: logger.exception("unknown error occurred") sys.exit(1)
有没有一种正确的方式,既能让程序因真实异常直接退出,又仅通过已配置的日志记录器按预期记录回溯信息?
正确解决方案:用全局异常钩子处理未捕获异常
核心思路是别在顶层捕获所有Exception,而是通过sys.excepthook接管未捕获异常的处理逻辑——这样既保证异常用你配置的JSON格式记录,程序也会因真实异常退出,调试时还能直接定位到错误发生的位置。
具体步骤:
- 配置自定义JSON日志格式化器,确保能把异常回溯信息塞进"exception"字段
- 设置
sys.excepthook,让所有未捕获的异常都通过日志记录器输出,代替Python默认的stderr打印 - 删掉顶层的try-except块,让异常自然冒泡到全局钩子处理
示例代码:
import logging import sys from pythonjsonlogger import jsonlogger # 也可以自己写自定义Formatter,用第三方库更省心 def setup_logging(): logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) # 配置JSON格式,包含异常字段 formatter = jsonlogger.JsonFormatter( "%(asctime)s %(levelname)s %(message)s %(exception)s" ) handler = logging.StreamHandler() handler.setFormatter(formatter) logger.addHandler(handler) # 关掉日志传播,避免默认处理器重复输出 logger.propagate = False return logger def handle_uncaught_exception(exc_type, exc_value, exc_traceback): # 键盘中断(Ctrl+C)按默认逻辑处理 if issubclass(exc_type, KeyboardInterrupt): sys.__excepthook__(exc_type, exc_value, exc_traceback) return logger = logging.getLogger(__name__) # 用CRITICAL级别记录异常,传入exc_info拿到完整回溯 logger.critical("uncaught exception occurred", exc_info=(exc_type, exc_value, exc_traceback)) # 把全局异常钩子换成我们自己的 sys.excepthook = handle_uncaught_exception if __name__ == "__main__": logger = setup_logging() logger.info("worker started") # 执行工作任务,不用包try-except # 模拟一个异常 1 / 0 logger.info("worker finished successfully")
这个方案的优势:
- 所有日志都是统一的JSON格式,异常回溯老老实实待在"exception"字段里,CloudWatch里不会拆成多条事件
- 程序因真实异常退出,调试的时候直接跳转到出错的那一行,保留原生调试体验
- 不会有重复日志,所有输出都走你配置的日志记录器
- 避开了
sys.exit带来的调试定位问题
若必须捕获特定异常:
要是你得抓部分异常做额外操作,记录日志后直接raise就行——因为全局钩子已经接管了异常输出,不会再冒出未格式化的内容:
try: # 执行某个特定操作 some_operation() except SpecificError as e: logger.error(f"特定错误发生: {e}", exc_info=True) raise # 重新抛出,让全局钩子处理
内容的提问来源于stack exchange,提问作者Rob Allsopp
相关产品推荐
相关产品推荐

