Marvin's Blog【程式人生】

Ability will never catch up with the demand for it

16 Jun 2020

Flask文档阅读笔记(三)

Flask 文档1.1.x 走读笔记。

Application Errors

程序出错是一个必然性事件:

  • 用户可能中止请求,而服务端正在读取上行数据
  • 服务端过载而不能处理更多请求
  • 文件系统满了
  • 硬盘崩溃了
  • 其他后端服务器过载
  • 用到的第三方库出错
  • 服务器请求第三方资源的时候网络出错

上面列举的只是你所要面对的异常中的一小部分。如果在产生环境中出现异常,Flask会显示一个非常简单的页面,并且用logger记录异常。但是你可以做的不止这些

Error Logging Tools¶

Sentry 是一个服务,专门为你监视应用的异常。Sentry可以集合错误信息,保存完整的堆栈以及本地变量以便调试。其开源版本在https://github.com/getsentry/sentry

Error handlers¶

你可以针对不同的HTTP状态码注册不同的错误处理函数:

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

# or, without the decorator
app.register_error_handler(400, handle_bad_request)

werkzeug.exceptions.HTTPException的子类,如BadRequest,在注册错误处理函数时,跟http状态码对等,比如BadRequest.code == 400。

非标准的http状态码无法直接注册,因为不被Werkzeug认识。所以要定义一个跟这个状态码对应的HTTPException:

class InsufficientStorage(werkzeug.exceptions.HTTPException):
    code = 507
    description = 'Not enough storage space.'

app.register_error_handler(InsufficientStorage, handle_507)

raise InsufficientStorage()

当Flask第一次捕获一个异常时,先看其状态码,如果状态码没有注册直接的处理函数,那么异常的类型层级中最下层的处理函数会被调用。如果也没有这样的处理函数,那么可能Flask就是简单显示一个500 Internal Server Error。

也就是说,如果ConnetionRefusedError被抛出,然后系统注册有ConnectionErro以及ConnectionRefusedError。后者的处理函数会被调用。

Blueprint的异常处理函数会先于全局异常处理函数被调用。但是Blueprint无法处理404错误,因为这发生在路由发生之前,所以还没有进入blueprint的管辖范围。

如果直接在HTTPException或者Exception级别注册异常处理函数,那么收到的异常可能会超过你想象。比如HTTPException处理函数可能会遇到404或者405错误,这些都不是你的代码生成的。

下面的这个例子在Exception级别捕获所有异常,但是HTTPException会被放行,因为HTTPException可以作为合适的应答返回给客户端:

from werkzeug.exceptions import HTTPException

@app.errorhandler(Exception)
def handle_exception(e):
    # pass through HTTP errors
    if isinstance(e, HTTPException):
        return e

    # now you're handling non-HTTP exceptions only
    return render_template("500_generic.html", e=e), 500

注意,如果同时注册了Exception和HTTPException处理函数,那么Exception处理函数不会处理HTTPException,因为后者更具体,所以优先被选中。

前面提到,默认情况下,未捕获的异常会被Flask当作500 Internal Server Error处理,参考flask.Flask.handle_exception()。

也可以为InternalServerError注册一个异常处理器,这个异常处理器会被传一个InternalServerError的实例,然后其original_exception属性带有原异常信息。

original_exception属性在早期的Werkzeug版本中只存在云未捕获异常的情况下,所以要注意判断其是否为None:

@app.errorhandler(InternalServerError)
def handle_500(e):
    original = getattr(e, "original_exception", None)

    if original is None:
        # direct 500 error, such as abort(500)
        return render_template("500.html"), 500

    # wrapped unhandled error
    return render_template("500_unhandled.html", e=original), 500

Debugging Application Errors

如果出问题了,首先尝试的办法是看是否能够在命令行重现。也就是在命令行的开发模式下重新启动应用。主要,要和出错的情况使用同一个用户账户,避免权限问题。Flask内建服务器的debug=True选项可以开启调试模式。

如果想单步跟踪,那么Flask提供了一个调试器。如果想要其他的调试器,那么要设置一些选项:

  • debug,必须设为true才能开始调试
  • use_debugger,是否使用内建调试器
  • use_reloader,如果资源发生改变,是否重新加载

