Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Added logging support.

  • Loading branch information...
commit e7f67e13339073007a84271cce91200bfdb81006 1 parent 8c26bec
Armin Ronacher authored
Showing with 132 additions and 10 deletions.
  1. +4 −0 CHANGES
  2. +65 −9 flask.py
  3. +63 −1 tests/flask_tests.py
4 CHANGES
View
@@ -9,6 +9,10 @@ Version 0.5
Release date to be announced
- added support for categories for flashed messages.
+- the application now configures a :class:`logging.Handler` and will
+ log request handling exceptions to that logger when not in debug
+ mode. This makes it possible to receive mails on server errors
+ for example.
Version 0.2
-----------
74 flask.py
View
@@ -21,7 +21,7 @@
LocalStack, LocalProxy, create_environ, SharedDataMiddleware, \
ImmutableDict, cached_property, wrap_file, Headers
from werkzeug.routing import Map, Rule
-from werkzeug.exceptions import HTTPException
+from werkzeug.exceptions import HTTPException, InternalServerError
from werkzeug.contrib.securecookie import SecureCookie
# try to load the best simplejson implementation available. If JSON
@@ -659,6 +659,18 @@ class Flask(_PackageBoundObject):
#: .. versionadded:: 0.2
use_x_sendfile = False
+ #: the logging format used for the debug logger. This is only used when
+ #: the application is in debug mode, otherwise the attached logging
+ #: handler does the formatting.
+ #:
+ #: .. versionadded:: 0.5
+ debug_log_format = (
+ '-' * 80 + '\n' +
+ '%(levelname)s in %(module)s, %(filename)s:%(lineno)d]:\n' +
+ '%(message)s\n' +
+ '-' * 80
+ )
+
#: options that are passed directly to the Jinja2 environment
jinja_options = ImmutableDict(
autoescape=True,
@@ -753,6 +765,24 @@ def __init__(self, import_name):
)
self.jinja_env.filters['tojson'] = _tojson_filter
+ @cached_property
+ def logger(self):
+ """A :class:`logging.Logger` object for this application. The
+ default configuration is to log to stderr if the application is
+ in debug mode.
+ """
+ from logging import getLogger, StreamHandler, Formatter, DEBUG
+ class DebugHandler(StreamHandler):
+ def emit(x, record):
+ if self.debug:
+ StreamHandler.emit(x, record)
+ handler = DebugHandler()
+ handler.setLevel(DEBUG)
+ handler.setFormatter(Formatter(self.debug_log_format))
+ logger = getLogger(self.import_name)
+ logger.addHandler(handler)
+ return logger
+
def create_jinja_loader(self):
"""Creates the Jinja loader. By default just a package loader for
the configured package is returned that looks up templates in the
@@ -1010,6 +1040,38 @@ def context_processor(self, f):
self.template_context_processors[None].append(f)
return f
+ def handle_http_exception(self, e):
+ """Handles an HTTP exception. By default this will invoke the
+ registered error handlers and fall back to returning the
+ exception as response.
+
+ .. versionadded: 0.5
+ """
+ handler = self.error_handlers.get(e.code)
+ if handler is None:
+ return e
+ return handler(e)
+
+ def handle_exception(self, e):
+ """Default exception handling that kicks in when an exception
+ occours that is not catched. In debug mode the exception will
+ be re-raised immediately, otherwise it is logged an the handler
+ for an 500 internal server error is used. If no such handler
+ exists, a default 500 internal server error message is displayed.
+
+ .. versionadded: 0.5
+ """
+ handler = self.error_handlers.get(500)
+ if self.debug:
+ raise
+ self.logger.exception('Exception on %s [%s]' % (
+ request.path,
+ request.method
+ ))
+ if handler is None:
+ return InternalServerError()
+ return handler(e)
+
def dispatch_request(self):
"""Does the request dispatching. Matches the URL and returns the
return value of the view or error handler. This does not have to
@@ -1022,15 +1084,9 @@ def dispatch_request(self):
raise req.routing_exception
return self.view_functions[req.endpoint](**req.view_args)
except HTTPException, e:
- handler = self.error_handlers.get(e.code)
- if handler is None:
- return e
- return handler(e)
+ return self.handle_http_exception(e)
except Exception, e:
- handler = self.error_handlers.get(500)
- if self.debug or handler is None:
- raise
- return handler(e)
+ return self.handle_exception(e)
def make_response(self, rv):
"""Converts the return value from a view function to a real
64 tests/flask_tests.py
View
@@ -16,6 +16,7 @@
import flask
import unittest
import tempfile
+from contextlib import contextmanager
from datetime import datetime
from werkzeug import parse_date, parse_options_header
from cStringIO import StringIO
@@ -26,6 +27,16 @@
sys.path.append(os.path.join(example_path, 'minitwit'))
+@contextmanager
+def catch_stderr():
+ old_stderr = sys.stderr
+ sys.stderr = rv = StringIO()
+ try:
+ yield rv
+ finally:
+ sys.stderr = old_stderr
+
+
class ContextTestCase(unittest.TestCase):
def test_context_binding(self):
@@ -585,6 +596,56 @@ def test_attachment(self):
assert options['filename'] == 'index.txt'
+class LoggingTestCase(unittest.TestCase):
+
+ def test_debug_log(self):
+ app = flask.Flask(__name__)
+ app.debug = True
+ @app.route('/')
+ def index():
+ app.logger.warning('the standard library is dead')
+ return ''
+
+ @app.route('/exc')
+ def exc():
+ 1/0
+ c = app.test_client()
+
+ with catch_stderr() as err:
+ rv = c.get('/')
+ out = err.getvalue()
+ assert 'WARNING in flask_tests, flask_tests.py' in out
+ assert 'the standard library is dead' in out
+
+ with catch_stderr() as err:
+ try:
+ c.get('/exc')
+ except ZeroDivisionError:
+ pass
+ else:
+ assert False, 'debug log ate the exception'
+
+ def test_exception_logging(self):
+ from logging import StreamHandler
+ out = StringIO()
+ app = flask.Flask(__name__)
+ app.logger.addHandler(StreamHandler(out))
+
+ @app.route('/')
+ def index():
+ 1/0
+
+ rv = app.test_client().get('/')
+ assert rv.status_code == 500
+ assert 'Internal Server Error' in rv.data
+
+ err = out.getvalue()
+ assert 'Exception on / [GET]' in err
+ assert 'Traceback (most recent call last):' in err
+ assert '1/0' in err
+ assert 'ZeroDivisionError:' in err
+
+
def suite():
from minitwit_tests import MiniTwitTestCase
from flaskr_tests import FlaskrTestCase
@@ -592,8 +653,9 @@ def suite():
suite.addTest(unittest.makeSuite(ContextTestCase))
suite.addTest(unittest.makeSuite(BasicFunctionalityTestCase))
suite.addTest(unittest.makeSuite(TemplatingTestCase))
- suite.addTest(unittest.makeSuite(SendfileTestCase))
suite.addTest(unittest.makeSuite(ModuleTestCase))
+ suite.addTest(unittest.makeSuite(SendfileTestCase))
+ suite.addTest(unittest.makeSuite(LoggingTestCase))
if flask.json_available:
suite.addTest(unittest.makeSuite(JSONTestCase))
suite.addTest(unittest.makeSuite(MiniTwitTestCase))
Please sign in to comment.
Something went wrong with that request. Please try again.