处理应用异常

译者:plucury#gmail.com

New in version 0.3.

应用程序处理失败,服务器处理失败。在你的产品中这些异常迟早会暴露出来,即使你的代码是完全正确的, 你仍然会一次次的面对这些异常。原因?因为所有的一切都有可能失败。在以下的几种情况中,完美的代码 却导致了服务器的错误:

  • 当应用系统正在读取传入的数据时,客户端过早的结束了请求。
  • 数据库超过负荷,无法处理查询请求。
  • 文件系统没有空间了。
  • 硬盘挂了。
  • 终端服务器超过负荷。
  • 你所使用的代码库中存在编程错误。
  • 服务器与其他系统的网络连接中断了。

而这只是你所要面对的问题中一些最简单的例子。那我们将如何来解决这些问题呢?在默认的情况下,你的 应用程序在生产模式下运行,Flask将显示一个十分简单的页面并记录这些异常通过使用 logger .

但是你可以做得更多,并且我们将会讨论几种更好的方案来处理这些异常。

报错邮件

如果应用程序以生产模式运行(通常在服务器上你会这么做),在默认情况下你不会看见任何的日志信息。 这是为什么呢?因为Flask是一个零配置框架,而如果没有配置的话,框架又应该把日志文件放到哪里去 呢?依靠假设并不是一个很好的方法,因为总是会存在各种不同的可能,也许那个我们假设放置日志的地方 用户并没有权限访问。另外,对于大多数小型的应用程序来说也不会有人去关注他的日志。

实际上,我可以向你保证即使你为你的程序配置了放置错误信息的日志文件,你也永远不会去查看他,除非 当你的用户向你报告了一个事件而你需要去排查错误的时候。你所需要的只是,当异常第二次发生时接收到 一封报警邮件,然后你在针对其中的情况进行处理。

Flask使用了python内置的日志系统,并且他会在你需要是向你发生关于异常的邮件。这里是一个关于如何 配置Flask的日志以向你发送异常邮件的例子:

ADMINS = ['yourname@example.com']
if not app.debug:
    import logging
    from logging.handlers import SMTPHandler
    mail_handler = SMTPHandler('127.0.0.1',
                               'server-error@example.com',
                               ADMINS, 'YourApplication Failed')
    mail_handler.setLevel(logging.ERROR)
    app.logger.addHandler(mail_handler)

这是如何操作的呢?我们创建了一个新的类 SMTPHandler ,他 将通过 127.0.0.1 的邮件服务器向所有的 ADMINS 用户发送标题为“YourApplication Failed” 邮件,并且将发件地址配置为 server-error@example.com 。此外,我们还提供了对 需要证书的邮件服务器的支持,关于这部分的文档,请查看 SMTPHandler

邮件处理器只会发送异常和错误的信息,因为我们并不希望通过邮件获取警告信息或其他一些处理过程中 产生的没有用的日志。

当你在产品中使用它们的时候,请务必查看 日志格式 以使得报错邮件中包含更多的信息。这 些信息将为你解决很多的烦恼。

日志文件

即使你已经有了报错邮件,你可能仍然希望能够查看到警告信息。为了排查问题,尽可能的保存更多的 信息不失为一个好主意。请注意,Flask的系统核心本身并不会去记录任何警告信息,因此编写记录那 些看起来不对劲的地方的代码将是你的责任。

这里提供了几个处理类,但对于基本的记录错误日志而言他们并不是总是那么的有用。而其中最值得我们 注意的是以下几项:

  • FileHandler - 将日志信息写入文件系统中
  • RotatingFileHandler - 将日志信息写入文件系统中,并且 当日志达到一定数量时会滚动记录最新的信息。
  • NTEventLogHandler - 将日志发送到windows系统的日 志事件中。如果你的系统部署在windows环境中,那么这正是你想要的。
  • SysLogHandler - 将日志发送到UNIX的系统日志中。

一旦你选择了你的日志处理类,你就可以向上文中配置SMTP处理类一样的来配置它们,唯一需要注意的 是使用更低级别的设置(我这里使用的是 WARNING ):

if not app.debug:
    import logging
    from logging.handlers import TheHandlerYouWant
    file_handler = TheHandlerYouWant(...)
    file_handler.setLevel(logging.WARNING)
    app.logger.addHandler(file_handler)

日志格式

在默认情况下,处理器只会将日志信息写入文件或是用邮件发送给你。而日志应该记录更多的信息,你必须 配置你的日志,使它能够让你更方便的知道发生了什么样的错误,以及更重要的是告诉你哪里发生了错误。

格式处理器(formatter)可以让你获取格式化的字符串。你需要知道是日志的连接是自动进行的,你不需要 将它包含在格式处理器的格式化字符串中。

这里有几个例子:

电子邮件

from logging import Formatter
mail_handler.setFormatter(Formatter('''
Message type:       %(levelname)s
Location:           %(pathname)s:%(lineno)d
Module:             %(module)s
Function:           %(funcName)s
Time:               %(asctime)s

Message:

%(message)s
'''))

日志文件

from logging import Formatter
file_handler.setFormatter(Formatter(
    '%(asctime)s %(levelname)s: %(message)s '
    '[in %(pathname)s:%(lineno)d]'
))

复杂的日志格式

这里是一系列用户格式化字符串的变量。这里的列表并不完整,你可以通过查看官方文档的 logging 部分来获取完整的列表。

格式 描述
%(levelname)s 日志等级。 ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL').
%(pathname)s 调用日志的源文件的全路径(如果可用)
%(filename)s 文件名。
%(module)s 模块名。
%(funcName)s 方法名。
%(lineno)d 调用日志的代码所在源文件中的行号。(如果可用)
%(asctime)s 创建日志的可阅读时间。默认的格式是 "2003-07-08 16:49:45,896" (逗号后的 时间是毫秒)。可以通过复写 formatTime() 方法来修改它
%(message)s 日志信息。同 msg % args

如果你需要更多的定制化格式,你可以实现格式处理器(formatter)的子类。它有以下三个有趣的方法:

format():
处理实际的格式。它需要接收一个 LogRecord 对象,并返回一个被 格式话的字符串。
formatTime():
调用 asctime 进行格式化。如果你需要不同的时间格式,可以复写这个方法。
formatException()
调用异常格式化。它接收一个 exc_info 元组并返回一个字符串。通常它会很好 的运行,你并不需要复写它。

获取更多的信息,请查看官方文档。

其他代码库

目前为止,我们只配置了你的程序自身的日志。而其他的代码库同样可以需要记录日志。比如,SQLAlchemy 使用了很多日志。使用 logging 包可以一次性的配置所有的日志,当我并不推荐那样做。因为当 多个程序在同一个Python解释器上运行是,你将无法单独的对他们进行配置。

相对的,我推荐你只对你所关注的日志进行配置,通过 getLogger() 方法获取 所有的日志处理器,并通过迭代获取他们:

from logging import getLogger
loggers = [app.logger, getLogger('sqlalchemy'),
           getLogger('otherlibrary')]
for logger in loggers:
    logger.addHandler(mail_handler)
    logger.addHandler(file_handler)