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
This commit is contained in:
Edward Betts 2021-10-22 08:02:46 +01:00
parent db7b13b706
commit 7a3cb474ac
2 changed files with 77 additions and 2 deletions

46
matcher/error_mail.py Normal file
View file

@ -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)

View file

@ -5,12 +5,14 @@ from flask import (Flask, render_template, request, jsonify, redirect, url_for,
from sqlalchemy import func from sqlalchemy import func
from sqlalchemy.sql.expression import update from sqlalchemy.sql.expression import update
from matcher import (nominatim, model, database, commons, wikidata, wikidata_api, 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 matcher.data import property_map
from time import time, sleep from time import time, sleep
from requests_oauthlib import OAuth1Session from requests_oauthlib import OAuth1Session
from lxml import etree from lxml import etree
from sqlalchemy.orm.attributes import flag_modified import werkzeug.exceptions
import inspect
import flask_login import flask_login
import requests import requests
import json import json
@ -24,6 +26,7 @@ re_point = re.compile(r'^POINT\((.+) (.+)\)$')
app = Flask(__name__) app = Flask(__name__)
app.debug = True app.debug = True
app.config.from_object('config.default') app.config.from_object('config.default')
error_mail.setup_error_mail(app)
login_manager = flask_login.LoginManager(app) login_manager = flask_login.LoginManager(app)
login_manager.login_view = 'login_route' login_manager.login_view = 'login_route'
@ -47,6 +50,32 @@ def shutdown_session(exception=None):
def global_user(): def global_user():
g.user = flask_login.current_user._get_current_object() 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): def cors_jsonify(*args, **kwargs):
response = jsonify(*args, **kwargs) response = jsonify(*args, **kwargs)
response.headers["Access-Control-Allow-Origin"] = "*" response.headers["Access-Control-Allow-Origin"] = "*"