Repository: ckan/ckan

Issue:
Error emails are not sent in flask routes
### CKAN Version if known (or site URL)
2.8, master

### Please describe the expected behaviour
When some route generates an error and error emails are configured, stacktrace and other info are expected to be mailed in specified email address.

### Please describe the actual behaviour
If the route is migrated to flask, the error email is never sent. Most likely due to the that mailing is based on pylons error middleware https://docs.pylonsproject.org/projects/pylons-webframework/en/latest/modules/middleware.html#pylons.middleware.ErrorHandler


### What steps can be taken to reproduce the issue? 
Configure error emails.
Insert syntax error to route migrated to flask. For example view/user.py.
Error page is generated, email is not sent.



Discussion:
Yes, this was an internal Pylons feature so we lost it during the migration. It seems pretty easy to add an [email log handler](http://flask.pocoo.org/docs/1.0/logging/#email-errors-to-admins) that would replace this functionality. And perhaps there is an already existing Flask plugin that wraps this.

Marking as Good for Contribution in case @Zharktas or someone else wants to work on it.

Resolving patch:
diff --git a/ckan/config/middleware/flask_app.py b/ckan/config/middleware/flask_app.py
--- a/ckan/config/middleware/flask_app.py
+++ b/ckan/config/middleware/flask_app.py
@@ -1,6 +1,7 @@
 # encoding: utf-8
 
 import os
+import sys
 import re
 import time
 import inspect
@@ -41,6 +42,7 @@
 
 import ckan.lib.plugins as lib_plugins
 import logging
+from logging.handlers import SMTPHandler
 log = logging.getLogger(__name__)
 
 
@@ -442,6 +444,7 @@ def _register_error_handler(app):
     u'''Register error handler'''
 
     def error_handler(e):
+        log.error(e, exc_info=sys.exc_info)
         if isinstance(e, HTTPException):
             extra_vars = {u'code': [e.code], u'content': e.description}
             # TODO: Remove
@@ -456,3 +459,37 @@ def error_handler(e):
         app.register_error_handler(code, error_handler)
     if not app.debug and not app.testing:
         app.register_error_handler(Exception, error_handler)
+        if config.get('email_to'):
+            _setup_error_mail_handler(app)
+
+
+def _setup_error_mail_handler(app):
+
+    class ContextualFilter(logging.Filter):
+        def filter(self, log_record):
+            log_record.url = request.path
+            log_record.method = request.method
+            log_record.ip = request.environ.get("REMOTE_ADDR")
+            log_record.headers = request.headers
+            return True
+
+    mailhost = tuple(config.get('smtp.server', 'localhost').split(":"))
+    mail_handler = SMTPHandler(
+        mailhost=mailhost,
+        fromaddr=config.get('error_email_from'),
+        toaddrs=[config.get('email_to')],
+        subject='Application Error'
+    )
+
+    mail_handler.setFormatter(logging.Formatter('''
+Time:               %(asctime)s
+URL:                %(url)s
+Method:             %(method)s
+IP:                 %(ip)s
+Headers:            %(headers)s
+
+'''))
+
+    context_provider = ContextualFilter()
+    app.logger.addFilter(context_provider)
+    app.logger.addHandler(mail_handler)


Test patch: