在Python中记录带回溯的异常

239 投票
13 回答
246675 浏览
提问于 2025-04-15 14:44

我该如何记录我的Python异常信息呢?

try:
    do_something()
except:
    # How can I log my exception here, complete with its traceback?

13 个回答

79

最近,我的工作要求我记录我们应用程序中的所有错误信息和异常。我尝试了很多网上其他人分享的方法,比如上面提到的,但最后我选择了一种不同的方式,就是重写 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',它会处理错误级别的日志并发送回家进行分析。

240

使用 exc_info 选项可能会更好,这样可以保留警告或错误的标题:

try:
    # coode in here
except Exception as e:
    logging.error(e, exc_info=True)
330

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

撰写回答