Code

Ticket #12012: t12012-alpha1.diff

File t12012-alpha1.diff, 43.3 KB (added by russellm, 4 years ago)

First draft of logging patch for trunk

Line 
1diff -r e943e3ecbf20 django/conf/__init__.py
2--- a/django/conf/__init__.py   Tue Sep 21 19:32:22 2010 +0000
3+++ b/django/conf/__init__.py   Sat Sep 25 13:53:31 2010 +0800
4@@ -16,6 +16,7 @@
5 
6 ENVIRONMENT_VARIABLE = "DJANGO_SETTINGS_MODULE"
7 
8+
9 class LazySettings(LazyObject):
10     """
11     A lazy proxy for either global Django settings or a custom settings object.
12@@ -114,6 +115,15 @@
13             os.environ['TZ'] = self.TIME_ZONE
14             time.tzset()
15 
16+        # Settings are configured, so we can set up the logger
17+        # First find the logging configuration function ...
18+        logging_config_path, logging_config_func_name = self.LOGGING_CONFIG.rsplit('.', 1)
19+        logging_config_module = importlib.import_module(logging_config_path)
20+        logging_config_func = getattr(logging_config_module, logging_config_func_name)
21+
22+        # ... then invoke it with the logging settings
23+        logging_config_func(self.LOGGING)
24+
25 class UserSettingsHolder(object):
26     """
27     Holder for user configured settings.
28diff -r e943e3ecbf20 django/conf/global_settings.py
29--- a/django/conf/global_settings.py    Tue Sep 21 19:32:22 2010 +0000
30+++ b/django/conf/global_settings.py    Sat Sep 25 13:53:31 2010 +0800
31@@ -499,6 +499,38 @@
32 # django.contrib.messages to avoid imports in this settings file.
33 
34 ###########
35+# LOGGING #
36+###########
37+
38+LOGGING_CONFIG = 'django.utils.log.dictConfig'
39+LOGGING = {
40+    'version': 1,
41+    'disable_existing_loggers': False,
42+    'handlers': {
43+        'null': {
44+            'level':'DEBUG',
45+            'class':'django.utils.log.NullHandler',
46+        },
47+        'mail_admins': {
48+            'level': 'ERROR',
49+            'class': 'django.utils.log.AdminEmailHandler'
50+        }
51+    },
52+    'loggers': {
53+        'django': {
54+            'handlers':['null'],
55+            'propagate': True,
56+            'level':'INFO',
57+        },
58+        'django.request':{
59+            'handlers': ['mail_admins'],
60+            'level': 'ERROR',
61+            'propagate': True,
62+        },
63+    }
64+}
65+
66+###########
67 # TESTING #
68 ###########
69 
70diff -r e943e3ecbf20 django/conf/project_template/settings.py
71--- a/django/conf/project_template/settings.py  Tue Sep 21 19:32:22 2010 +0000
72+++ b/django/conf/project_template/settings.py  Sat Sep 25 13:53:31 2010 +0800
73@@ -94,3 +94,30 @@
74     # Uncomment the next line to enable admin documentation:
75     # 'django.contrib.admindocs',
76 )
77+
78+LOGGING = {
79+    'version': 1,
80+    'disable_existing_loggers': False,
81+    'handlers': {
82+        'console':{
83+            'level':'DEBUG',
84+            'class':'logging.StreamHandler',
85+        },
86+        'mail_admins': {
87+            'level': 'ERROR',
88+            'class': 'django.utils.log.AdminEmailHandler'
89+        }
90+    },
91+    'loggers': {
92+        'django': {
93+            'handlers':['console'],
94+            'propagate': True,
95+            'level':'INFO',
96+        },
97+        'django.request':{
98+            'handlers': ['mail_admins'],
99+            'level': 'ERROR',
100+            'propagate': True,
101+        },
102+    }
103+}
104diff -r e943e3ecbf20 django/core/handlers/base.py
105--- a/django/core/handlers/base.py      Tue Sep 21 19:32:22 2010 +0000
106+++ b/django/core/handlers/base.py      Sat Sep 25 13:53:31 2010 +0800
107@@ -1,3 +1,4 @@
108+import logging
109 import sys
110 
111 from django import http
112@@ -5,6 +6,9 @@
113 from django.utils.encoding import force_unicode
114 from django.utils.importlib import import_module
115 
116+logger = logging.getLogger('django.request')
117+
118+
119 class BaseHandler(object):
120     # Changes that are always applied to a response (in this order).
121     response_fixes = [
122@@ -118,6 +122,7 @@
123 
124                 return response
125             except http.Http404, e:
126+                logger.warning('404 Not Found: %s' % request.path, extra={'request': request})
127                 if settings.DEBUG:
128                     from django.views import debug
129                     return debug.technical_404_response(request, e)
130@@ -131,6 +136,7 @@
131                         finally:
132                             receivers = signals.got_request_exception.send(sender=self.__class__, request=request)
133             except exceptions.PermissionDenied:
134+                logger.warning('403 Forbidden (Permission denied): %s' % request.path, extra={'request': request})
135                 return http.HttpResponseForbidden('<h1>Permission denied</h1>')
136             except SystemExit:
137                 # Allow sys.exit() to actually exit. See tickets #1023 and #4701
138@@ -155,7 +161,6 @@
139         available would be an error.
140         """
141         from django.conf import settings
142-        from django.core.mail import mail_admins
143 
144         if settings.DEBUG_PROPAGATE_EXCEPTIONS:
145             raise
146@@ -164,14 +169,11 @@
147             from django.views import debug
148             return debug.technical_500_response(request, *exc_info)
149 
150-        # When DEBUG is False, send an error message to the admins.
151-        subject = 'Error (%s IP): %s' % ((request.META.get('REMOTE_ADDR') in settings.INTERNAL_IPS and 'internal' or 'EXTERNAL'), request.path)
152-        try:
153-            request_repr = repr(request)
154-        except:
155-            request_repr = "Request repr() unavailable"
156-        message = "%s\n\n%s" % (self._get_traceback(exc_info), request_repr)
157-        mail_admins(subject, message, fail_silently=True)
158+        logger.error('500 Internal Server Error: %s' % request.path,
159+            exc_info=exc_info,
160+            extra={'request':request}
161+        )
162+
163         # If Http500 handler is not installed, re-raise last exception
164         if resolver.urlconf_module is None:
165             raise exc_info[1], None, exc_info[2]
166@@ -179,11 +181,6 @@
167         callback, param_dict = resolver.resolve500()
168         return callback(request, **param_dict)
169 
170-    def _get_traceback(self, exc_info=None):
171-        "Helper function to return the traceback as a string"
172-        import traceback
173-        return '\n'.join(traceback.format_exception(*(exc_info or sys.exc_info())))
174-
175     def apply_response_fixes(self, request, response):
176         """
177         Applies each of the functions in self.response_fixes to the request and
178diff -r e943e3ecbf20 django/core/handlers/modpython.py
179--- a/django/core/handlers/modpython.py Tue Sep 21 19:32:22 2010 +0000
180+++ b/django/core/handlers/modpython.py Sat Sep 25 13:53:31 2010 +0800
181@@ -1,5 +1,7 @@
182+import logging
183 import os
184 from pprint import pformat
185+import sys
186 from warnings import warn
187 
188 from django import http
189@@ -9,6 +11,9 @@
190 from django.utils import datastructures
191 from django.utils.encoding import force_unicode, smart_str, iri_to_uri
192 
193+logger = logging.getLogger('django.request')
194+
195+
196 # NOTE: do *not* import settings (or any module which eventually imports
197 # settings) until after ModPythonHandler has been called; otherwise os.environ
198 # won't be set up correctly (with respect to settings).
199@@ -200,6 +205,10 @@
200             try:
201                 request = self.request_class(req)
202             except UnicodeDecodeError:
203+                logger.warning('400 Bad Request (UnicodeDecodeError): %s' % request.path,
204+                    exc_info=sys.exc_info(),
205+                    extra={'request': request}
206+                )
207                 response = http.HttpResponseBadRequest()
208             else:
209                 response = self.get_response(request)
210diff -r e943e3ecbf20 django/core/handlers/wsgi.py
211--- a/django/core/handlers/wsgi.py      Tue Sep 21 19:32:22 2010 +0000
212+++ b/django/core/handlers/wsgi.py      Sat Sep 25 13:53:31 2010 +0800
213@@ -1,5 +1,7 @@
214+import logging
215+from pprint import pformat
216+import sys
217 from threading import Lock
218-from pprint import pformat
219 try:
220     from cStringIO import StringIO
221 except ImportError:
222@@ -12,6 +14,9 @@
223 from django.utils import datastructures
224 from django.utils.encoding import force_unicode, iri_to_uri
225 
226+logger = logging.getLogger('django.request')
227+
228+
229 # See http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
230 STATUS_CODE_TEXT = {
231     100: 'CONTINUE',
232@@ -236,6 +241,10 @@
233             try:
234                 request = self.request_class(environ)
235             except UnicodeDecodeError:
236+                logger.warning('400 Bad Request (UnicodeDecodeError): %s' % request.path,
237+                    exc_info=sys.exc_info(),
238+                    extra={'request': request}
239+                )
240                 response = http.HttpResponseBadRequest()
241             else:
242                 response = self.get_response(request)
243diff -r e943e3ecbf20 django/db/backends/util.py
244--- a/django/db/backends/util.py        Tue Sep 21 19:32:22 2010 +0000
245+++ b/django/db/backends/util.py        Sat Sep 25 13:53:31 2010 +0800
246@@ -1,9 +1,12 @@
247 import datetime
248 import decimal
249+import logging
250 from time import time
251 
252 from django.utils.hashcompat import md5_constructor
253 
254+logger = logging.getLogger('django.db.backends')
255+
256 class CursorDebugWrapper(object):
257     def __init__(self, cursor, db):
258         self.cursor = cursor
259@@ -15,11 +18,15 @@
260             return self.cursor.execute(sql, params)
261         finally:
262             stop = time()
263+            duration = stop - start
264             sql = self.db.ops.last_executed_query(self.cursor, sql, params)
265             self.db.queries.append({
266                 'sql': sql,
267-                'time': "%.3f" % (stop - start),
268+                'time': "%.3f" % duration,
269             })
270+            logger.debug('(%.3f) %s; args=%s' % (duration, sql, params),
271+                extra={'duration':duration, 'sql':sql, 'params':params}
272+            )
273 
274     def executemany(self, sql, param_list):
275         start = time()
276@@ -27,10 +34,14 @@
277             return self.cursor.executemany(sql, param_list)
278         finally:
279             stop = time()
280+            duration = stop - start
281             self.db.queries.append({
282                 'sql': '%s times: %s' % (len(param_list), sql),
283-                'time': "%.3f" % (stop - start),
284+                'time': "%.3f" % duration,
285             })
286+            logger.debug('(%.3f) %s; args=%s' % (duration, sql, param_list),
287+                extra={'duration':duration, 'sql':sql, 'params':param_list}
288+            )
289 
290     def __getattr__(self, attr):
291         if attr in self.__dict__:
292diff -r e943e3ecbf20 django/db/models/loading.py
293--- a/django/db/models/loading.py       Tue Sep 21 19:32:22 2010 +0000
294+++ b/django/db/models/loading.py       Sat Sep 25 13:53:31 2010 +0800
295@@ -55,10 +55,20 @@
296         try:
297             if self.loaded:
298                 return
299+            # First, try to invoke the startup sequence for every app.
300+            # Only do this once, and at the top level of nesting.
301+            if not self.nesting_level:
302+                for app_name in settings.INSTALLED_APPS:
303+                    app_module = import_module(app_name)
304+                    if module_has_submodule(app_module, 'startup'):
305+                        import_module('.startup', app_name)
306+            # Now try to load every app
307             for app_name in settings.INSTALLED_APPS:
308                 if app_name in self.handled:
309                     continue
310                 self.load_app(app_name, True)
311+            # Check to see if we've imported everything.
312+            # If some apps have been postponed, try to load them again.
313             if not self.nesting_level:
314                 for app_name in self.postponed:
315                     self.load_app(app_name)
316diff -r e943e3ecbf20 django/middleware/common.py
317--- a/django/middleware/common.py       Tue Sep 21 19:32:22 2010 +0000
318+++ b/django/middleware/common.py       Sat Sep 25 13:53:31 2010 +0800
319@@ -1,3 +1,4 @@
320+import logging
321 import re
322 
323 from django.conf import settings
324@@ -7,6 +8,9 @@
325 from django.core import urlresolvers
326 from django.utils.hashcompat import md5_constructor
327 
328+logger = logging.getLogger('django.request')
329+
330+
331 class CommonMiddleware(object):
332     """
333     "Common" middleware for taking care of some basic operations:
334@@ -38,6 +42,9 @@
335         if 'HTTP_USER_AGENT' in request.META:
336             for user_agent_regex in settings.DISALLOWED_USER_AGENTS:
337                 if user_agent_regex.search(request.META['HTTP_USER_AGENT']):
338+                    logger.warning('403 Forbidden (User agent): %s' % request.path,
339+                        extra={'request': request}
340+                    )
341                     return http.HttpResponseForbidden('<h1>Forbidden</h1>')
342 
343         # Check for a redirect based on settings.APPEND_SLASH
344diff -r e943e3ecbf20 django/middleware/csrf.py
345--- a/django/middleware/csrf.py Tue Sep 21 19:32:22 2010 +0000
346+++ b/django/middleware/csrf.py Sat Sep 25 13:53:31 2010 +0800
347@@ -6,6 +6,7 @@
348 """
349 
350 import itertools
351+import logging
352 import re
353 import random
354 
355@@ -20,6 +21,8 @@
356 
357 _HTML_TYPES = ('text/html', 'application/xhtml+xml')
358 
359+logger = logging.getLogger('django.request')
360+
361 # Use the system (hardware-based) random number generator if it exists.
362 if hasattr(random, 'SystemRandom'):
363     randrange = random.SystemRandom().randrange
364@@ -169,14 +172,26 @@
365                 # we can use strict Referer checking.
366                 referer = request.META.get('HTTP_REFERER')
367                 if referer is None:
368+                    logger.warning('403 Forbidden (%s): %s' % (REASON_NO_COOKIE, request.path),
369+                        extra={
370+                            'request': request,
371+                            'reason': REASON_NO_REFERER,
372+                        }
373+                    )
374                     return reject(REASON_NO_REFERER)
375 
376                 # The following check ensures that the referer is HTTPS,
377                 # the domains match and the ports match - the same origin policy.
378                 good_referer = 'https://%s/' % request.get_host()
379                 if not referer.startswith(good_referer):
380-                    return reject(REASON_BAD_REFERER %
381-                                  (referer, good_referer))
382+                    reason = REASON_BAD_REFERER % (referer, good_referer)
383+                    logger.warning('403 Forbidden (%s): %s' % (reason, request.path),
384+                        extra={
385+                            'request': request,
386+                            'reason': reason,
387+                        }
388+                    )
389+                    return reject(reason)
390 
391             # If the user didn't already have a CSRF cookie, then fall back to
392             # the Django 1.1 method (hash of session ID), so a request is not
393@@ -190,6 +205,12 @@
394                     # No CSRF cookie and no session cookie. For POST requests,
395                     # we insist on a CSRF cookie, and in this way we can avoid
396                     # all CSRF attacks, including login CSRF.
397+                    logger.warning('403 Forbidden (%s): %s' % (REASON_NO_COOKIE, request.path),
398+                        extra={
399+                            'request': request,
400+                            'reason': REASON_NO_COOKIE,
401+                        }
402+                    )
403                     return reject(REASON_NO_COOKIE)
404             else:
405                 csrf_token = request.META["CSRF_COOKIE"]
406@@ -199,8 +220,20 @@
407             if request_csrf_token != csrf_token:
408                 if cookie_is_new:
409                     # probably a problem setting the CSRF cookie
410+                    logger.warning('403 Forbidden (%s): %s' % (REASON_NO_CSRF_COOKIE, request.path),
411+                        extra={
412+                            'request': request,
413+                            'reason': REASON_NO_CSRF_COOKIE,
414+                        }
415+                    )
416                     return reject(REASON_NO_CSRF_COOKIE)
417                 else:
418+                    logger.warning('403 Forbidden (%s): %s' % (REASON_BAD_TOKEN, request.path),
419+                        extra={
420+                            'request': request,
421+                            'reason': REASON_BAD_TOKEN,
422+                        }
423+                    )
424                     return reject(REASON_BAD_TOKEN)
425 
426         return accept()
427diff -r e943e3ecbf20 django/utils/dictconfig.py
428--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
429+++ b/django/utils/dictconfig.py        Sat Sep 25 13:53:31 2010 +0800
430@@ -0,0 +1,547 @@
431+# This is a copy of the Python logging.config.dictconfig module,
432+# reproduced with permission. It is provided here for backwards
433+# compatibility for Python versions prior to 2.7.
434+#
435+# Copyright 2009-2010 by Vinay Sajip. All Rights Reserved.
436+#
437+# Permission to use, copy, modify, and distribute this software and its
438+# documentation for any purpose and without fee is hereby granted,
439+# provided that the above copyright notice appear in all copies and that
440+# both that copyright notice and this permission notice appear in
441+# supporting documentation, and that the name of Vinay Sajip
442+# not be used in advertising or publicity pertaining to distribution
443+# of the software without specific, written prior permission.
444+# VINAY SAJIP DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING
445+# ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
446+# VINAY SAJIP BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR
447+# ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER
448+# IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
449+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
450+
451+import logging.handlers
452+import re
453+import sys
454+import types
455+
456+IDENTIFIER = re.compile('^[a-z_][a-z0-9_]*$', re.I)
457+
458+def valid_ident(s):
459+    m = IDENTIFIER.match(s)
460+    if not m:
461+        raise ValueError('Not a valid Python identifier: %r' % s)
462+    return True
463+
464+#
465+# This function is defined in logging only in recent versions of Python
466+#
467+try:
468+    from logging import _checkLevel
469+except ImportError:
470+    def _checkLevel(level):
471+        if isinstance(level, int):
472+            rv = level
473+        elif str(level) == level:
474+            if level not in logging._levelNames:
475+                raise ValueError('Unknown level: %r' % level)
476+            rv = logging._levelNames[level]
477+        else:
478+            raise TypeError('Level not an integer or a '
479+                            'valid string: %r' % level)
480+        return rv
481+
482+# The ConvertingXXX classes are wrappers around standard Python containers,
483+# and they serve to convert any suitable values in the container. The
484+# conversion converts base dicts, lists and tuples to their wrapped
485+# equivalents, whereas strings which match a conversion format are converted
486+# appropriately.
487+#
488+# Each wrapper should have a configurator attribute holding the actual
489+# configurator to use for conversion.
490+
491+class ConvertingDict(dict):
492+    """A converting dictionary wrapper."""
493+
494+    def __getitem__(self, key):
495+        value = dict.__getitem__(self, key)
496+        result = self.configurator.convert(value)
497+        #If the converted value is different, save for next time
498+        if value is not result:
499+            self[key] = result
500+            if type(result) in (ConvertingDict, ConvertingList,
501+                                ConvertingTuple):
502+                result.parent = self
503+                result.key = key
504+        return result
505+
506+    def get(self, key, default=None):
507+        value = dict.get(self, key, default)
508+        result = self.configurator.convert(value)
509+        #If the converted value is different, save for next time
510+        if value is not result:
511+            self[key] = result
512+            if type(result) in (ConvertingDict, ConvertingList,
513+                                ConvertingTuple):
514+                result.parent = self
515+                result.key = key
516+        return result
517+
518+    def pop(self, key, default=None):
519+        value = dict.pop(self, key, default)
520+        result = self.configurator.convert(value)
521+        if value is not result:
522+            if type(result) in (ConvertingDict, ConvertingList,
523+                                ConvertingTuple):
524+                result.parent = self
525+                result.key = key
526+        return result
527+
528+class ConvertingList(list):
529+    """A converting list wrapper."""
530+    def __getitem__(self, key):
531+        value = list.__getitem__(self, key)
532+        result = self.configurator.convert(value)
533+        #If the converted value is different, save for next time
534+        if value is not result:
535+            self[key] = result
536+            if type(result) in (ConvertingDict, ConvertingList,
537+                                ConvertingTuple):
538+                result.parent = self
539+                result.key = key
540+        return result
541+
542+    def pop(self, idx=-1):
543+        value = list.pop(self, idx)
544+        result = self.configurator.convert(value)
545+        if value is not result:
546+            if type(result) in (ConvertingDict, ConvertingList,
547+                                ConvertingTuple):
548+                result.parent = self
549+        return result
550+
551+class ConvertingTuple(tuple):
552+    """A converting tuple wrapper."""
553+    def __getitem__(self, key):
554+        value = tuple.__getitem__(self, key)
555+        result = self.configurator.convert(value)
556+        if value is not result:
557+            if type(result) in (ConvertingDict, ConvertingList,
558+                                ConvertingTuple):
559+                result.parent = self
560+                result.key = key
561+        return result
562+
563+class BaseConfigurator(object):
564+    """
565+    The configurator base class which defines some useful defaults.
566+    """
567+
568+    CONVERT_PATTERN = re.compile(r'^(?P<prefix>[a-z]+)://(?P<suffix>.*)$')
569+
570+    WORD_PATTERN = re.compile(r'^\s*(\w+)\s*')
571+    DOT_PATTERN = re.compile(r'^\.\s*(\w+)\s*')
572+    INDEX_PATTERN = re.compile(r'^\[\s*(\w+)\s*\]\s*')
573+    DIGIT_PATTERN = re.compile(r'^\d+$')
574+
575+    value_converters = {
576+        'ext' : 'ext_convert',
577+        'cfg' : 'cfg_convert',
578+    }
579+
580+    # We might want to use a different one, e.g. importlib
581+    importer = __import__
582+
583+    def __init__(self, config):
584+        self.config = ConvertingDict(config)
585+        self.config.configurator = self
586+
587+    def resolve(self, s):
588+        """
589+        Resolve strings to objects using standard import and attribute
590+        syntax.
591+        """
592+        name = s.split('.')
593+        used = name.pop(0)
594+        found = self.importer(used)
595+        for frag in name:
596+            used += '.' + frag
597+            try:
598+                found = getattr(found, frag)
599+            except AttributeError:
600+                self.importer(used)
601+                found = getattr(found, frag)
602+        return found
603+
604+    def ext_convert(self, value):
605+        """Default converter for the ext:// protocol."""
606+        return self.resolve(value)
607+
608+    def cfg_convert(self, value):
609+        """Default converter for the cfg:// protocol."""
610+        rest = value
611+        m = self.WORD_PATTERN.match(rest)
612+        if m is None:
613+            raise ValueError("Unable to convert %r" % value)
614+        else:
615+            rest = rest[m.end():]
616+            d = self.config[m.groups()[0]]
617+            #print d, rest
618+            while rest:
619+                m = self.DOT_PATTERN.match(rest)
620+                if m:
621+                    d = d[m.groups()[0]]
622+                else:
623+                    m = self.INDEX_PATTERN.match(rest)
624+                    if m:
625+                        idx = m.groups()[0]
626+                        if not self.DIGIT_PATTERN.match(idx):
627+                            d = d[idx]
628+                        else:
629+                            try:
630+                                n = int(idx) # try as number first (most likely)
631+                                d = d[n]
632+                            except TypeError:
633+                                d = d[idx]
634+                if m:
635+                    rest = rest[m.end():]
636+                else:
637+                    raise ValueError('Unable to convert '
638+                                     '%r at %r' % (value, rest))
639+        #rest should be empty
640+        return d
641+
642+    def convert(self, value):
643+        """
644+        Convert values to an appropriate type. dicts, lists and tuples are
645+        replaced by their converting alternatives. Strings are checked to
646+        see if they have a conversion format and are converted if they do.
647+        """
648+        if not isinstance(value, ConvertingDict) and isinstance(value, dict):
649+            value = ConvertingDict(value)
650+            value.configurator = self
651+        elif not isinstance(value, ConvertingList) and isinstance(value, list):
652+            value = ConvertingList(value)
653+            value.configurator = self
654+        elif not isinstance(value, ConvertingTuple) and\
655+                 isinstance(value, tuple):
656+            value = ConvertingTuple(value)
657+            value.configurator = self
658+        elif isinstance(value, basestring): # str for py3k
659+            m = self.CONVERT_PATTERN.match(value)
660+            if m:
661+                d = m.groupdict()
662+                prefix = d['prefix']
663+                converter = self.value_converters.get(prefix, None)
664+                if converter:
665+                    suffix = d['suffix']
666+                    converter = getattr(self, converter)
667+                    value = converter(suffix)
668+        return value
669+
670+    def configure_custom(self, config):
671+        """Configure an object with a user-supplied factory."""
672+        c = config.pop('()')
673+        if not hasattr(c, '__call__') and hasattr(types, 'ClassType') and type(c) != types.ClassType:
674+            c = self.resolve(c)
675+        props = config.pop('.', None)
676+        # Check for valid identifiers
677+        kwargs = dict([(k, config[k]) for k in config if valid_ident(k)])
678+        result = c(**kwargs)
679+        if props:
680+            for name, value in props.items():
681+                setattr(result, name, value)
682+        return result
683+
684+    def as_tuple(self, value):
685+        """Utility function which converts lists to tuples."""
686+        if isinstance(value, list):
687+            value = tuple(value)
688+        return value
689+
690+class DictConfigurator(BaseConfigurator):
691+    """
692+    Configure logging using a dictionary-like object to describe the
693+    configuration.
694+    """
695+
696+    def configure(self):
697+        """Do the configuration."""
698+
699+        config = self.config
700+        if 'version' not in config:
701+            raise ValueError("dictionary doesn't specify a version")
702+        if config['version'] != 1:
703+            raise ValueError("Unsupported version: %s" % config['version'])
704+        incremental = config.pop('incremental', False)
705+        EMPTY_DICT = {}
706+        logging._acquireLock()
707+        try:
708+            if incremental:
709+                handlers = config.get('handlers', EMPTY_DICT)
710+                # incremental handler config only if handler name
711+                # ties in to logging._handlers (Python 2.7)
712+                if sys.version_info[:2] == (2, 7):
713+                    for name in handlers:
714+                        if name not in logging._handlers:
715+                            raise ValueError('No handler found with '
716+                                             'name %r'  % name)
717+                        else:
718+                            try:
719+                                handler = logging._handlers[name]
720+                                handler_config = handlers[name]
721+                                level = handler_config.get('level', None)
722+                                if level:
723+                                    handler.setLevel(_checkLevel(level))
724+                            except StandardError, e:
725+                                raise ValueError('Unable to configure handler '
726+                                                 '%r: %s' % (name, e))
727+                loggers = config.get('loggers', EMPTY_DICT)
728+                for name in loggers:
729+                    try:
730+                        self.configure_logger(name, loggers[name], True)
731+                    except StandardError, e:
732+                        raise ValueError('Unable to configure logger '
733+                                         '%r: %s' % (name, e))
734+                root = config.get('root', None)
735+                if root:
736+                    try:
737+                        self.configure_root(root, True)
738+                    except StandardError, e:
739+                        raise ValueError('Unable to configure root '
740+                                         'logger: %s' % e)
741+            else:
742+                disable_existing = config.pop('disable_existing_loggers', True)
743+
744+                logging._handlers.clear()
745+                del logging._handlerList[:]
746+
747+                # Do formatters first - they don't refer to anything else
748+                formatters = config.get('formatters', EMPTY_DICT)
749+                for name in formatters:
750+                    try:
751+                        formatters[name] = self.configure_formatter(
752+                                                            formatters[name])
753+                    except StandardError, e:
754+                        raise ValueError('Unable to configure '
755+                                         'formatter %r: %s' % (name, e))
756+                # Next, do filters - they don't refer to anything else, either
757+                filters = config.get('filters', EMPTY_DICT)
758+                for name in filters:
759+                    try:
760+                        filters[name] = self.configure_filter(filters[name])
761+                    except StandardError, e:
762+                        raise ValueError('Unable to configure '
763+                                         'filter %r: %s' % (name, e))
764+
765+                # Next, do handlers - they refer to formatters and filters
766+                # As handlers can refer to other handlers, sort the keys
767+                # to allow a deterministic order of configuration
768+                handlers = config.get('handlers', EMPTY_DICT)
769+                for name in sorted(handlers):
770+                    try:
771+                        handler = self.configure_handler(handlers[name])
772+                        handler.name = name
773+                        handlers[name] = handler
774+                    except StandardError, e:
775+                        raise ValueError('Unable to configure handler '
776+                                         '%r: %s' % (name, e))
777+                # Next, do loggers - they refer to handlers and filters
778+
779+                #we don't want to lose the existing loggers,
780+                #since other threads may have pointers to them.
781+                #existing is set to contain all existing loggers,
782+                #and as we go through the new configuration we
783+                #remove any which are configured. At the end,
784+                #what's left in existing is the set of loggers
785+                #which were in the previous configuration but
786+                #which are not in the new configuration.
787+                root = logging.root
788+                existing = root.manager.loggerDict.keys()
789+                #The list needs to be sorted so that we can
790+                #avoid disabling child loggers of explicitly
791+                #named loggers. With a sorted list it is easier
792+                #to find the child loggers.
793+                existing.sort()
794+                #We'll keep the list of existing loggers
795+                #which are children of named loggers here...
796+                child_loggers = []
797+                #now set up the new ones...
798+                loggers = config.get('loggers', EMPTY_DICT)
799+                for name in loggers:
800+                    if name in existing:
801+                        i = existing.index(name)
802+                        prefixed = name + "."
803+                        pflen = len(prefixed)
804+                        num_existing = len(existing)
805+                        i = i + 1 # look at the entry after name
806+                        while (i < num_existing) and\
807+                              (existing[i][:pflen] == prefixed):
808+                            child_loggers.append(existing[i])
809+                            i = i + 1
810+                        existing.remove(name)
811+                    try:
812+                        self.configure_logger(name, loggers[name])
813+                    except StandardError, e:
814+                        raise ValueError('Unable to configure logger '
815+                                         '%r: %s' % (name, e))
816+
817+                #Disable any old loggers. There's no point deleting
818+                #them as other threads may continue to hold references
819+                #and by disabling them, you stop them doing any logging.
820+                #However, don't disable children of named loggers, as that's
821+                #probably not what was intended by the user.
822+                for log in existing:
823+                    logger = root.manager.loggerDict[log]
824+                    if log in child_loggers:
825+                        logger.level = logging.NOTSET
826+                        logger.handlers = []
827+                        logger.propagate = True
828+                    elif disable_existing:
829+                        logger.disabled = True
830+
831+                # And finally, do the root logger
832+                root = config.get('root', None)
833+                if root:
834+                    try:
835+                        self.configure_root(root)
836+                    except StandardError, e:
837+                        raise ValueError('Unable to configure root '
838+                                         'logger: %s' % e)
839+        finally:
840+            logging._releaseLock()
841+
842+    def configure_formatter(self, config):
843+        """Configure a formatter from a dictionary."""
844+        if '()' in config:
845+            factory = config['()'] # for use in exception handler
846+            try:
847+                result = self.configure_custom(config)
848+            except TypeError, te:
849+                if "'format'" not in str(te):
850+                    raise
851+                #Name of parameter changed from fmt to format.
852+                #Retry with old name.
853+                #This is so that code can be used with older Python versions
854+                #(e.g. by Django)
855+                config['fmt'] = config.pop('format')
856+                config['()'] = factory
857+                result = self.configure_custom(config)
858+        else:
859+            fmt = config.get('format', None)
860+            dfmt = config.get('datefmt', None)
861+            result = logging.Formatter(fmt, dfmt)
862+        return result
863+
864+    def configure_filter(self, config):
865+        """Configure a filter from a dictionary."""
866+        if '()' in config:
867+            result = self.configure_custom(config)
868+        else:
869+            name = config.get('name', '')
870+            result = logging.Filter(name)
871+        return result
872+
873+    def add_filters(self, filterer, filters):
874+        """Add filters to a filterer from a list of names."""
875+        for f in filters:
876+            try:
877+                filterer.addFilter(self.config['filters'][f])
878+            except StandardError, e:
879+                raise ValueError('Unable to add filter %r: %s' % (f, e))
880+
881+    def configure_handler(self, config):
882+        """Configure a handler from a dictionary."""
883+        formatter = config.pop('formatter', None)
884+        if formatter:
885+            try:
886+                formatter = self.config['formatters'][formatter]
887+            except StandardError, e:
888+                raise ValueError('Unable to set formatter '
889+                                 '%r: %s' % (formatter, e))
890+        level = config.pop('level', None)
891+        filters = config.pop('filters', None)
892+        if '()' in config:
893+            c = config.pop('()')
894+            if not hasattr(c, '__call__') and hasattr(types, 'ClassType') and type(c) != types.ClassType:
895+                c = self.resolve(c)
896+            factory = c
897+        else:
898+            klass = self.resolve(config.pop('class'))
899+            #Special case for handler which refers to another handler
900+            if issubclass(klass, logging.handlers.MemoryHandler) and\
901+                'target' in config:
902+                try:
903+                    config['target'] = self.config['handlers'][config['target']]
904+                except StandardError, e:
905+                    raise ValueError('Unable to set target handler '
906+                                     '%r: %s' % (config['target'], e))
907+            elif issubclass(klass, logging.handlers.SMTPHandler) and\
908+                'mailhost' in config:
909+                config['mailhost'] = self.as_tuple(config['mailhost'])
910+            elif issubclass(klass, logging.handlers.SysLogHandler) and\
911+                'address' in config:
912+                config['address'] = self.as_tuple(config['address'])
913+            factory = klass
914+        kwargs = dict([(k, config[k]) for k in config if valid_ident(k)])
915+        try:
916+            result = factory(**kwargs)
917+        except TypeError, te:
918+            if "'stream'" not in str(te):
919+                raise
920+            #The argument name changed from strm to stream
921+            #Retry with old name.
922+            #This is so that code can be used with older Python versions
923+            #(e.g. by Django)
924+            kwargs['strm'] = kwargs.pop('stream')
925+            result = factory(**kwargs)
926+        if formatter:
927+            result.setFormatter(formatter)
928+        if level is not None:
929+            result.setLevel(_checkLevel(level))
930+        if filters:
931+            self.add_filters(result, filters)
932+        return result
933+
934+    def add_handlers(self, logger, handlers):
935+        """Add handlers to a logger from a list of names."""
936+        for h in handlers:
937+            try:
938+                logger.addHandler(self.config['handlers'][h])
939+            except StandardError, e:
940+                raise ValueError('Unable to add handler %r: %s' % (h, e))
941+
942+    def common_logger_config(self, logger, config, incremental=False):
943+        """
944+        Perform configuration which is common to root and non-root loggers.
945+        """
946+        level = config.get('level', None)
947+        if level is not None:
948+            logger.setLevel(_checkLevel(level))
949+        if not incremental:
950+            #Remove any existing handlers
951+            for h in logger.handlers[:]:
952+                logger.removeHandler(h)
953+            handlers = config.get('handlers', None)
954+            if handlers:
955+                self.add_handlers(logger, handlers)
956+            filters = config.get('filters', None)
957+            if filters:
958+                self.add_filters(logger, filters)
959+
960+    def configure_logger(self, name, config, incremental=False):
961+        """Configure a non-root logger from a dictionary."""
962+        logger = logging.getLogger(name)
963+        self.common_logger_config(logger, config, incremental)
964+        propagate = config.get('propagate', None)
965+        if propagate is not None:
966+            logger.propagate = propagate
967+
968+    def configure_root(self, config, incremental=False):
969+        """Configure a root logger from a dictionary."""
970+        root = logging.getLogger()
971+        self.common_logger_config(root, config, incremental)
972+
973+dictConfigClass = DictConfigurator
974+
975+def dictConfig(config):
976+    """Configure logging using a dictionary."""
977+    dictConfigClass(config).configure()
978diff -r e943e3ecbf20 django/utils/log.py
979--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
980+++ b/django/utils/log.py       Sat Sep 25 13:53:31 2010 +0800
981@@ -0,0 +1,52 @@
982+import logging
983+from django.core import mail
984+
985+# Make sure a NullHandler is available
986+# This was added in Python 2.7/3.2
987+try:
988+    from logging import NullHandler
989+except ImportError:
990+    class NullHandler(logging.Handler):
991+        def emit(self, record):
992+            pass
993+
994+# Make sure that dictConfig is available
995+# This was added in Python 2.7/3.2
996+try:
997+    from logging.config import dictConfig
998+except ImportError:
999+    from django.utils.dictconfig import dictConfig
1000+
1001+# Ensure the creation of the Django logger
1002+logger = logging.getLogger('django')
1003+
1004+
1005+class AdminEmailHandler(logging.Handler):
1006+    """An exception log handler that emails log entries to site admins
1007+
1008+    If the request is passed as the first argument to the log record,
1009+    request data will be provided in the
1010+    """
1011+    def emit(self, record):
1012+        import traceback
1013+        from django.conf import settings
1014+
1015+        try:
1016+            request = record.request
1017+
1018+            subject = 'Error (%s IP): %s' % (
1019+                (request.META.get('REMOTE_ADDR') in settings.INTERNAL_IPS and 'internal' or 'EXTERNAL'),
1020+                request.path
1021+            )
1022+            request_repr = repr(request)
1023+        except:
1024+            subject = 'Error: Unknown URL'
1025+            request_repr = "Request repr() unavailable"
1026+
1027+        if record.exc_info:
1028+            stack_trace = '\n'.join(traceback.format_exception(*record.exc_info))
1029+        else:
1030+            stack_trace = 'No stack trace available'
1031+
1032+        message = "%s\n\n%s" % (stack_trace, request_repr)
1033+        mail.mail_admins(subject, message, fail_silently=True)
1034diff -r e943e3ecbf20 django/views/decorators/http.py
1035--- a/django/views/decorators/http.py   Tue Sep 21 19:32:22 2010 +0000
1036+++ b/django/views/decorators/http.py   Sat Sep 25 13:53:31 2010 +0800
1037@@ -10,14 +10,17 @@
1038 from calendar import timegm
1039 from datetime import timedelta
1040 from email.Utils import formatdate
1041+import logging
1042 
1043 from django.utils.decorators import decorator_from_middleware, available_attrs
1044 from django.utils.http import parse_etags, quote_etag
1045 from django.middleware.http import ConditionalGetMiddleware
1046 from django.http import HttpResponseNotAllowed, HttpResponseNotModified, HttpResponse
1047 
1048+conditional_page = decorator_from_middleware(ConditionalGetMiddleware)
1049 
1050-conditional_page = decorator_from_middleware(ConditionalGetMiddleware)
1051+logger = logging.getLogger('django.request')
1052+
1053 
1054 def require_http_methods(request_method_list):
1055     """
1056@@ -33,6 +36,9 @@
1057     def decorator(func):
1058         def inner(request, *args, **kwargs):
1059             if request.method not in request_method_list:
1060+                logger.warning('405 Method Not Allowed (%s): %s' % (request.method, request.path),
1061+                    extra={'request': request}
1062+                )
1063                 return HttpResponseNotAllowed(request_method_list)
1064             return func(request, *args, **kwargs)
1065         return wraps(func, assigned=available_attrs(func))(inner)
1066@@ -111,9 +117,15 @@
1067                     if request.method in ("GET", "HEAD"):
1068                         response = HttpResponseNotModified()
1069                     else:
1070+                        logger.warning('412 Precondition Failed: %s' % request.path,
1071+                            extra={'request': request}
1072+                        )
1073                         response = HttpResponse(status=412)
1074                 elif if_match and ((not res_etag and "*" in etags) or
1075                         (res_etag and res_etag not in etags)):
1076+                    logger.warning('412 Precondition Failed: %s' % request.path,
1077+                        extra={'request': request}
1078+                    )
1079                     response = HttpResponse(status=412)
1080                 elif (not if_none_match and if_modified_since and
1081                         request.method == "GET" and
1082diff -r e943e3ecbf20 django/views/generic/simple.py
1083--- a/django/views/generic/simple.py    Tue Sep 21 19:32:22 2010 +0000
1084+++ b/django/views/generic/simple.py    Sat Sep 25 13:53:31 2010 +0800
1085@@ -1,6 +1,11 @@
1086+import logging
1087+
1088 from django.template import loader, RequestContext
1089 from django.http import HttpResponse, HttpResponseRedirect, HttpResponsePermanentRedirect, HttpResponseGone
1090 
1091+logger = logging.getLogger('django.request')
1092+
1093+
1094 def direct_to_template(request, template, extra_context=None, mimetype=None, **kwargs):
1095     """
1096     Render a given template with any extra URL parameters in the context as
1097@@ -46,4 +51,5 @@
1098         klass = permanent and HttpResponsePermanentRedirect or HttpResponseRedirect
1099         return klass(url % kwargs)
1100     else:
1101+        logger.warning('410 Gone: %s' % request.path, extra={'request': request})
1102         return HttpResponseGone()