Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

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
  • Loading branch information...
commit 43503b093a35ca4707c16d865f10929960bfa0b8 1 parent 9eb2afd
Carl Meyer authored
41  django/conf/__init__.py
@@ -135,6 +135,9 @@ def __init__(self, settings_module):
135 135
             logging_config_module = importlib.import_module(logging_config_path)
136 136
             logging_config_func = getattr(logging_config_module, logging_config_func_name)
137 137
 
  138
+            # Backwards-compatibility shim for #16288 fix
  139
+            compat_patch_logging_config(self.LOGGING)
  140
+
138 141
             # ... then invoke it with the logging settings
139 142
             logging_config_func(self.LOGGING)
140 143
 
@@ -165,3 +168,41 @@ def __dir__(self):
165 168
 
166 169
 settings = LazySettings()
167 170
 
  171
+
  172
+
  173
+def 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: adding implicit debug-false-only filter. "
  189
+            "See http://docs.djangoproject.com/en/dev/releases/1.4/"
  190
+            "#request-exceptions-are-now-always-logged",
  191
+            PendingDeprecationWarning)
  192
+
  193
+        filter_name = "require_debug_false"
  194
+
  195
+        filters = logging_config.setdefault("filters", {})
  196
+        while filter_name in filters:
  197
+            filter_name = filter_name + "_"
  198
+
  199
+        def _callback(record):
  200
+            from django.conf import settings
  201
+            return not settings.DEBUG
  202
+
  203
+        filters[filter_name] = {
  204
+            "()": "django.utils.log.CallbackFilter",
  205
+            "callback": _callback
  206
+            }
  207
+
  208
