Ticket #16288: 16288.diff

File 16288.diff, 15.0 KB (added by Carl Meyer, 13 years ago)

patch with back-compat shim, docs, tests

  • django/conf/__init__.py

    diff --git a/django/conf/__init__.py b/django/conf/__init__.py
    index 8964b1b..d5de56a 100644
    a b class Settings(BaseSettings):  
    135135            logging_config_module = importlib.import_module(logging_config_path)
    136136            logging_config_func = getattr(logging_config_module, logging_config_func_name)
    137137
     138            # Backwards-compatibility shim for #16288 fix
     139            compat_patch_logging_config(self.LOGGING)
     140
    138141            # ... then invoke it with the logging settings
    139142            logging_config_func(self.LOGGING)
    140143
    class UserSettingsHolder(BaseSettings):  
    165168
    166169settings = LazySettings()
    167170
     171
     172
     173def compat_patch_logging_config(logging_config):
     174    """
     175    Backwards-compatibility shim for #16288 fix. Takes initial value of
     176    ``LOGGING`` setting and patches it in-place (issuing deprecation warning)
     177    if "mail_admins" logging handler is configured but has no filters.
     178
     179    """
     180    #  Shim only if LOGGING["handlers"]["mail_admins"] exists,
     181    #  but has no "filters" key
     182    if "filters" not in logging_config.get(
     183        "handlers", {}).get(
     184        "mail_admins", {"filters": []}):
     185
     186        warnings.warn(
     187            "You have no filters defined on the 'mail_admins' logging "
     188            "handler. Since uncaught request exceptions are now logged "
     189            "in DEBUG mode as well as in production "
     190            "(see https://code.djangoproject.com/ticket/16288), "
     191            "to maintain backward-compatibility Django is adding a "
     192            "filter for you to prevent emailing admins in DEBUG mode. "
     193            "To get rid of this warning, add the filter explicitly "
     194            "in your LOGGING config "
     195            "(see http://docs.djangoproject.com/en/dev/releases/1.4/"
     196            "#request-exceptions-are-now-always-logged).",
     197            PendingDeprecationWarning)
     198
     199        filter_name = "require_debug_false"
     200
     201        filters = logging_config.setdefault("filters", {})
     202        while filter_name in filters:
     203            filter_name = filter_name + "_"
     204
     205        def _callback(record):
     206            from django.conf import settings
     207            return not settings.DEBUG
     208
     209        filters[filter_name] = {
     210            "()": "django.utils.log.CallbackFilter",
     211            "callback": _callback
     212            }
     213
     214        logging_config["handlers"]["mail_admins"]["filters"] = [filter_name]
  • django/conf/global_settings.py

    diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py
    index ab31c15..fedea55 100644
    a b LOGGING_CONFIG = 'django.utils.log.dictConfig'  
    525525LOGGING = {
    526526    'version': 1,
    527527    'disable_existing_loggers': False,
     528    'filters': {
     529        'require_debug_false': {
     530            '()': 'django.utils.log.CallbackFilter',
     531            'callback': lambda r: not DEBUG
     532        }
     533    },
    528534    'handlers': {
    529535        'mail_admins': {
    530536            'level': 'ERROR',
     537            'filters': ['require_debug_false'],
    531538            'class': 'django.utils.log.AdminEmailHandler'
    532539        }
    533540    },
  • django/conf/project_template/settings.py

    diff --git a/django/conf/project_template/settings.py b/django/conf/project_template/settings.py
    index e719dec..794522b 100644
    a b INSTALLED_APPS = (  
    125125
    126126# A sample logging configuration. The only tangible logging
    127127# performed by this configuration is to send an email to
    128 # the site admins on every HTTP 500 error.
     128# the site admins on every HTTP 500 error when DEBUG=False.
    129129# See http://docs.djangoproject.com/en/dev/topics/logging for
    130130# more details on how to customize your logging configuration.
    131131LOGGING = {
    132132    'version': 1,
    133133    'disable_existing_loggers': False,
     134    'filters': {
     135        'require_debug_false': {
     136            '()': 'django.utils.log.CallbackFilter',
     137            'callback': lambda r: not DEBUG
     138        }
     139    },
    134140    'handlers': {
    135141        'mail_admins': {
    136142            'level': 'ERROR',
     143            'filters': ['require_debug_false'],
    137144            'class': 'django.utils.log.AdminEmailHandler'
    138145        }
    139146    },
  • django/core/handlers/base.py

    diff --git a/django/core/handlers/base.py b/django/core/handlers/base.py
    index 7cae42e..5529097 100644
    a b class BaseHandler(object):  
    198198        if settings.DEBUG_PROPAGATE_EXCEPTIONS:
    199199            raise
    200200
    201         if settings.DEBUG:
    202             from django.views import debug
    203             return debug.technical_500_response(request, *exc_info)
    204 
    205201        logger.error('Internal Server Error: %s' % request.path,
    206202            exc_info=exc_info,
    207203            extra={
    class BaseHandler(object):  
    210206            }
    211207        )
    212208
     209        if settings.DEBUG:
     210            from django.views import debug
     211            return debug.technical_500_response(request, *exc_info)
     212
    213213        # If Http500 handler is not installed, re-raise last exception
    214214        if resolver.urlconf_module is None:
    215215            raise exc_info[1], None, exc_info[2]
  • django/utils/log.py

    diff --git a/django/utils/log.py b/django/utils/log.py
    index 969a9d9..d0f5109 100644
    a b class AdminEmailHandler(logging.Handler):  
    7070        reporter = ExceptionReporter(request, is_email=True, *exc_info)
    7171        html_message = self.include_html and reporter.get_traceback_html() or None
    7272        mail.mail_admins(subject, message, fail_silently=True, html_message=html_message)
     73
     74
     75class CallbackFilter(logging.Filter):
     76    """
     77    A logging filter that checks the return value of a given callable (which
     78    takes the record-to-be-logged as its only parameter) to decide whether to
     79    log a record.
     80
     81    """
     82    def __init__(self, callback):
     83        self.callback = callback
     84
     85
     86    def filter(self, record):
     87        if self.callback(record):
     88            return 1
     89        return 0
  • docs/internals/deprecation.txt

    diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt
    index 131f813..cd65a89 100644
    a b their deprecation, as per the :ref:`Django deprecation policy  
    210210        * Legacy ways of calling
    211211          :func:`~django.views.decorators.cache.cache_page` will be removed.
    212212
     213        * The backward-compatibility shim to automatically add a debug-false
     214          filter to the ``'mail_admins'`` logging handler will be removed. The
     215          :setting:`LOGGING` setting should include this filter explicitly if
     216          it is desired.
     217
    213218    * 2.0
    214219        * ``django.views.defaults.shortcut()``. This function has been moved
    215220          to ``django.contrib.contenttypes.views.shortcut()`` as part of the
  • docs/releases/1.4.txt

    diff --git a/docs/releases/1.4.txt b/docs/releases/1.4.txt
    index 7cc3fb1..0635ece 100644
    a b releases 8.0 and 8.1 was near (November 2010.)  
    387387
    388388Django 1.4 takes that policy further and sets 8.2 as the minimum PostgreSQL
    389389version it officially supports.
     390
     391Request exceptions are now always logged
     392~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     393
     394When :doc:`logging support </topics/logging/>` was added to Django in 1.3, the
     395admin error email support was moved into the
     396:class:`django.utils.log.AdminEmailHandler`, attached to the
     397``'django.request'`` logger. In order to maintain the established behavior of
     398error emails, the ``'django.request'`` logger was called only when
     399:setting:`DEBUG` was `False`.
     400
     401To increase the flexibility of request-error logging, the ``'django.request'``
     402logger is now called regardless of the value of :setting:`DEBUG`, and the
     403default settings file for new projects now includes a separate filter attached
     404to :class:`django.utils.log.AdminEmailHandler` to prevent admin error emails in
     405`DEBUG` mode::
     406
     407   'filters': {
     408        'require_debug_false': {
     409            '()': 'django.utils.log.CallbackFilter',
     410            'callback': lambda r: not DEBUG
     411        }
     412    },
     413    'handlers': {
     414        'mail_admins': {
     415            'level': 'ERROR',
     416            'filters': ['require_debug_false'],
     417            'class': 'django.utils.log.AdminEmailHandler'
     418        }
     419    },
     420
     421If your project was created prior to this change, your :setting:`LOGGING`
     422setting will not include this new filter. In order to maintain
     423backwards-compatibility, Django will detect that your ``'mail_admins'`` handler
     424configuration includes no ``'filters'`` section, and will automatically add
     425this filter for you and issue a pending-deprecation warning. This will become a
     426deprecation warning in Django 1.5, and in Django 1.6 the
     427backwards-compatibility shim will be removed entirely.
     428
     429The existence of any ``'filters'`` key under the ``'mail_admins'`` handler will
     430disable this backward-compatibility shim and deprecation warning.
  • docs/topics/logging.txt

    diff --git a/docs/topics/logging.txt b/docs/topics/logging.txt
    index 06050af..9137038 100644
    a b Python logging module.  
    501501    :ref:`Filtering error reports<filtering-error-reports>`.
    502502
    503503.. _django-sentry: http://pypi.python.org/pypi/django-sentry
     504
     505
     506Filters
     507-------
     508
     509Django provides one log filter in addition to those provided by the
     510Python logging module.
     511
     512.. class:: CallbackFilter(callback)
     513
     514   .. versionadded:: 1.4
     515
     516   This filter accepts a callback function (which should accept a single
     517   argument, the record to be logged), and calls it for each record that passes
     518   through the filter. Handling of that record will not proceed if the callback
     519   returns False.
     520
     521   This filter is used as follows in the default :setting:`LOGGING`
     522   configuration to ensure that the :class:`AdminEmailHandler` only sends error
     523   emails to admins when :setting:`DEBUG` is `False`::
     524
     525       'filters': {
     526            'require_debug_false': {
     527                '()': 'django.utils.log.CallbackFilter',
     528                'callback': lambda r: not DEBUG
     529            }
     530        },
     531        'handlers': {
     532            'mail_admins': {
     533                'level': 'ERROR',
     534                'filters': ['require_debug_false'],
     535                'class': 'django.utils.log.AdminEmailHandler'
     536            }
     537        },
  • new file tests/regressiontests/logging_tests/tests.py

    diff --git a/tests/regressiontests/logging_tests/__init__.py b/tests/regressiontests/logging_tests/__init__.py
    new file mode 100644
    index 0000000..e69de29
    diff --git a/tests/regressiontests/logging_tests/models.py b/tests/regressiontests/logging_tests/models.py
    new file mode 100644
    index 0000000..e69de29
    diff --git a/tests/regressiontests/logging_tests/tests.py b/tests/regressiontests/logging_tests/tests.py
    new file mode 100644
    index 0000000..e2da4d4
    - +  
     1from __future__ import with_statement
     2
     3import copy
     4
     5from django.conf import compat_patch_logging_config
     6from django.test import TestCase
     7from django.utils.log import CallbackFilter
     8
     9
     10# logging config prior to using filter with mail_admins
     11OLD_LOGGING = {
     12    'version': 1,
     13    'disable_existing_loggers': False,
     14    'handlers': {
     15        'mail_admins': {
     16            'level': 'ERROR',
     17            'class': 'django.utils.log.AdminEmailHandler'
     18        }
     19    },
     20    'loggers': {
     21        'django.request': {
     22            'handlers': ['mail_admins'],
     23            'level': 'ERROR',
     24            'propagate': True,
     25        },
     26    }
     27}
     28
     29
     30
     31class PatchLoggingConfigTest(TestCase):
     32    def test_filter_added(self):
     33        """
     34        Test that debug-false filter is added to mail_admins handler if it has
     35        no filters.
     36
     37        """
     38        config = copy.deepcopy(OLD_LOGGING)
     39        compat_patch_logging_config(config)
     40
     41        self.assertEqual(
     42            config["handlers"]["mail_admins"]["filters"],
     43            ['require_debug_false'])
     44
     45
     46    def test_filter_configuration(self):
     47        """
     48        Test that the debug-false filter is a CallbackFilter with a callback
     49        that works as expected (returns ``not DEBUG``).
     50
     51        """
     52        config = copy.deepcopy(OLD_LOGGING)
     53        compat_patch_logging_config(config)
     54
     55        flt = config["filters"]["require_debug_false"]
     56
     57        self.assertEqual(flt["()"], "django.utils.log.CallbackFilter")
     58
     59        callback = flt["callback"]
     60
     61        with self.settings(DEBUG=True):
     62            self.assertEqual(callback("record is not used"), False)
     63
     64        with self.settings(DEBUG=False):
     65            self.assertEqual(callback("record is not used"), True)
     66
     67
     68    def test_no_patch_if_filters_key_exists(self):
     69        """
     70        Test that the logging configuration is not modified if the mail_admins
     71        handler already has a "filters" key.
     72
     73        """
     74        config = copy.deepcopy(OLD_LOGGING)
     75        config["handlers"]["mail_admins"]["filters"] = []
     76        new_config = copy.deepcopy(config)
     77        compat_patch_logging_config(new_config)
     78
     79        self.assertEqual(config, new_config)
     80
     81    def test_no_patch_if_no_mail_admins_handler(self):
     82        """
     83        Test that the logging configuration is not modified if the mail_admins
     84        handler is not present.
     85
     86        """
     87        config = copy.deepcopy(OLD_LOGGING)
     88        config["handlers"].pop("mail_admins")
     89        new_config = copy.deepcopy(config)
     90        compat_patch_logging_config(new_config)
     91
     92        self.assertEqual(config, new_config)
     93
     94
     95class CallbackFilterTest(TestCase):
     96    def test_sense(self):
     97        f_false = CallbackFilter(lambda r: False)
     98        f_true = CallbackFilter(lambda r: True)
     99
     100        self.assertEqual(f_false.filter("record"), False)
     101        self.assertEqual(f_true.filter("record"), True)
     102
     103    def test_passes_on_record(self):
     104        collector = []
     105        def _callback(record):
     106            collector.append(record)
     107            return True
     108        f = CallbackFilter(_callback)
     109
     110        f.filter("a record")
     111
     112        self.assertEqual(collector, ["a record"])
  • tests/regressiontests/views/views.py

    diff --git a/tests/regressiontests/views/views.py b/tests/regressiontests/views/views.py
    index 732ff28..7d59257 100644
    a b def raises_template_does_not_exist(request):  
    134134
    135135def send_log(request, exc_info):
    136136    logger = getLogger('django.request')
     137    # The default logging config has a logging filter to ensure admin emails are
     138    # only sent with DEBUG=False, but since someone might choose to remove that
     139    # filter, we still want to be able to test the behavior of error emails
     140    # with DEBUG=True. So we need to remove the filter temporarily.
     141    admin_email_handler = [
     142        h for h in logger.handlers
     143        if h.__class__.__name__ == "AdminEmailHandler"
     144        ][0]
     145    orig_filters = admin_email_handler.filters
     146    admin_email_handler.filters = []
    137147    logger.error('Internal Server Error: %s' % request.path,
    138148        exc_info=exc_info,
    139149        extra={
    def send_log(request, exc_info):  
    141151            'request': request
    142152        }
    143153    )
     154    admin_email_handler.filters = orig_filters
    144155
    145156def non_sensitive_view(request):
    146157    # Do not just use plain strings for the variables' values in the code
Back to Top