在Python中记录带回溯的异常
我该如何记录我的Python异常信息呢?
try:
do_something()
except:
# How can I log my exception here, complete with its traceback?
13 个回答
最近,我的工作要求我记录我们应用程序中的所有错误信息和异常。我尝试了很多网上其他人分享的方法,比如上面提到的,但最后我选择了一种不同的方式,就是重写 traceback.print_exception
。
我在 http://www.bbarrows.com/ 上写了一篇文章,内容更容易理解,不过我也会在这里粘贴一些内容。
当我被要求记录我们软件在实际运行中可能遇到的所有异常时,我尝试了多种方法来记录 Python 的异常信息。起初,我认为 Python 的系统异常处理钩子 sys.excepthook
是插入日志代码的理想位置。我尝试了类似下面的代码:
import traceback
import StringIO
import logging
import os, sys
def my_excepthook(excType, excValue, traceback, logger=logger):
logger.error("Logging an uncaught exception",
exc_info=(excType, excValue, traceback))
sys.excepthook = my_excepthook
这个方法在主线程中有效,但我很快发现我的 sys.excepthook
在我进程启动的新线程中并不存在。这是个大问题,因为在这个项目中,大部分操作都是在线程中进行的。
经过搜索和阅读大量文档后,我发现最有用的信息来自 Python 的问题跟踪器。
这个讨论串的第一篇帖子展示了 sys.excepthook
在不同线程中不持久化的工作示例(如下所示)。显然,这是预期的行为。
import sys, threading
def log_exception(*args):
print 'got exception %s' % (args,)
sys.excepthook = log_exception
def foo():
a = 1 / 0
threading.Thread(target=foo).start()
在这个 Python 问题讨论串中,大家提出了两种解决方法。要么是子类化 Thread
,并在我们自己的 try-except 块中包裹 run 方法,以便捕获和记录异常;要么是对 threading.Thread.run
进行猴子补丁,使其在自己的 try-except 块中运行并记录异常。
第一种方法,即子类化 Thread
,在我看来代码不够优雅,因为你必须在每个需要记录线程的地方都导入并使用你自定义的 Thread
类。这让我很麻烦,因为我得在整个代码库中搜索并替换所有普通的 Threads
为这个自定义的 Thread
。不过,这样的 Thread
功能很明确,如果自定义日志代码出现问题,别人也更容易诊断和调试。自定义的日志线程可能长这样:
class TracebackLoggingThread(threading.Thread):
def run(self):
try:
super(TracebackLoggingThread, self).run()
except (KeyboardInterrupt, SystemExit):
raise
except Exception, e:
logger = logging.getLogger('')
logger.exception("Logging an uncaught exception")
第二种方法,即对 threading.Thread.run
进行猴子补丁,很不错,因为我只需在 __main__
后面运行一次,就能在所有异常中插入我的日志代码。不过,猴子补丁在调试时可能会很麻烦,因为它改变了某些东西的预期功能。Python 问题跟踪器建议的补丁是:
def installThreadExcepthook():
"""
Workaround for sys.excepthook thread bug
From
http://spyced.blogspot.com/2007/06/workaround-for-sysexcepthook-bug.html
(https://sourceforge.net/tracker/?func=detail&atid=105470&aid=1230540&group_id=5470).
Call once from __main__ before creating any threads.
If using psyco, call psyco.cannotcompile(threading.Thread.run)
since this replaces a new-style class method.
"""
init_old = threading.Thread.__init__
def init(self, *args, **kwargs):
init_old(self, *args, **kwargs)
run_old = self.run
def run_with_except_hook(*args, **kw):
try:
run_old(*args, **kw)
except (KeyboardInterrupt, SystemExit):
raise
except:
sys.excepthook(*sys.exc_info())
self.run = run_with_except_hook
threading.Thread.__init__ = init
直到我开始测试我的异常日志记录时,我才意识到我走错了方向。
为了测试,我在代码的某个地方放了一个
raise Exception("Test")
但是,调用这个方法的地方被一个 try-except 块包裹着,这个块打印了异常信息但吞掉了异常。这让我很沮丧,因为我看到异常信息被打印到标准输出,但没有被记录下来。于是我决定,记录异常信息的一个更简单的方法就是对所有 Python 代码用来打印异常信息的方法 traceback.print_exception
进行猴子补丁。
最终,我得到了类似下面的代码:
def add_custom_print_exception():
old_print_exception = traceback.print_exception
def custom_print_exception(etype, value, tb, limit=None, file=None):
tb_output = StringIO.StringIO()
traceback.print_tb(tb, limit, tb_output)
logger = logging.getLogger('customLogger')
logger.error(tb_output.getvalue())
tb_output.close()
old_print_exception(etype, value, tb, limit=None, file=None)
traceback.print_exception = custom_print_exception
这段代码将异常信息写入一个字符串缓冲区,并将其记录为错误信息。我设置了一个自定义的日志处理器 'customLogger',它会处理错误级别的日志并发送回家进行分析。
使用 exc_info
选项可能会更好,这样可以保留警告或错误的标题:
try:
# coode in here
except Exception as e:
logging.error(e, exc_info=True)
在except:
处理块中使用logging.exception
,可以记录当前发生的异常以及相关的追踪信息,并在前面加上一条消息。
import logging
LOG_FILENAME = '/tmp/logging_example.out'
logging.basicConfig(filename=LOG_FILENAME, level=logging.DEBUG)
logging.debug('This message should go to the log file')
try:
run_my_stuff()
except:
logging.exception('Got exception on main handler')
raise
现在查看日志文件/tmp/logging_example.out
:
DEBUG:root:This message should go to the log file
ERROR:root:Got exception on main handler
Traceback (most recent call last):
File "/tmp/teste.py", line 9, in <module>
run_my_stuff()
NameError: name 'run_my_stuff' is not defined