+        logging_config["handlers"]["mail_admins"]["filters"] = [filter_name]
7  django/conf/global_settings.py
@@ -525,9 +525,16 @@
525 525
 LOGGING = {
526 526
     'version': 1,
527 527
     'disable_existing_loggers': False,
  528
+    'filters': {
  529
+        'require_debug_false': {
  530
+            '()': 'django.utils.log.CallbackFilter',
  531
+            'callback': lambda r: not DEBUG
  532
+        }
  533
+    },
528 534
     'handlers': {
529 535
         'mail_admins': {
530 536
             'level': 'ERROR',
  537
+            'filters': ['require_debug_false'],
531 538
             'class': 'django.utils.log.AdminEmailHandler'
532 539
         }
533 540
     },
9  django/conf/project_template/settings.py
@@ -125,15 +125,22 @@
125 125
 
126 126
 # A sample logging configuration. The only tangible logging
127 127
 # 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.
129 129
 # See http://docs.djangoproject.com/en/dev/topics/logging for
130 130
 # more details on how to customize your logging configuration.
131 131
 LOGGING = {
132 132
     'version': 1,
133 133
     'disable_existing_loggers': False,
  134
+    'filters': {
  135
+        'require_debug_false': {
  136
+            '()': 'django.utils.log.CallbackFilter',
  137
+            'callback': lambda r: not DEBUG
  138
+        }
  139
+    },
134 140
     'handlers': {
135 141
         'mail_admins': {
136 142
             'level': 'ERROR',
  143
+            'filters': ['require_debug_false'],
137 144
             'class': 'django.utils.log.AdminEmailHandler'
138 145
         }
139 146
     },
8  django/core/handlers/base.py
@@ -198,10 +198,6 @@ def handle_uncaught_exception(self, request, resolver, exc_info):
198 198
         if settings.DEBUG_PROPAGATE_EXCEPTIONS:
199 199
             raise
200 200
 
201  
-        if settings.DEBUG:
202  
-            from django.views import debug
203  
-            return debug.technical_500_response(request, *exc_info)
204  
-
205 201
         logger.error('Internal Server Error: %s' % request.path,
206 202
             exc_info=exc_info,
207 203
             extra={
@@ -210,6 +206,10 @@ def handle_uncaught_exception(self, request, resolver, exc_info):
210 206
             }
211 207
         )
212 208
 
  209
+        if settings.DEBUG:
  210
+            from django.views import debug
  211
+            return debug.technical_500_response(request, *exc_info)
  212
+
213 213
         # If Http500 handler is not installed, re-raise last exception
214 214
         if resolver.urlconf_module is None:
215 215
             raise exc_info[1], None, exc_info[2]
17  django/utils/log.py
@@ -70,3 +70,20 @@ def emit(self, record):
70 70
         reporter = ExceptionReporter(request, is_email=True, *exc_info)
71 71
         html_message = self.include_html and reporter.get_traceback_html() or None
72 72
         mail.mail_admins(subject, message, fail_silently=True, html_message=html_message)
  73
+
  74
+
  75
+class 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
5  docs/internals/deprecation.txt
@@ -210,6 +210,11 @@ their deprecation, as per the :ref:`Django deprecation policy
210 210
         * Legacy ways of calling
211 211
           :func:`~django.views.decorators.cache.cache_page` will be removed.
212 212
 
  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
+
213 218
     * 2.0
214 219
         * ``django.views.defaults.shortcut()``. This function has been moved
215 220
           to ``django.contrib.contenttypes.views.shortcut()`` as part of the
41  docs/releases/1.4.txt
@@ -387,3 +387,44 @@ releases 8.0 and 8.1 was near (November 2010.)
387 387
 
388 388
 Django 1.4 takes that policy further and sets 8.2 as the minimum PostgreSQL
389 389
 version it officially supports.
  390
+
  391
+Request exceptions are now always logged
  392
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  393
+
  394
+When :doc:`logging support </topics/logging/>` was added to Django in 1.3, the
  395
+admin 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
  398
+error emails, the ``'django.request'`` logger was called only when
  399
+:setting:`DEBUG` was `False`.
  400
+
  401
+To increase the flexibility of request-error logging, the ``'django.request'``
  402
+logger is now called regardless of the value of :setting:`DEBUG`, and the
  403
+default settings file for new projects now includes a separate filter attached
  404
+to :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
+
  421
+If your project was created prior to this change, your :setting:`LOGGING`
  422
+setting will not include this new filter. In order to maintain
  423
+backwards-compatibility, Django will detect that your ``'mail_admins'`` handler
  424
+configuration includes no ``'filters'`` section, and will automatically add
  425
+this filter for you and issue a pending-deprecation warning. This will become a
  426
+deprecation warning in Django 1.5, and in Django 1.6 the
  427
+backwards-compatibility shim will be removed entirely.
  428
+
  429
+The existence of any ``'filters'`` key under the ``'mail_admins'`` handler will
  430
+disable this backward-compatibility shim and deprecation warning.
34  docs/topics/logging.txt
@@ -501,3 +501,37 @@ Python logging module.
501 501
     :ref:`Filtering error reports<filtering-error-reports>`.
502 502
 
503 503
 .. _django-sentry: http://pypi.python.org/pypi/django-sentry
  504
+
  505
+
  506
+Filters
  507
+-------
  508
+
  509
+Django provides one log filter in addition to those provided by the
  510
+Python 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
+        },
0  tests/regressiontests/logging_tests/__init__.py
No changes.
0  tests/regressiontests/logging_tests/models.py
No changes.
117  tests/regressiontests/logging_tests/tests.py
... ...
@@ -0,0 +1,117 @@
  1
+from __future__ import with_statement
  2
+
  3
+import copy
  4
+
  5
+from django.conf import compat_patch_logging_config
  6
+from django.test import TestCase
  7
+from django.utils.log import CallbackFilter
  8
+
  9
+
  10
+# logging config prior to using filter with mail_admins
  11
+OLD_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
+
  31
+class PatchLoggingConfigTest(TestCase):
  32
+    """
  33
+    Tests for backward-compat shim for #16288. These tests should be removed in
  34
+    Django 1.6 when that shim and DeprecationWarning are removed.
  35
+
  36
+    """
  37
+    def test_filter_added(self):
  38
+        """
  39
+        Test that debug-false filter is added to mail_admins handler if it has
  40
+        no filters.
  41
+
  42
+        """
  43
+        config = copy.deepcopy(OLD_LOGGING)
  44
+        compat_patch_logging_config(config)
  45
+
  46
+        self.assertEqual(
  47
+            config["handlers"]["mail_admins"]["filters"],
  48
+            ['require_debug_false'])
  49
+
  50
+
  51
+    def test_filter_configuration(self):
  52
+        """
  53
+        Test that the debug-false filter is a CallbackFilter with a callback
  54
+        that works as expected (returns ``not DEBUG``).
  55
+
  56
+        """
  57
+        config = copy.deepcopy(OLD_LOGGING)
  58
+        compat_patch_logging_config(config)
  59
+
  60
+        flt = config["filters"]["require_debug_false"]
  61
+
  62
+        self.assertEqual(flt["()"], "django.utils.log.CallbackFilter")
  63
+
  64
+        callback = flt["callback"]
  65
+
  66
+        with self.settings(DEBUG=True):
  67
+            self.assertEqual(callback("record is not used"), False)
  68
+
  69
+        with self.settings(DEBUG=False):
  70
+            self.assertEqual(callback("record is not used"), True)
  71
+
  72
+
  73
+    def test_no_patch_if_filters_key_exists(self):
  74
+        """
  75
+        Test that the logging configuration is not modified if the mail_admins
  76
+        handler already has a "filters" key.
  77
+
  78
+        """
  79
+        config = copy.deepcopy(OLD_LOGGING)
  80
+        config["handlers"]["mail_admins"]["filters"] = []
  81
+        new_config = copy.deepcopy(config)
  82
+        compat_patch_logging_config(new_config)
  83
+
  84
+        self.assertEqual(config, new_config)
  85
+
  86
+    def test_no_patch_if_no_mail_admins_handler(self):
  87
+        """
  88
+        Test that the logging configuration is not modified if the mail_admins
  89
+        handler is not present.
  90
+
  91
+        """
  92
+        config = copy.deepcopy(OLD_LOGGING)
  93
+        config["handlers"].pop("mail_admins")
  94
+        new_config = copy.deepcopy(config)
  95
+        compat_patch_logging_config(new_config)
  96
+
  97
+        self.assertEqual(config, new_config)
  98
+
  99
+
  100
+class CallbackFilterTest(TestCase):
  101
+    def test_sense(self):
  102
+        f_false = CallbackFilter(lambda r: False)
  103
+        f_true = CallbackFilter(lambda r: True)
  104
+
  105
+        self.assertEqual(f_false.filter("record"), False)
  106
+        self.assertEqual(f_true.filter("record"), True)
  107
+
  108
+    def test_passes_on_record(self):
  109
+        collector = []
  110
+        def _callback(record):
  111
+            collector.append(record)
  112
+            return True
  113
+        f = CallbackFilter(_callback)
  114
+
  115
+        f.filter("a record")
  116
+
  117
+        self.assertEqual(collector, ["a record"])
11  tests/regressiontests/views/views.py
@@ -134,6 +134,16 @@ def raises_template_does_not_exist(request):
134 134
 
135 135
 def send_log(request, exc_info):
136 136
     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 = []
137 147
     logger.error('Internal Server Error: %s' % request.path,
138 148
         exc_info=exc_info,
139 149
         extra={
@@ -141,6 +151,7 @@ def send_log(request, exc_info):
141 151
             'request': request
142 152
         }
143 153
     )
  154
+    admin_email_handler.filters = orig_filters
144 155
 
145 156
 def non_sensitive_view(request):
146 157
     # Do not just use plain strings for the variables' values in the code

0 notes on commit 43503b0

Please sign in to comment.
Something went wrong with that request. Please try again.