我怎样使用调试信息记录Python错误?

741

我正在使用logging.error将Python异常消息打印到日志文件中:

import logging
try:
    1/0
except ZeroDivisionError as e:
    logging.error(e)  # ERROR:root:division by zero

是否有可能打印关于异常和生成它的代码的更详细信息,而不仅仅是异常字符串?像行号或堆栈跟踪之类的东西会很棒。

17个回答

1124

logger.exception会在错误信息旁边输出堆栈跟踪。

例如:

import logging
try:
    1/0
except ZeroDivisionError:
    logging.exception("message")

输出:

ERROR:root:message
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
ZeroDivisionError: integer division or modulo by zero

@Paulo Cheque 指出,“请注意在Python 3中,您必须仅在except部分内调用logging.exception方法。如果您在任意位置调用此方法,则可能会收到异常结果。文档已经对此进行了警告。”


214
exception 方法只是简单地调用 error(message, exc_info=1)。只要你从异常上下文中向任何一个日志记录方法传递了 exc_info 参数,你就会得到一个回溯信息。 - Helmut Grohne
23
你还可以设置 sys.excepthook(参见这里)来避免必须在所有代码中都加入 try/except 块。 - jul
26
您可以只写except Exception:,因为您并没有以任何方式使用 e ;) - Marco Ferrari
28
在尝试交互式调试代码时,您可能希望检查变量 e。 :) 这就是为什么我总是把它包含在内的原因。 - Vicki Laidler
10
如果我理解有误,请纠正我。在这种情况下,异常没有得到真正的处理,因此在except语句块的末尾添加raise是有意义的。否则,程序将继续运行,就好像一切都很正常。 - Dror
显示剩余8条评论

271

使用exc_info选项可能更好,因为它允许您选择错误级别(如果您使用exception,它将始终处于error级别):

try:
    # do something here
except Exception as e:
    logging.critical(e, exc_info=True)  # log exception info at CRITICAL log level

@CivFan:我实际上没有看过其他的编辑或者文章介绍;那个介绍也是由第三方编辑添加的。我在被删除的评论中没有看到任何有意这么做的地方,但是为了避免审查时出现问题,我可能会撤销我的编辑并删除评论。现在已经过了太长时间,投票肯定是针对编辑后版本的了。 - Martijn Pieters
logging.fatal 是 logging 库中的一个方法吗?我只看到了 critical - Ian
4
@Ian 是 critical 的别名,就像 warnwarning 的别名一样。 - 0xc0de
如果我们使用exc_info,还有没有额外的价值也记录e? - undefined

218

logging.exception 的一个好处是,与SiggyF的回答不同的是,您可以传递任意消息,而日志记录仍将显示所有异常详细信息的完整回溯:

import logging
try:
    1/0
except ZeroDivisionError:
    logging.exception("Deliberate divide by zero traceback")

默认情况下(在最近的版本中),日志记录行为只会将错误打印到sys.stderr,效果如下:

使用最近版本的默认日志行为,默认只将错误信息打印到sys.stderr,效果如下:

>>> import logging
>>> try:
...     1/0
... except ZeroDivisionError:
...     logging.exception("Deliberate divide by zero traceback")
... 
ERROR:root:Deliberate divide by zero traceback
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
ZeroDivisionError: integer division or modulo by zero

异常可以在不提供消息的情况下记录吗? - Stevoisiak
@StevenVascellaro - 我建议你传递 '' 如果你真的不想输入消息... 但是函数至少需要一个参数才能被调用,所以你必须给它一些东西。 - ArtOfWarfare
@ncoghlan,您能否为以下问题提供解决方案:https://stackoverflow.com/questions/68168756/python-how-to-handle-https-connection-refused-error-and-write-the-logs-in-serve - Stark

49

引用

如果您的应用程序以另一种方式记录日志,而不是使用logging模块呢?

现在可以在此处使用traceback

import traceback

def log_traceback(ex, ex_traceback=None):
    if ex_traceback is None:
        ex_traceback = ex.__traceback__
    tb_lines = [ line.rstrip('\n') for line in
                 traceback.format_exception(ex.__class__, ex, ex_traceback)]
    exception_logger.log(tb_lines)
  • Python 2 中使用:

try:
    # your function call is here
