Как написать отличное сообщение журнала отладки Flask в файл в процессе производства?

У меня есть приложение Flask, которое работает хорошо и время от времени выдает ошибку, которая видна при запуске с debug=True:

if __name__ == '__main__':
    app.run(debug=True)

Я получаю полезные сообщения об ошибках, такие как:

Traceback (most recent call last):
  File "./main.py", line 871, in index_route

KeyError: 'stateIIIII'

Я хотел бы, чтобы сообщения об ошибках, подобные этим, сохранялись в файл, когда я запускаю приложение в рабочей среде (используя Lighttpd + fastcgi).

Изучив различные вопросы StackOverflow (http://flask.pocoo.org/docs/errorhandling/, http://docs.python.org/2/library/logging.html и др.); список рассылки Flask; и несколько блогов, кажется, нет простого способа просто отправить все замечательные сообщения об ошибках в файл - мне нужно использовать модуль ведения журнала Python для настройки вещей. Поэтому я придумал следующий код.

В верхней части моего файла приложения у меня есть различные импорты, за которыми следуют:

app = Flask(__name__)

if app.debug is not True:   
    import logging
    from logging.handlers import RotatingFileHandler
    file_handler = RotatingFileHandler('python.log', maxBytes=1024 * 1024 * 100, backupCount=20)
    file_handler.setLevel(logging.ERROR)
    app.logger.setLevel(logging.ERROR)
    app.logger.addHandler(file_handler)

Затем я поместил код для каждого маршрута в оператор try/except и использовал трассировку, чтобы выяснить, из какой строки возникла ошибка, и вывести красивое сообщение об ошибке:

def some_route():
    try:
        # code for route in here (including a return statement)

    except:
        exc_type, exc_value, exc_traceback = sys.exc_info()
        app.logger.error(traceback.print_exception(exc_type, exc_value, exc_traceback, limit=2))
        return render_template('error.html')

А затем прямо в конце файла я удаляю оператор debug=True. Хотя я не думаю, что мне нужно это делать, поскольку приложение запускается сервером fastcgi (?), Когда оно запускается в производстве. Последние две строки кода моего приложения выглядят так:

if __name__ == '__main__':
    app.run()

Я изо всех сил пытаюсь заставить это работать. Я думаю, что лучшее, что мне удалось, - это сохранить одно сообщение журнала ошибок в файле с помощью (app.logger.error('test message') ), но оно печатает только это одно сообщение. Попытка зарегистрировать другую ошибку сразу после этой просто игнорируется.


person Ben    schedule 26.12.2012    source источник
comment
См. также: flask.pocoo.org/docs/0.12/errorhandling   -  person Martin Thoma    schedule 12.09.2017


Ответы (5)


Я не знаю, почему это не работает, но я могу сказать, как я это делаю.

Прежде всего, вам не нужно устанавливать уровень app.logger. Поэтому удалите эту строку app.logger.setLevel().

Вы хотите сохранить исключение и вернуть страницу ошибки для каждого представления. Писать этот код везде — большая работа. Flask предоставляет способ сделать это. Определите такой метод обработчика ошибок.

    @app.errorhandler(500)
    def internal_error(exception):
        app.logger.error(exception)
        return render_template('500.html'), 500

Всякий раз, когда представление вызывает исключение, этот метод будет вызываться и передавать исключение в качестве аргумента. Ведение журнала Python предоставляет метод исключения, который используется для сохранения полной трассировки исключения.

Поскольку это обрабатывает все исключения, вам даже не нужно помещать код в блок try/except. Однако, если вы хотите что-то сделать перед вызовом обработчика ошибок (например, для сеанса отката или транзакции), сделайте следующее:

    try:
        #code
    except:
        #code
        raise

Если вы хотите, чтобы дата и время добавлялись для каждой записи в вашем файле журнала, можно использовать следующий код (вместо аналогичного кода, указанного в вопросе).

if app.debug is not True:   
    import logging
    from logging.handlers import RotatingFileHandler
    file_handler = RotatingFileHandler('python.log', maxBytes=1024 * 1024 * 100, backupCount=20)
    file_handler.setLevel(logging.ERROR)
    formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
    file_handler.setFormatter(formatter)
    app.logger.addHandler(file_handler)
person codecool    schedule 26.12.2012
comment
Но документация, по-видимому, подразумевает, что flask автоматически регистрирует исключения? в документах говорится, что если вы добавите обработчик почты, вы автоматически получите электронное письмо, если возникнут ошибки. Значит, @app.errorhandler не нужно? - person hwjp; 21.05.2014
comment
Ага. догадаться. 400 ошибок (ключевые ошибки в request.form) по умолчанию не регистрируются как настоящие исключения, а 500 — регистрируются. - person hwjp; 21.05.2014
comment
Не могли бы вы рассказать, почему уровень ошибки не нужно или не следует устанавливать? (приложение.logger.setLevel) - person Phalgun; 28.08.2014
comment
Сэкономьте мне часы отладки :)! Респект за классное объяснение. - person 0bserver07; 24.08.2015
comment
когда я устанавливаю file_handler.setLevel(logging.INFO), он по-прежнему регистрируется только на уровне предупреждения (по умолчанию root). Мне пришлось установить его в app.logger.setLevel(logging.INFO), затем он начинает регистрировать информацию и уровни выше. Любая идея, почему это так? - person Guagua; 06.11.2015
comment
Если бы я использовал uwsgi... куда бы я поместил свои журналы? Какие разрешения я должен установить для своего журнала? Спасибо! - person user805981; 17.11.2015
comment
если app.debug не равно True, всегда должно быть, если не app.debug ‹/sermon› - person domoarigato; 23.08.2016
comment
@Guagua Используйте app.logger.exception(exception), и трассировка стека будет записана. - person peacepassion; 17.11.2016
comment
Отмечу, что метод, украшенный @app.errorhandler(500), не нужен (написано начиная с Flask 0.12.1). По умолчанию Flask регистрирует все необработанные исключения в вашем регистраторе и обработчике. Если вы добавите этот метод, он будет работать, вы просто получите дважды зарегистрированные исключения регистратором по умолчанию и дополнительным журналом, который вы создаете с помощью app.logger.error(exception). - person coolboyjules; 27.06.2017
comment
2020-01-29 20:04:05,456 - ds_app.ds_backend - ОШИБКА - 2020-01-29 20:04:32,518 - ds_app.ds_backend - ОШИБКА - 2020-01-29 20:04:33,674 - ds_app.ds_backend - ОШИБКА - 2020-01-29 20:04:34,887 - ds_app.ds_backend - ОШИБКА - 2020-01-29 20:04:36,022 - ds_app.ds_backend - ОШИБКА - Я следовал выше. Его ошибка захвата, но не передача трассировки исключения. Не могли бы вы помочь мне с этим? - person iamabhaykmr; 29.01.2020

