Fixed #16288 -- Enabled django.request exception logger regardless of DEBUG setting.

Thanks Matt Bennett for report and draft patch; Vinay Sajip and Russell Keith-Magee for review.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@16444 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Carl Meyer 2011-06-22 06:01:44 +00:00
parent 9eb2afddfa
commit 43503b093a
12 changed files with 285 additions and 5 deletions

View File

@ -135,6 +135,9 @@ class Settings(BaseSettings):
logging_config_module = importlib.import_module(logging_config_path) logging_config_module = importlib.import_module(logging_config_path)
logging_config_func = getattr(logging_config_module, logging_config_func_name) logging_config_func = getattr(logging_config_module, logging_config_func_name)
# Backwards-compatibility shim for #16288 fix
compat_patch_logging_config(self.LOGGING)
# ... then invoke it with the logging settings # ... then invoke it with the logging settings
logging_config_func(self.LOGGING) logging_config_func(self.LOGGING)
@ -165,3 +168,41 @@ class UserSettingsHolder(BaseSettings):
settings = LazySettings() settings = LazySettings()
def compat_patch_logging_config(logging_config):
"""
Backwards-compatibility shim for #16288 fix. Takes initial value of
``LOGGING`` setting and patches it in-place (issuing deprecation warning)
if "mail_admins" logging handler is configured but has no filters.
"""
# Shim only if LOGGING["handlers"]["mail_admins"] exists,
# but has no "filters" key
if "filters" not in logging_config.get(
"handlers", {}).get(
"mail_admins", {"filters": []}):
warnings.warn(
"You have no filters defined on the 'mail_admins' logging "
"handler: adding implicit debug-false-only filter. "
"See http://docs.djangoproject.com/en/dev/releases/1.4/"
"#request-exceptions-are-now-always-logged",
PendingDeprecationWarning)
filter_name = "require_debug_false"
filters = logging_config.setdefault("filters", {})
while filter_name in filters:
filter_name = filter_name + "_"
def _callback(record):
from django.conf import settings
return not settings.DEBUG
filters[filter_name] = {
"()": "django.utils.log.CallbackFilter",
"callback": _callback
}
logging_config["handlers"]["mail_admins"]["filters"] = [filter_name]

View File

