From 7a3cb474ac33c3153650ec48d6a175ad96749111 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Fri, 22 Oct 2021 08:02:46 +0100 Subject: [PATCH] Improve error handling * Report errors to admin by e-mail * Return details of error in JSON for API calls * Show error details page for non-API calls --- matcher/error_mail.py | 46 +++++++++++++++++++++++++++++++++++++++++++ web_view.py | 33 +++++++++++++++++++++++++++++-- 2 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 matcher/error_mail.py diff --git a/matcher/error_mail.py b/matcher/error_mail.py new file mode 100644 index 0000000..45ad845 --- /dev/null +++ b/matcher/error_mail.py @@ -0,0 +1,46 @@ +import logging +from logging.handlers import SMTPHandler +from logging import Formatter +from flask import request + +PROJECT = 'osm-wikidata' + +class MatcherSMTPHandler(SMTPHandler): + def getSubject(self, record): # noqa: N802 + return (f'{PROJECT} error: {record.exc_info[0].__name__}' + if (record.exc_info and record.exc_info[0]) + else f'{PROJECT} error: {record.pathname}:{record.lineno:d}') + + +class RequestFormatter(Formatter): + def format(self, record): + record.request = request + return super().format(record) + + +def setup_error_mail(app): + if not app.config.get('ERROR_MAIL'): + return + formatter = RequestFormatter(''' + Message type: {levelname} + Location: {pathname:s}:{lineno:d} + Module: {module:s} + Function: {funcName:s} + Time: {asctime:s} + GET args: {request.args!r} + URL: {request.url} + + Message: + + {message:s} + ''', style='{') + + mail_handler = MatcherSMTPHandler(app.config['SMTP_HOST'], + app.config['MAIL_FROM'], + app.config['ADMINS'], + app.name + ' error') + mail_handler.setFormatter(formatter) + + mail_handler.setLevel(logging.ERROR) + app.logger.propagate = True + app.logger.addHandler(mail_handler) diff --git a/web_view.py b/web_view.py index 08544b5..a47335b 100755 --- a/web_view.py +++ b/web_view.py @@ -5,12 +5,14 @@ from flask import (Flask, render_template, request, jsonify, redirect, url_for, from sqlalchemy import func from sqlalchemy.sql.expression import update from matcher import (nominatim, model, database, commons, wikidata, wikidata_api, - osm_oauth, edit, mail, api) + osm_oauth, edit, mail, api, error_mail) +from werkzeug.debug.tbtools import get_current_traceback from matcher.data import property_map from time import time, sleep from requests_oauthlib import OAuth1Session from lxml import etree -from sqlalchemy.orm.attributes import flag_modified +import werkzeug.exceptions +import inspect import flask_login import requests import json @@ -24,6 +26,7 @@ re_point = re.compile(r'^POINT\((.+) (.+)\)$') app = Flask(__name__) app.debug = True app.config.from_object('config.default') +error_mail.setup_error_mail(app) login_manager = flask_login.LoginManager(app) login_manager.login_view = 'login_route' @@ -47,6 +50,32 @@ def shutdown_session(exception=None): def global_user(): g.user = flask_login.current_user._get_current_object() +def dict_repr_values(d): + return {key: repr(value) for key, value in d.items()} + + +@app.errorhandler(werkzeug.exceptions.InternalServerError) +def exception_handler(e): + tb = get_current_traceback() + last_frame = next(frame for frame in reversed(tb.frames) if not frame.is_library) + last_frame_args = inspect.getargs(last_frame.code) + if request.path.startswith("/api/"): + return cors_jsonify({ + "success": False, + "error": tb.exception, + "traceback": tb.plaintext, + "locals": dict_repr_values(last_frame.locals), + "last_function": { + "name": tb.frames[-1].function_name, + "args": repr(last_frame_args), + }, + }), 500 + + return render_template('show_error.html', + tb=tb, + last_frame=last_frame, + last_frame_args=last_frame_args), 500 + def cors_jsonify(*args, **kwargs): response = jsonify(*args, **kwargs) response.headers["Access-Control-Allow-Origin"] = "*"