【问题标题】:How do I log a Python error with debug information?如何使用调试信息记录 Python 错误?
【发布时间】:2011-07-08 16:32:21
【问题描述】:

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

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

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

【问题讨论】:

    标签: python exception logging exception-handling


    【解决方案1】:

    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 方法。如果您在任意位置调用此方法,您可能会遇到奇怪的异常。文档警报关于那个。”

    【讨论】:

    • exception 方法只调用error(message, exc_info=1)。只要您将exc_info 从异常上下文传递给任何日志记录方法,您就会得到回溯。
    • 您还可以设置sys.excepthook(参见here)以避免将所有代码包装在try/except中。
    • 你可以写except Exception:,因为你没有以任何方式使用e ;)
    • 在尝试交互式调试代码时,您可能很想检查e。 :) 这就是为什么我总是包含它。
    • 如果我错了,请纠正我,在这种情况下,没有真正处理异常,因此在except 范围的末尾添加raise 是有意义的。否则,将继续运行,就好像一切正​​常。
    【解决方案2】:

    使用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:我实际上并没有看其他编辑或帖子介绍;该介绍也是由第 3 方编辑添加的。我在已删除的 cmets 中看不到任何意图,但我也可以撤消我的编辑并删除 cmets,这里的投票时间太长了,除了编辑版本之外的任何其他内容.
    • logging.fatal 是日志库中的方法吗?我只看到critical
    • @Ian 它是critical 的别名,就像warnwarning 一样。
    【解决方案3】:

    SiggyF's answer 没有显示的关于logging.exception 的一个好处是您可以传入任意消息,并且日志记录仍会显示包含所有异常详细信息的完整回溯:

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

    使用默认(在最近的版本中)将错误打印到 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
    

    【讨论】:

    • 可以在不提供消息的情况下记录异常吗?
    • @StevenVascellaro - 如果您真的不想输入消息,我建议您传递 ''... 但是,如果没有至少一个参数,则无法调用该函数,所以你必须给它一些东西。
    • @ncoghlan 您能否为以下问题提出解决方案:stackoverflow.com/questions/68168756/…
    【解决方案4】:

    Quoting

    如果您的应用程序以其他方式记录日志——不使用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)
      

    【讨论】:

    • 为什么将“_, _, ex_traceback = sys.exc_info()”放在函数log_traceback之外,然后作为参数传递呢?为什么不直接在函数内部使用呢?
    • @BasilMusa,回答你的问题,简而言之,与Python 3兼容,因为ex_traceback来自Python 3下的ex.__traceback__,但ex_traceback来自Python下的sys.exc_info() 2.
    • 为什么不使用traceback.format_exc() 而不是traceback.format_exception(...)
    【解决方案5】:

    您可以毫无例外地记录堆栈跟踪。

    https://docs.python.org/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>
    >>>
    

    【讨论】:

      【解决方案6】:

      如果您使用普通日志 - 您的所有日志记录都应符合以下规则:one record = one line。按照此规则,您可以使用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)
      

      利润!

      【讨论】:

        【解决方案7】:

        这个答案建立在上面的优秀答案之上。

        在大多数应用程序中,您不会直接调用 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)
        

        【讨论】:

        • 如果您想要一个专门用于异常的记录器,这确实是一个有用的收尾工作。
        【解决方案8】:

        一点装饰器处理(非常松散地受到 Maybe monad 和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 的参数中指定此返回值来使解决方案通用)。

        【讨论】:

          【解决方案9】:

          如果“调试信息”表示引发异常时存在的值,那么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 工具,我会命名:

          其中一些会为您提供漂亮的崩溃消息:

          但您可能会在 pypi 上找到更多信息

          【讨论】:

            【解决方案10】:

            在您的日志模块(如果是自定义模块)中启用 stack_info。

            api_logger.exceptionLog("*Input your Custom error message*",stack_info=True)
            

            【讨论】:

              【解决方案11】:

              如果您查看this code example(适用于 Python 2 和 3),您会看到下面的函数定义可以提取

              • 方法
              • 行号
              • 代码上下文
              • 文件路径

              对于整个堆栈跟踪,是否存在异常:

              def sentry_friendly_trace(get_last_exception=True):
                  try:
                      current_call = list(map(frame_trans, traceback.extract_stack()))
                      alert_frame = current_call[-4]
                      before_call = current_call[:-4]
              
                      err_type, err, tb = sys.exc_info() if get_last_exception else (None, None, None)
                      after_call = [alert_frame] if err_type is None else extract_all_sentry_frames_from_exception(tb)
              
                      return before_call + after_call, err, alert_frame
                  except:
                      return None, None, None
              

              当然,这个函数取决于上面链接的整个要点,尤其是extract_all_sentry_frames_from_exception()frame_trans(),但异常信息提取总计不到 60 行。

              希望有帮助!

              【讨论】:

                【解决方案12】:

                我将所有功能都包装在我的定制设计的记录器周围:

                import json
                import timeit
                import traceback
                import sys
                import unidecode
                
                def main_writer(f,argument):
                  try:
                    f.write(str(argument))
                  except UnicodeEncodeError:
                    f.write(unidecode.unidecode(argument))
                
                
                def logger(*argv,logfile="log.txt",singleLine = False):
                  """
                  Writes Logs to LogFile
                  """
                  with open(logfile, 'a+') as f:
                    for arg in argv:
                      if arg == "{}":
                        continue
                      if type(arg) == dict and len(arg)!=0:
                        json_object = json.dumps(arg, indent=4, default=str)
                        f.write(str(json_object))
                        f.flush()
                        """
                        for key,val in arg.items():
                          f.write(str(key) + " : "+ str(val))
                          f.flush()
                        """
                      elif type(arg) == list and len(arg)!=0:
                        for each in arg:
                          main_writer(f,each)
                          f.write("\n")
                          f.flush()
                      else:
                        main_writer(f,arg)
                        f.flush()
                      if singleLine==False:
                        f.write("\n")
                    if singleLine==True:
                      f.write("\n")
                
                def tryFunc(func, func_name=None, *args, **kwargs):
                  """
                  Time for Successfull Runs
                  Exception Traceback for Unsuccessful Runs
                  """
                  stack = traceback.extract_stack()
                  filename, codeline, funcName, text = stack[-2]
                  func_name = func.__name__ if func_name is None else func_name # sys._getframe().f_code.co_name # func.__name__
                  start = timeit.default_timer()
                  x = None
                  try:
                    x = func(*args, **kwargs)
                    stop = timeit.default_timer()
                    # logger("Time to Run {} : {}".format(func_name, stop - start))
                  except Exception as e:
                    logger("Exception Occurred for {} :".format(func_name))
                    logger("Basic Error Info :",e)
                    logger("Full Error TraceBack :")
                    # logger(e.message, e.args)
                    logger(traceback.format_exc())
                  return x
                
                def bad_func():
                  return 'a'+ 7
                
                if __name__ == '__main__':
                    logger(234)
                    logger([1,2,3])
                    logger(['a','b','c'])
                    logger({'a':7,'b':8,'c':9})
                    tryFunc(bad_func)
                

                【讨论】:

                  【解决方案13】:

                  如果您可以处理额外的依赖关系,那么使用twisted.log,您不必显式记录错误,它还会将整个回溯和时间返回到文件或流。

                  【讨论】:

                  • 也许twisted 是一个很好的建议,但这个答案并没有多大贡献。它没有说明如何使用twisted.log,也没有说明它比标准库中的logging 模块有什么优势,也没有解释“您不必显式记录错误”的含义.
                  【解决方案14】:

                  一种简洁的方法是使用format_exc(),然后解析输出以获取相关部分:

                  from traceback import format_exc
                  
                  try:
                      1/0
                  except Exception:
                      print 'the relevant part is: '+format_exc().split('\n')[-2]
                  

                  问候

                  【讨论】:

                  • 嗯?为什么那是“相关部分”.split('\n')[-2] 所做的只是扔掉行号和来自format_exc() 的结果的回溯 - 您通常想要的有用信息!更何况,它连那个都做不好;如果您的异常消息包含换行符,那么此方法将仅打印异常消息的最后一行 - 这意味着您将丢失异常类和大部分异常消息,并丢失回溯。 -1.
                  猜你喜欢
                  • 2011-11-23
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 2011-08-11
                  • 2016-07-28
                  • 1970-01-01
                  • 2011-11-10
                  相关资源
                  最近更新 更多