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:
parent
db7b13b706
commit
7a3cb474ac
46
matcher/error_mail.py
Normal file
46
matcher/error_mail.py
Normal 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)
|
33
web_view.py
33
web_view.py
|
@ -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"] = "*"
|
||||||
|
|
Loading…
Reference in a new issue