except Exception as ex:
    _, _, ex_traceback = sys.exc_info()
    log_traceback(ex, ex_traceback)
  • Python 3 中使用它:

    try:
        x = get_number()
    except Exception as ex:
        log_traceback(ex)
    

  • 你为什么要在log_traceback函数外部放置"_, _, ex_traceback = sys.exc_info()",然后将其作为参数传递?为什么不直接在函数内部使用它? - Basil Musa
    @BasilMusa,简而言之,为了与Python 3兼容,因为ex_traceback是来自Python 3下的ex.__traceback__,但在Python 2下,ex_traceback是来自sys.exc_info() - zangw
    为什么不使用traceback.format_exc()而是使用traceback.format_exception(...) - Alexandre Huat

    22

    您可以在不使用异常的情况下记录堆栈跟踪。

    https://docs.python.org/zh-cn/3/library/logging.html#logging.Logger.debug

    第二个可选关键字参数是stack_info,默认为False。如果设置为True,则将堆栈信息添加到日志消息中,包括实际的日志调用。请注意,这不是指定exc_info时显示的同一堆栈信息:前者是从堆栈底部到当前线程中的日志调用的堆栈帧,而后者是有关已展开的堆栈帧的信息,在搜索异常处理程序时会遵循异常。

    示例:

    >>> import logging
    >>> logging.basicConfig(level=logging.DEBUG)
    >>> logging.getLogger().info('This prints the stack', stack_info=True)
    INFO:root:This prints the stack
    Stack (most recent call last):
      File "<stdin>", line 1, in <module>
    >>>
    

    12
    如果您使用普通的日志记录,所有的日志记录都应该符合这个规则:一条记录 = 一行。遵循这个规则,您可以使用grep和其他工具来处理日志文件。
    但回溯信息是多行的。因此,我的答案是在这个线程中zangw提出解决方案的扩展版本。问题在于回溯行中可能包含\n,因此我们需要额外的工作来摆脱这些行尾。
    import logging
    
    
    logger = logging.getLogger('your_logger_here')
    
    def log_app_error(e: BaseException, level=logging.ERROR) -> None:
        e_traceback = traceback.format_exception(e.__class__, e, e.__traceback__)
        traceback_lines = []
        for line in [line.rstrip('\n') for line in e_traceback]:
            traceback_lines.extend(line.splitlines())
        logger.log(level, traceback_lines.__str__())
    

    在分析日志之后,您可以从日志文件中复制/粘贴所需的回溯行,并执行以下操作:

    ex_traceback = ['line 1', 'line 2', ...]
    for line in ex_traceback:
        print(line)
    

    盈利!


    11

    这个回答是基于以上出色的回答而建立的。

    在大多数应用程序中,您不会直接调用logging.exception(e)。最有可能的是,您已经定义了一个特定于您的应用程序或模块的自定义记录器,类似于以下内容:

    # Set the name of the app or module
    my_logger = logging.getLogger('NEM Sequencer')
    # Set the log level
    my_logger.setLevel(logging.INFO)
    
    # Let's say we want to be fancy and log to a graylog2 log server
    graylog_handler = graypy.GELFHandler('some_server_ip', 12201)
    graylog_handler.setLevel(logging.INFO)
    my_logger.addHandler(graylog_handler)
    
    在这种情况下,只需使用记录器以如下方式调用异常(e)即可:
    try:
        1/0
    except ZeroDivisionError, e:
        my_logger.exception(e)
    

    如果您想要一个专门用于异常的记录器,那么这确实是一个有用的完善。 - logicOnAbstractions

    11

    如果“调试信息”意味着异常引发时存在的值,那么logging.exception(...)将无法帮助您。因此,您需要一种工具,可以自动记录所有变量值以及回溯行。

    开箱即用,您将获得类似以下的日志

    2020-03-30 18:24:31 main ERROR   File "./temp.py", line 13, in get_ratio
    2020-03-30 18:24:31 main ERROR     return height / width
    2020-03-30 18:24:31 main ERROR       height = 300
    2020-03-30 18:24:31 main ERROR       width = 0
    2020-03-30 18:24:31 main ERROR builtins.ZeroDivisionError: division by zero
    

    看看一些 PyPI 工具,我会列举以下:

    它们中的一些将给您提供漂亮的崩溃消息:enter image description here

    但您可能会在 PyPI 上发现更多。


    6

    稍加修饰(受Maybe单子和lifting启发)的一点点处理。可以安全地删除Python 3.6类型注释并使用旧的消息格式化样式。

    fallible.py

    from functools import wraps
    from typing import Callable, TypeVar, Optional
    import logging
    
    
    A = TypeVar('A')
    
    
    def fallible(*exceptions, logger=None) \
            -> Callable[[Callable[..., A]], Callable[..., Optional[A]]]:
        """
        :param exceptions: a list of exceptions to catch
        :param logger: pass a custom logger; None means the default logger, 
                       False disables logging altogether.
        """
        def fwrap(f: Callable[..., A]) -> Callable[..., Optional[A]]:
    
            @wraps(f)
            def wrapped(*args, **kwargs):
                try:
                    return f(*args, **kwargs)
                except exceptions:
                    message = f'called {f} with *args={args} and **kwargs={kwargs}'
                    if logger:
                        logger.exception(message)
                    if logger is None:
                        logging.exception(message)
                    return None
    
            return wrapped
    
        return fwrap
    

    演示:

    In [1] from fallible import fallible
    
    In [2]: @fallible(ArithmeticError)
        ...: def div(a, b):
        ...:     return a / b
        ...: 
        ...: 
    
    In [3]: div(1, 2)
    Out[3]: 0.5
    
    In [4]: res = div(1, 0)
    ERROR:root:called <function div at 0x10d3c6ae8> with *args=(1, 0) and **kwargs={}
    Traceback (most recent call last):
      File "/Users/user/fallible.py", line 17, in wrapped
        return f(*args, **kwargs)
      File "<ipython-input-17-e056bd886b5c>", line 3, in div
        return a / b
    
    In [5]: repr(res)
    'None'
    

    您可以修改此解决方案,以从except部分返回比None更有意义的内容(甚至通过在fallible的参数中指定此返回值来使解决方案通用)。

    2
    在您的日志模块中(如果是自定义模块),只需启用stack_info即可。
    api_logger.exceptionLog("*Input your Custom error message*",stack_info=True)
    

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接