Для тех, кто прочитает это позже.

Я думаю, что лучше помещать больше полезной информации в сообщения об ошибках. URL-адрес, IP-адрес клиента, пользовательский агент и т. д. Flask регистрирует исключения внутри (в режиме app.debug==False) с помощью функции Flask.log_exception. Итак, вместо того, чтобы записывать вещи вручную в @app.errorhandler, я делаю что-то вроде этого:

class MoarFlask(Flask):
    def log_exception(self, exc_info):
        """...description omitted..."""
        self.logger.error(
            """
Request:   {method} {path}
IP:        {ip}
User:      {user}
Agent:     {agent_platform} | {agent_browser} {agent_browser_version}
Raw Agent: {agent}
            """.format(
                method = request.method,
                path = request.path,
                ip = request.remote_addr,
                agent_platform = request.user_agent.platform,
                agent_browser = request.user_agent.browser,
                agent_browser_version = request.user_agent.version,
                agent = request.user_agent.string,
                user=user
            ), exc_info=exc_info
        )

Затем во время настройки привяжите FileHandler к app.logger и продолжайте. Я не использую StreamHandler, потому что многие серверы (например, uWSGI) любят загрязнять его своими собственными проприетарными многословными бесполезными сообщениями, которые нельзя отключить.

Не бойтесь расширять Flask. Вы будете вынуждены сделать это рано или поздно ;)

person Ivan Kleshnin    schedule 13.12.2013
comment
Классный ответ Иван, хочу реализовать это сам! Не могли бы вы предоставить дополнительную информацию о том, как вы привязывали FileHandler к app.logger? Я понял вашу идею, я просто не вижу, как вы ее реализовали :-) - person Juxhin; 28.01.2016
comment
Извините, @Juxhih, я хотел бы вам ответить, но последний раз, когда я использовал Python, был 2 года назад, и я совершенно забыл об этом. - person Ivan Kleshnin; 28.01.2016
comment
Какой позор, хотелось бы реализовать это, так как это красиво и лаконично. Если вам случится наткнуться на старый код, сообщите нам (мне) об этом. - person Juxhin; 28.01.2016
comment
Можете ли вы опубликовать это как отдельный вопрос, на который я мог бы ответить? Мне нужно время, чтобы найти это в своих архивах, и я хотел бы получить взамен некоторый бонус к карме ;) - person Ivan Kleshnin; 28.01.2016
comment
Вечером постараюсь не забыть добавить пост :-) - person Juxhin; 28.01.2016
comment
Я действительно копаю «проприетарное-словное-бесполезное-не-выключаемое-отключение» как описание ошибок uWSGI! - person Two-Bit Alchemist; 07.04.2016
comment
Juxhin, используйте последний код предыдущего ответа :) - person PatricioS; 06.07.2016
comment
просто к сведению: функция dedent из textwrap действительно может помочь вашему шаблону ошибки. docs.python.org/2/library/textwrap.html#textwrap. дедент - person dusktreader; 03.11.2016

