应用的错误

0.3 新版功能.

应用故障,服务器故障。 早晚你会在产品中看见异常。 即使你的代码是 100% 正确的, 你仍然会不时看见异常。 为什么? 因为涉及的所有一切都会出现故障。 这里给出一些 完美正确的代码导致服务器错误的情况:

  • 客户端提前终止请求,而应用依然在读取进来的数据
  • 数据库服务器超载,并无法处理查询
  • 文件系统满了
  • 硬盘损坏
  • 后端服务器超载
  • 你所用的库出现程序错误
  • 服务器的网络连接或其它系统故障

而且这只是你可能面对的问题的简单情形。 那么,我们应该怎么处理这一系列问题? 默认情况下,如果你的应用在以生产模式运行, Flask 会显示一个非常简单的页面并 记录异常到 logger

但是你还可以做些别的,我们会介绍一些更好的设置来应对错误。

错误日志记录工具

发送错误邮件,即使只是关键的邮件,可能会变得压倒性,如果足够的用户击中错误,日志文件通常从来没有看过。这就是为什么我们建议使用Sentry来处理应用程序错误。它可作为GitHub上的开源项目提供,也可作为托管版本提供,您可以免费试用。Sentry聚合重复的错误,捕获完整的堆栈跟踪和本地变量用于调试,并根据新的错误或频率阈值发送邮件。

要使用Sentry,您需要安装raven客户端:

$ pip install raven

然后将此添加到您的Flask应用程序:

from raven.contrib.flask import Sentry
sentry = Sentry(app, dsn='YOUR_DSN_HERE')

或者如果你使用工厂,你也可以在以后init:

from raven.contrib.flask import Sentry
sentry = Sentry(dsn='YOUR_DSN_HERE')

def create_app():
    app = Flask(__name__)
    sentry.init_app(app)
    ...
    return app

YOUR_DSN_HERE值需要替换为您从Sentry安装中获得的DSN值。

之后故障会自动报告给Sentry,从那里您可以收到错误通知。

错误处理器

在错误发生时,你可能想显示自定义的错误页面给用户。这可以通过注册错误处理器来实现。

错误处理器就是普通的即插试图,但是它们不是注册用于路由,而是注册用于完成其它任务时引发的异常。

注册

错误处理器使用errorhandler()或者register_error_handler()注册:

@app.errorhandler(werkzeug.exceptions.BadRequest)
def handle_bad_request(e):
    return 'bad request!'

app.register_error_handler(400, lambda e: 'bad request!')

这两种方式是等效的,但是第一种方式更加清晰,让你有一个函数来调用你的whim(和测试)。请注意,示例中的werkzeug.exceptions.HTTPException子类(如BadRequest)及其HTTP代码在交给注册方法或装饰器(BadRequest)时可以互换。代码 == 400)。

然而,你不会被局限于HTTPException或HTTP状态码,你可以给任何异常类注册一个处理器。

在版本0.11中更改:错误处理程序现在通过其注册的异常类的特异性而不是它们注册的顺序进行优先级排序。

处理

一旦引发异常实例,就遍历其类层次结构,并在注册了处理程序的异常类中进行搜索。选择最具体的处理程序。

例如,如果ConnectionRefusedError被引发,而且有一个处理器注册给ConnectionErrorConnectionRefusedError,更具体的ConnectionRefusedError处理器将被对这个异常实例调用,并显示它的响应给用户。

错误邮件

如果你的应用在生产模式下运行(会在你的服务器上做),默认情况下,你不会看见 任何日志消息。 原因是Flask默认只报告给WSGI错误流或stderr(取决于哪一个可用)。这里最终有时很难找到。通常它在您的Web服务器的日志文件中。

事实上,我现在向你保证,如果你给应用错误配置一个日志文件,你将永远不会去看 它,除非在调试问题时用户向你报告。 你需要的应是异常发生时的邮件,然后你会得 到一个警报,并做些什么。 然后你会得到一个警报,你可以做一些事情。

Flask 使用 Python 内置的日志系统,而且它确实向你发送你可能需要的错误邮件。 这里给出你如何配置 Flask 日志记录器向你发送报告异常的邮件:

ADMINS = ['[email protected]']
if not app.debug:
    import logging
    from logging.handlers import SMTPHandler
    mail_handler = SMTPHandler('127.0.0.1',
                               '[email protected]',
                               ADMINS, 'YourApplication Failed')
    mail_handler.setLevel(logging.ERROR)
    app.logger.addHandler(mail_handler)