@ -525,9 +525,16 @@ LOGGING_CONFIG = 'django.utils.log.dictConfig'
LOGGING = { LOGGING = {
'version': 1, 'version': 1,
'disable_existing_loggers': False, 'disable_existing_loggers': False,
'filters': {
'require_debug_false': {
'()': 'django.utils.log.CallbackFilter',
'callback': lambda r: not DEBUG
}
},
'handlers': { 'handlers': {
'mail_admins': { 'mail_admins': {
'level': 'ERROR', 'level': 'ERROR',
'filters': ['require_debug_false'],
'class': 'django.utils.log.AdminEmailHandler' 'class': 'django.utils.log.AdminEmailHandler'
} }
}, },

View File

@ -125,15 +125,22 @@ INSTALLED_APPS = (
# A sample logging configuration. The only tangible logging # A sample logging configuration. The only tangible logging
# performed by this configuration is to send an email to # performed by this configuration is to send an email to
# the site admins on every HTTP 500 error. # the site admins on every HTTP 500 error when DEBUG=False.
# See http://docs.djangoproject.com/en/dev/topics/logging for # See http://docs.djangoproject.com/en/dev/topics/logging for
# more details on how to customize your logging configuration. # more details on how to customize your logging configuration.
LOGGING = { LOGGING = {
'version': 1, 'version': 1,
'disable_existing_loggers': False, 'disable_existing_loggers': False,
'filters': {
'require_debug_false': {
'()': 'django.utils.log.CallbackFilter',
'callback': lambda r: not DEBUG
}
},
'handlers': { 'handlers': {
'mail_admins': { 'mail_admins': {
'level': 'ERROR', 'level': 'ERROR',
'filters': ['require_debug_false'],
'class': 'django.utils.log.AdminEmailHandler' 'class': 'django.utils.log.AdminEmailHandler'
} }
}, },

View File

@ -198,10 +198,6 @@ class BaseHandler(object):
if settings.DEBUG_PROPAGATE_EXCEPTIONS: if settings.DEBUG_PROPAGATE_EXCEPTIONS:
raise raise
if settings.DEBUG:
from django.views import debug
return debug.technical_500_response(request, *exc_info)
logger.error('Internal Server Error: %s' % request.path, logger.error('Internal Server Error: %s' % request.path,
exc_info=exc_info, exc_info=exc_info,
extra={ extra={
@ -210,6 +206,10 @@ class BaseHandler(object):
} }
) )
if settings.DEBUG:
from django.views import debug
return debug.technical_500_response(request, *exc_info)
# If Http500 handler is not installed, re-raise last exception # If Http500 handler is not installed, re-raise last exception
if resolver.urlconf_module is None: if resolver.urlconf_module is None:
raise exc_info[1], None, exc_info[2] raise exc_info[1], None, exc_info[2]

View File

@ -70,3 +70,20 @@ class AdminEmailHandler(logging.Handler):
reporter = ExceptionReporter(request, is_email=True, *exc_info) reporter = ExceptionReporter(request, is_email=True, *exc_info)
html_message = self.include_html and reporter.get_traceback_html() or None html_message = self.include_html and reporter.get_traceback_html() or None
mail.mail_admins(subject, message, fail_silently=True, html_message=html_message) mail.mail_admins(subject, message, fail_silently=True, html_message=html_message)
class CallbackFilter(logging.Filter):
"""
A logging filter that checks the return value of a given callable (which
takes the record-to-be-logged as its only parameter) to decide whether to
log a record.
"""
def __init__(self, callback):
self.callback = callback
def filter(self, record):
if self.callback(record):
return 1
return 0

View File

@ -210,6 +210,11 @@ their deprecation, as per the :ref:`Django deprecation policy
* Legacy ways of calling * Legacy ways of calling
:func:`~django.views.decorators.cache.cache_page` will be removed. :func:`~django.views.decorators.cache.cache_page` will be removed.
* The backward-compatibility shim to automatically add a debug-false
filter to the ``'mail_admins'`` logging handler will be removed. The
:setting:`LOGGING` setting should include this filter explicitly if
it is desired.
* 2.0 * 2.0
* ``django.views.defaults.shortcut()``. This function has been moved * ``django.views.defaults.shortcut()``. This function has been moved
to ``django.contrib.contenttypes.views.shortcut()`` as part of the to ``django.contrib.contenttypes.views.shortcut()`` as part of the

View File

@ -387,3 +387,44 @@ releases 8.0 and 8.1 was near (November 2010.)
Django 1.4 takes that policy further and sets 8.2 as the minimum PostgreSQL Django 1.4 takes that policy further and sets 8.2 as the minimum PostgreSQL
version it officially supports. version it officially supports.
Request exceptions are now always logged
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
When :doc:`logging support </topics/logging/>` was added to Django in 1.3, the
admin error email support was moved into the
:class:`django.utils.log.AdminEmailHandler`, attached to the
``'django.request'`` logger. In order to maintain the established behavior of
error emails, the ``'django.request'`` logger was called only when
:setting:`DEBUG` was `False`.
To increase the flexibility of request-error logging, the ``'django.request'``
logger is now called regardless of the value of :setting:`DEBUG`, and the
default settings file for new projects now includes a separate filter attached
to :class:`django.utils.log.AdminEmailHandler` to prevent admin error emails in
`DEBUG` mode::
'filters': {
'require_debug_false': {
'()': 'django.utils.log.CallbackFilter',
'callback': lambda r: not DEBUG
}
},
'handlers': {
'mail_admins': {
'level': 'ERROR',
'filters': ['require_debug_false'],
'class': 'django.utils.log.AdminEmailHandler'
}
},
If your project was created prior to this change, your :setting:`LOGGING`
setting will not include this new filter. In order to maintain
backwards-compatibility, Django will detect that your ``'mail_admins'`` handler
configuration includes no ``'filters'`` section, and will automatically add
this filter for you and issue a pending-deprecation warning. This will become a
deprecation warning in Django 1.5, and in Django 1.6 the
backwards-compatibility shim will be removed entirely.
The existence of any ``'filters'`` key under the ``'mail_admins'`` handler will
disable this backward-compatibility shim and deprecation warning.

View File

@ -501,3 +501,37 @@ Python logging module.
:ref:`Filtering error reports<filtering-error-reports>`. :ref:`Filtering error reports<filtering-error-reports>`.
.. _django-sentry: http://pypi.python.org/pypi/django-sentry .. _django-sentry: http://pypi.python.org/pypi/django-sentry
Filters
-------
Django provides one log filter in addition to those provided by the
Python logging module.
.. class:: CallbackFilter(callback)
.. versionadded:: 1.4
This filter accepts a callback function (which should accept a single
argument, the record to be logged), and calls it for each record that passes
through the filter. Handling of that record will not proceed if the callback
returns False.
This filter is used as follows in the default :setting:`LOGGING`
configuration to ensure that the :class:`AdminEmailHandler` only sends error
emails to admins when :setting:`DEBUG` is `False`::
'filters': {
'require_debug_false': {
'()': 'django.utils.log.CallbackFilter',
'callback': lambda r: not DEBUG
}
},
'handlers': {
'mail_admins': {
'level': 'ERROR',
'filters': ['require_debug_false'],
'class': 'django.utils.log.AdminEmailHandler'
}
},

View File

@ -0,0 +1,117 @@
from __future__ import with_statement
import copy
from django.conf import compat_patch_logging_config
from django.test import TestCase
from django.utils.log import CallbackFilter
# logging config prior to using filter with mail_admins
OLD_LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'mail_admins': {
'level': 'ERROR',
'class': 'django.utils.log.AdminEmailHandler'
}
},
'loggers': {
'django.request': {
'handlers': ['mail_admins'],
'level': 'ERROR',
'propagate': True,
},
}
}
class PatchLoggingConfigTest(TestCase):
"""
Tests for backward-compat shim for #16288. These tests should be removed in
Django 1.6 when that shim and DeprecationWarning are removed.
"""
def test_filter_added(self):
"""
Test that debug-false filter is added to mail_admins handler if it has
no filters.
"""
config = copy.deepcopy(OLD_LOGGING)
compat_patch_logging_config(config)
self.assertEqual(
config["handlers"]["mail_admins"]["filters"],
['require_debug_false'])
def test_filter_configuration(self):
"""
Test that the debug-false filter is a CallbackFilter with a callback
that works as expected (returns ``not DEBUG``).
"""
config = copy.deepcopy(OLD_LOGGING)
compat_patch_logging_config(config)
flt = config["filters"]["require_debug_false"]
self.assertEqual(flt["()"], "django.utils.log.CallbackFilter")
callback = flt["callback"]
with self.settings(DEBUG=True):
self.assertEqual(callback("record is not used"), False)
with self.settings(DEBUG=False):
self.assertEqual(callback("record is not used"), True)
def test_no_patch_if_filters_key_exists(self):
"""
Test that the logging configuration is not modified if the mail_admins
handler already has a "filters" key.
"""
config = copy.deepcopy(OLD_LOGGING)
config["handlers"]["mail_admins"]["filters"] = []
new_config = copy.deepcopy(config)
compat_patch_logging_config(new_config)
self.assertEqual(config, new_config)
def test_no_patch_if_no_mail_admins_handler(self):
"""
Test that the logging configuration is not modified if the mail_admins
handler is not present.
"""
config = copy.deepcopy(OLD_LOGGING)
config["handlers"].pop("mail_admins")
new_config = copy.deepcopy(config)
compat_patch_logging_config(new_config)
self.assertEqual(config, new_config)
class CallbackFilterTest(TestCase):
def test_sense(self):
f_false = CallbackFilter(lambda r: False)
f_true = CallbackFilter(lambda r: True)
self.assertEqual(f_false.filter("record"), False)
self.assertEqual(f_true.filter("record"), True)
def test_passes_on_record(self):
collector = []
def _callback(record):
collector.append(record)
return True
f = CallbackFilter(_callback)
f.filter("a record")
self.assertEqual(collector, ["a record"])

View File

@ -134,6 +134,16 @@ def raises_template_does_not_exist(request):
def send_log(request, exc_info): def send_log(request, exc_info):
logger = getLogger('django.request') logger = getLogger('django.request')
# The default logging config has a logging filter to ensure admin emails are
# only sent with DEBUG=False, but since someone might choose to remove that
# filter, we still want to be able to test the behavior of error emails
# with DEBUG=True. So we need to remove the filter temporarily.
admin_email_handler = [
h for h in logger.handlers
if h.__class__.__name__ == "AdminEmailHandler"
][0]
orig_filters = admin_email_handler.filters
admin_email_handler.filters = []
logger.error('Internal Server Error: %s' % request.path, logger.error('Internal Server Error: %s' % request.path,
exc_info=exc_info, exc_info=exc_info,
extra={ extra={
@ -141,6 +151,7 @@ def send_log(request, exc_info):
'request': request 'request': request
} }
) )
admin_email_handler.filters = orig_filters
def non_sensitive_view(request): def non_sensitive_view(request):
# Do not just use plain strings for the variables' values in the code # Do not just use plain strings for the variables' values in the code