diff --git a/CHANGES b/CHANGES index 3a7d696d..84aa4633 100644 --- a/CHANGES +++ b/CHANGES @@ -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 ----------- diff --git a/flask.py b/flask.py index 6fa3f7e3..72c24029 100644 --- a/flask.py +++ b/flask.py @@ -21,7 +21,7 @@ from werkzeug import Request as RequestBase, Response as ResponseBase, \ 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 @@ class Flask(_PackageBoundObject): ) 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 @@ class Flask(_PackageBoundObject): 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 @@ class Flask(_PackageBoundObject): 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 diff --git a/tests/flask_tests.py b/tests/flask_tests.py index f5dd12bb..6cde2a3c 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -16,6 +16,7 @@ import sys 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, 'flaskr')) 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 @@ class SendfileTestCase(unittest.TestCase): 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))