那么刚刚发生了什么? 我们创建了一个新的SMTPHandler用来监听127.0.0.1的邮件服务器向所有的ADMINS发送发件人为[email protected],主题为 “YourApplication Failed” 的邮件。 如果你的邮件服务器需要凭证,这些功能也 被提供了。 详情请见SMTPHandler的文档。

我们同样告诉处理程序只发送错误和更重要的消息。 因为我们的确不想收到警告或是 其它没用的,每次请求处理都会发生的日志邮件。

你在生产环境中运行它之前,请参阅 控制日志格式 来向错误邮件中置放更多的 信息。 这将节省你很多弯路。

日志记录到文件

即便你收到邮件,你可能还是想用日志记录警告信息。当调试问题的时候,收集更多的信息是个好主意。Flask 0.11在默认情况下,错误信息会自动记录到网站服务器的日志中。但是警告信息不会。请注意Flask本身在其核心系统不会发出任何警告级别的信息,所以如果发生奇怪的事情,在代码中打印警告信息是你的责任。

在日志系统的方框外提供了一些处理程序,但它们对记录基本错误并不是都有用。 最让人 感兴趣的可能是下面的几个:

  • FileHandler——记录日志到文件系统上的文件中。
  • RotatingFileHandler——记录日志到文件系统的文件中,并在一定数量的消息后rotate。
  • NTEventLogHandler - 将记录到Windows系统的系统事件日志。 如果你在 Windows 上做开发,这就是你想要用的。
  • SysLogHandler - 将日志发送到UNIX系统日志。

当你选择了日志处理程序,像前面对 SMTP 处理程序做的那样,只要确保使用一个低级 的设置(我推荐 WARNING ):

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

控制日志格式

默认情况下,错误处理只会把消息字符串记录到文件或邮件发送给你。 一个日志记 录应存储更多的信息,这使得配置你的日志记录器包含那些信息很重要,如此你会 对错误发生的原因,还有更重要的——错误在哪发生,有更好的了解。

格式可以从一个格式化字符串实例化。 注意回溯会自动附加到日志条目后。你不需要在日志格式的格式化字符串中这么做。

这里有一些配置实例:

邮件

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 ogRecord创建时人类可读的时间格式。 默认情况下,格 式为 "2003-07-08 16:49:45,896" (逗号后的数字 时间的毫秒部分)。 这可以通过对格式化器进行子类化并覆盖formatTime()方法来更改。
%(message)s 记录的消息,视为 msg % args

如果你想深度定制日志格式,你可以继承 Formatter 。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)

调试应用错误

对于生产应用,按照 记录应用错误 中的描述来配置你应用的日志记录和 通知。 这个章节讲述了调试部署配置和深入一个功能强大的 Python 调试器的要点。

有疑问时,手动运行

在配置你的应用到生产环境时时遇到了问题? 如果你拥有主机的 shell 权限,验证你 是否可以在部署环境中手动用 shell 运行你的应用。 确保在同一用户账户下运行配置 好的部署来解决权限问题。 你可以使用 Flask 内置的开发服务器并设置 debug=True , 这在捕获配置问题的时候非常有效,但是 请确保在可控环境下临时地这么做。 不要 在生产环境中使用 debug=True 运行。

调试器操作

为了深入跟踪代码的执行,Flask 提供了一个方框外的调试器(见 调试模式 )。 如果你想用其它的 Python 调试器,请注意相互的调试器接口。 你需要设置下面的参数来 使用你中意的调试器:

  • debug - 是否开启调试模式并捕获异常
  • use_debugger - 是否使用内部的 Flask 调试器
  • use_reloader - 是否在异常时重新载入并创建子进程

debug必须为 True (即异常必须被捕获)来允许其它的两个选项设置为任何值。

如果你使用 Aptana/Eclipse 来调试,你会需要把 use_debuggeruser_reloader 都设置为 False 。

一个可能有用的配置模式就是在你的 config.yaml 中设置为如下(当然,自行更改为适用 你应用的):

FLASK:
    DEBUG: True
    DEBUG_WITH_APTANA: True

然后在你应用的入口( main.py ),你可以写入下面的内容:

if __name__ == "__main__":
    # To allow aptana to receive errors, set use_debugger=False
    app = create_app(config="config.yaml")

    if app.debug: use_debugger = True
    try:
        # Disable Flask's debugger if external debugger is requested
        use_debugger = not(app.config.get('DEBUG_WITH_APTANA'))
    except:
        pass
    app.run(use_debugger=use_debugger, debug=app.debug,
            use_reloader=use_debugger, host='0.0.0.0')