Я не специалист по модулю logging, но, учитывая мой опыт работы с ним + несколько лет работы с Python + Flask, у вас может быть хорошая конфигурация ведения журнала, учитывая некоторые наблюдения:

  • в начале каждой функции (маршрута) создайте объект timestamp, чтобы зарегистрировать точное время, когда был сделан запрос, независимо от того, был ли он успешным или нет

  • используйте @app.after_request для регистрации каждого успешного запроса

  • используйте @app.errorhandler для регистрации общих ошибок + Tracebacks

Вот пример, демонстрирующий эту идею:

#/usr/bin/python3
""" Demonstration of logging feature for a Flask App. """

from logging.handlers import RotatingFileHandler
from flask import Flask, request, jsonify
from time import strftime

__author__ = "@ivanleoncz"

import logging
import traceback


app = Flask(__name__)

@app.route("/")
@app.route("/index")
def get_index():
    """ Function for / and /index routes. """
    return "Welcome to Flask! "


@app.route("/data")
def get_data():
    """ Function for /data route. """
    data = {
            "Name":"Ivan Leon",
            "Occupation":"Software Developer",
            "Technologies":"[Python, Flask, JavaScript, Java, SQL]"
    }
    return jsonify(data)


@app.route("/error")
def get_nothing():
    """ Route for intentional error. """
    return foobar # intentional non-existent variable


@app.after_request
def after_request(response):
    """ Logging after every request. """
    # This avoids the duplication of registry in the log,
    # since that 500 is already logged via @app.errorhandler.
    if response.status_code != 500:
        ts = strftime('[%Y-%b-%d %H:%M]')
        logger.error('%s %s %s %s %s %s',
                      ts,
                      request.remote_addr,
                      request.method,
                      request.scheme,
                      request.full_path,
                      response.status)
    return response


@app.errorhandler(Exception)
def exceptions(e):
    """ Logging after every Exception. """
    ts = strftime('[%Y-%b-%d %H:%M]')
    tb = traceback.format_exc()
    logger.error('%s %s %s %s %s 5xx INTERNAL SERVER ERROR\n%s',
                  ts,
                  request.remote_addr,
                  request.method,
                  request.scheme,
                  request.full_path,
                  tb)
    return "Internal Server Error", 500


if __name__ == '__main__':
    handler = RotatingFileHandler('app.log', maxBytes=10000, backupCount=3)        
    logger = logging.getLogger(__name__)
    logger.setLevel(logging.ERROR)
    logger.addHandler(handler)
    app.run(host="127.0.0.1",port=8000)

Для получения дополнительной информации о logrotate и журналах на стандартном выводе и в файле одновременно: эта суть

person ivanleoncz    schedule 02.09.2016
comment
Разве это не зарегистрирует ошибку дважды? Разве обе функции errorhandler и after_request не вызываются при возникновении ошибки? @ivanleoncz - person EpicDavi; 02.08.2017
comment
Привет, Дави. Спасибо за ваш вопрос :). Ну, я так думаю, потому что он генерирует Traceback для зарегистрированного запроса, но позвольте мне проверить здесь. Завтра я сделаю все возможное, чтобы обновить этот ответ, если я найду что-нибудь лучше, но основная часть ответа работает нормально: я использовал это до тех пор, пока и работает очень хорошо. С уважением. - person ivanleoncz; 02.08.2017
comment
Спасибо за ответ! @ivanleoncz После некоторого тестирования я думаю, что он вызывает оба, но я понял, что это не обязательно плохо, потому что приятно иметь запись обо всех сделанных запросах (в том же формате). Единственное, что я изменил, это заставить after_request использовать logger.info вместо logger.error (но если вы не используете какое-либо специальное форматирование, это не имеет большого значения) - person EpicDavi; 02.08.2017
comment
Круто, @EpicDavi! Да, действительно хорошо иметь и то, и другое. Во всяком случае, я обнаружил другой способ записи запросов в файл журнала, но было бы необходимо (с моей точки зрения) иметь декоратор @app.after_request, потому что вы можете отслеживать особые ситуации, такие как общие ошибки или Tracebacks. Я сделаю несколько тестов здесь в течение недели и предоставлю обновление для этого ответа, чтобы дать другие возможности. Спасибо, что связались и поделились :). С уважением! - person ivanleoncz; 02.08.2017
comment
Это круто - person Selam Getachew; 26.08.2017

Если вы используете пушку для запуска своего приложения Flask, вы можете регистрировать все исключения Flask в журналах пушки, добавив обработчики ошибок пушки в регистратор Flask:

In module/__init__.py:

@app.before_first_request
def setup_logging():
    if not app.debug:
        import logging
        gunicorn_logger = logging.getLogger('gunicorn.error')
        for handler in gunicorn_logger.handlers:
            app.logger.addHandler(handler)
person user545424    schedule 08.07.2018

В разделе «Разработка» обязательно установите: app.config['PROPAGATE_EXCEPTIONS'] = False. По умолчанию — «Нет»: https://flask.palletsprojects.com/en/1.1.x/config/

person Al Martins    schedule 11.11.2020