假如你使用Aptana/Eclispe来调试,那么use_debugger和use_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")

    use_debugger = app.debug and not(app.config.get('DEBUG_WITH_APTANA'))
    app.run(use_debugger=use_debugger, debug=app.debug,
            use_reloader=use_debugger, host='0.0.0.0')

Logging

Flask使用Python的logging模块。Flask的app.logger可以用来记录消息:

@app.route('/login', methods=['POST'])
def login():
    user = get_user(request.form['username'])

    if user.check_password(request.form['password']):
        login_user(user)
        app.logger.info('%s logged in successfully', user.username)
        return redirect(url_for('index'))
    else:
        app.logger.info('%s failed to log in', user.username)
        abort(401)

app.logger必须再app启动时就配置好。在配置好之前app.logger采用的是logging模块默认的行为。如果配置发生在访问了app.logger之后,那么你需要移除默认的处理函数:

from flask.logging import default_handler

app.logger.removeHandler(default_handler)

下面的例子使用dictConfig来创建一个和Flask默认配置相似的配置:

from logging.config import dictConfig

dictConfig({
    'version': 1,
    'formatters': {'default': {
        'format': '[%(asctime)s] %(levelname)s in %(module)s: %(message)s',
    }},
    'handlers': {'wsgi': {
        'class': 'logging.StreamHandler',
        'stream': 'ext://flask.logging.wsgi_errors_stream',
        'formatter': 'default'
    }},
    'root': {
        'level': 'INFO',
        'handlers': ['wsgi']
    }
})

app = Flask(__name__)

默认情况下,Flask会配置一个StreamHandler到app.logger,在处理请求的时候,它会把log记录到WSGI服务器的环境变量environ['wsgi.errors']中(默认是sys.stderr)。如果不是处理请求,那么默认就是sys.stderr。

Flask可以把错误信息通过邮件发送,需要配置logging.handlers.SMTPHandler :

import logging
from logging.handlers import SMTPHandler

mail_handler = SMTPHandler(
    mailhost='127.0.0.1',
    fromaddr='server-error@example.com',
    toaddrs=['admin@example.com'],
    subject='Application Error'
)
mail_handler.setLevel(logging.ERROR)
mail_handler.setFormatter(logging.Formatter(
    '[%(asctime)s] %(levelname)s in %(module)s: %(message)s'
))

if not app.debug:
    app.logger.addHandler(mail_handler)

如果想记录更多request相关的信息,可以派生logging.Formatter,添加所需字段:

from flask import has_request_context, request
from flask.logging import default_handler

class RequestFormatter(logging.Formatter):
    def format(self, record):
        if has_request_context():
            record.url = request.url
            record.remote_addr = request.remote_addr
        else:
            record.url = None
            record.remote_addr = None

        return super().format(record)

formatter = RequestFormatter(
    '[%(asctime)s] %(remote_addr)s requested %(url)s\n'
    '%(levelname)s in %(module)s: %(message)s'
)
default_handler.setFormatter(formatter)
mail_handler.setFormatter(formatter)

其他库也可能产生log,如果需要查看其他库产生的log,可以使用logging的root logger,并把处理器添加到root logger:

from flask.logging import default_handler

root = logging.getLogger()
root.addHandler(default_handler)
root.addHandler(mail_handler)

根据你的项目,你可能想配置单独的logger,而不是统一使用root logger:

for logger in (
    app.logger,
    logging.getLogger('sqlalchemy'),
    logging.getLogger('other_package'),
):
    logger.addHandler(default_handler)
    logger.addHandler(mail_handler)

Werkzeug 所使用的logger名为’werkzeug’,如果根logger没有配置相应的处理器,那么Werzeug会自己添加一个StreamHandler。

对于Flask扩展,他们会选择使用app.logger或者自定义的logger。

Configuration Handling¶

Flask要求配置在启动app前提供。app.config用来保存Flask自身的,以及应用提供的配置。app.config是一个字典的派生类:

app = Flask(__name__)
app.config['TESTING'] = True

有一些配置也会反映到app的属性上:

app.testing = True

使用dict.update()可以更新多项配置:

app.config.update(
    TESTING=True,
    SECRET_KEY=b'_5#y2L"F4Q8z\n\xec]/'
)

Environment and Debug Features

ENV和DEBUG两个项目用来控制app的所属环境和调试行为。ENV默认受FLASK_ENV 环境变量影响,默认值为production。在app启动前将FLASK_ENV设为development 可以进入开发环境,并开启调试功能。 FLASK_DEBUG是一个单独的开发,用来控制调试选项,不管当前是何环境。

Builtin Configuration Values

本章节对配置项进行了列举。

Configuring from Files

Flask可以从文件读取配置:

app = Flask(__name__)
# 从yourapplication.default_settings模块读入配置
app.config.from_object('yourapplication.default_settings')
# 从YOURAPPLICATION_SETTINGS所指的python文件读入配置
app.config.from_envvar('YOURAPPLICATION_SETTINGS')

下面是配置文件的例子:

# Example configuration
DEBUG = False
SECRET_KEY = b'_5#y2L"F4Q8z\n\xec]/'

Configuring from Environment Variables

一些配置项受环境变量影响,可以直接在环境变量中设置:

$ export SECRET_KEY='5f352379324c22463451387a0aec5d2f'
$ export MAIL_ENABLED=false
$ python run-app.py
 * Running on http://127.0.0.1:5000/

Configuration Best Practices

  • 把创建app和注册blueprint的工作放在函数内。这样可以支持多个应用的实例,并且容易达成不同的应用实例有不同的配置。
  • 避免模块导入过程中对配置的依赖,可以在模块导入后重新配置

一个好的做法是提供有意义的基础配置,这样不同环境下只需要对基本配置做一些小修改。

另一个有意思的做法是通过类继承来来达成配置的自定义:

class Config(object):
    DEBUG = False
    TESTING = False
    DATABASE_URI = 'sqlite:///:memory:'

class ProductionConfig(Config):
    DATABASE_URI = 'mysql://user@localhost/foo'

class DevelopmentConfig(Config):
    DEBUG = True

class TestingConfig(Config):
    TESTING = True

可以使用from_object()来选择配置:

app.config.from_object('configmodule.ProductionConfig')

需要注意的是from_object()并不实例化指定的对象类。此外,还支持在对象类中使用 @property:

class Config(object):
    """Base config, uses staging database server."""
    DEBUG = False
    TESTING = False
    DB_SERVER = '192.168.1.56'

    @property
    def DATABASE_URI(self):         # Note: all caps
        return 'mysql://user@{}/foo'.format(self.DB_SERVER)

class ProductionConfig(Config):
    """Uses production database server."""
    DB_SERVER = '192.168.19.32'

class DevelopmentConfig(Config):
    DB_SERVER = 'localhost'
    DEBUG = True

class TestingConfig(Config):
    DB_SERVER = 'localhost'
    DEBUG = True
    DATABASE_URI = 'sqlite:///:memory:'

一些小建议:

  • 创建默认的配置文件,并放置在版本管理中
  • 使用环境变量来切换配置
  • 在生产代码中使用fabric ,这样可以把代码和配置分开配送。

Instance Folders

长久以来,Flask支持通过Flask.root_path来访问源码根目录。Flask0.8引入了Flask.instance_path.,支持使用instance folder。instance folder即不需要再源代码目录,不受版本管理,此外可以根据部属的情况进行调整,适合用来保存运行时数据,或者配置文件。

如果想自定义Flask默认的instance folder,可以这么做:

# 必须提供绝对路径
app = Flask(__name__, instance_path='/path/to/instance/folder')

默认情况下instance folder的位置:

未安装模块

/myapp.py
/instance

** 未安装的包**

/myapp
    /__init__.py
/instance

安装的包或者模块

$PREFIX/lib/python2.X/site-packages/myapp
$PREFIX/var/myapp-instance

$PREFIX/和 sys.prefix的值一致。

前面提到,instance_folder可以用来存放配置,如果想加载其中的配置,可以:

app = Flask(__name__, instance_relative_config=True)
# 从模块中读取配置
app.config.from_object('yourapplication.default_settings')
# 从instance folder中读取配置,并覆盖模块中读取的配置,如果存在的话
app.config.from_pyfile('application.cfg', silent=True)

Flask提供一个Flask.open_instance_resource(),可以方便读取instance folder中的文件:

filename = os.path.join(app.instance_path, 'application.cfg')
with open(filename) as f:
    config = f.read()

# or via open_instance_resource:
with app.open_instance_resource('application.cfg') as f:
    config = f.read()

(本篇完)

Categories

Tags