summaryrefslogtreecommitdiff
path: root/django
diff options
context:
space:
mode:
authorRussell Keith-Magee <russell@keith-magee.com>2010-10-04 15:12:39 +0000
committerRussell Keith-Magee <russell@keith-magee.com>2010-10-04 15:12:39 +0000
commit24acca413977422681ca16b42fe9a6d763df2121 (patch)
tree0d3e1df0b023581725107984b5d6c57544bba7a0 /django
parent667d832e901ca6bb394054109e24a2ed6cadc563 (diff)
Fixed #12012 -- Added support for logging. Thanks to Vinay Sajip for his draft patch, and to the many people who gave feedback during development of the patch.
git-svn-id: http://code.djangoproject.com/svn/django/trunk@13981 bcc190cf-cafb-0310-a4f2-bffc1f526a37
Diffstat (limited to 'django')
-rw-r--r--django/conf/__init__.py11
-rw-r--r--django/conf/global_settings.py28
-rw-r--r--django/conf/project_template/settings.py23
-rw-r--r--django/core/handlers/base.py36
-rw-r--r--django/core/handlers/modpython.py12
-rw-r--r--django/core/handlers/wsgi.py14
-rw-r--r--django/db/backends/util.py15
-rw-r--r--django/middleware/common.py10
-rw-r--r--django/middleware/csrf.py37
-rw-r--r--django/utils/dictconfig.py553
-rw-r--r--django/utils/log.py56
-rw-r--r--django/views/decorators/http.py23
-rw-r--r--django/views/generic/simple.py10
13 files changed, 808 insertions, 20 deletions
diff --git a/django/conf/__init__.py b/django/conf/__init__.py
index 20d1aa1c86..8a28bc7075 100644
--- a/django/conf/__init__.py
+++ b/django/conf/__init__.py
@@ -16,6 +16,7 @@ from django.utils import importlib
ENVIRONMENT_VARIABLE = "DJANGO_SETTINGS_MODULE"
+
class LazySettings(LazyObject):
"""
A lazy proxy for either global Django settings or a custom settings object.
@@ -114,6 +115,16 @@ class Settings(object):
os.environ['TZ'] = self.TIME_ZONE
time.tzset()
+ # Settings are configured, so we can set up the logger if required
+ if self.LOGGING_CONFIG:
+ # First find the logging configuration function ...
+ logging_config_path, logging_config_func_name = self.LOGGING_CONFIG.rsplit('.', 1)
+ logging_config_module = importlib.import_module(logging_config_path)
+ logging_config_func = getattr(logging_config_module, logging_config_func_name)
+
+ # ... then invoke it with the logging settings
+ logging_config_func(self.LOGGING)
+
class UserSettingsHolder(object):
"""
Holder for user configured settings.
diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py
index 2714bfb9ab..cd85ce0641 100644
--- a/django/conf/global_settings.py
+++ b/django/conf/global_settings.py
@@ -499,6 +499,34 @@ MESSAGE_STORAGE = 'django.contrib.messages.storage.user_messages.LegacyFallbackS
# django.contrib.messages to avoid imports in this settings file.
###########
+# LOGGING #
+###########
+
+# The callable to use to configure logging
+LOGGING_CONFIG = 'django.utils.log.dictConfig'
+
+# The default logging configuration. This sends an email to
+# the site admins on every HTTP 500 error. All other log
+# records are sent to the bit bucket.
+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,
+ },
+ }
+}
+
+###########
# TESTING #
###########
diff --git a/django/conf/project_template/settings.py b/django/conf/project_template/settings.py
index c49df24ce5..3c783d4565 100644
--- a/django/conf/project_template/settings.py
+++ b/django/conf/project_template/settings.py
@@ -94,3 +94,26 @@ INSTALLED_APPS = (
# Uncomment the next line to enable admin documentation:
# 'django.contrib.admindocs',
)
+
+# A sample logging configuration. The only tangible logging
+# performed by this configuration is to send an email to
+# the site admins on every HTTP 500 error.
+# See http://docs.djangoproject.com/en/dev/topics/logging for
+# more details on how to customize your logging configuration.
+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,
+ },
+ }
+}
diff --git a/django/core/handlers/base.py b/django/core/handlers/base.py
index b03c2fd71e..a0aecbfdd3 100644
--- a/django/core/handlers/base.py
+++ b/django/core/handlers/base.py
@@ -1,3 +1,4 @@
+import logging
import sys
from django import http
@@ -5,6 +6,9 @@ from django.core import signals
from django.utils.encoding import force_unicode
from django.utils.importlib import import_module
+logger = logging.getLogger('django.request')
+
+
class BaseHandler(object):
# Changes that are always applied to a response (in this order).
response_fixes = [
@@ -118,6 +122,11 @@ class BaseHandler(object):
return response
except http.Http404, e:
+ logger.warning('Not Found: %s' % request.path,
+ extra={
+ 'status_code': 404,
+ 'request': request
+ })
if settings.DEBUG:
from django.views import debug
return debug.technical_404_response(request, e)
@@ -131,6 +140,11 @@ class BaseHandler(object):
finally:
receivers = signals.got_request_exception.send(sender=self.__class__, request=request)
except exceptions.PermissionDenied:
+ logger.warning('Forbidden (Permission denied): %s' % request.path,
+ extra={
+ 'status_code': 403,
+ 'request': request
+ })
return http.HttpResponseForbidden('<h1>Permission denied</h1>')
except SystemExit:
# Allow sys.exit() to actually exit. See tickets #1023 and #4701
@@ -155,7 +169,6 @@ class BaseHandler(object):
available would be an error.
"""
from django.conf import settings
- from django.core.mail import mail_admins
if settings.DEBUG_PROPAGATE_EXCEPTIONS:
raise
@@ -164,14 +177,14 @@ class BaseHandler(object):
from django.views import debug
return debug.technical_500_response(request, *exc_info)
- # When DEBUG is False, send an error message to the admins.
- subject = 'Error (%s IP): %s' % ((request.META.get('REMOTE_ADDR') in settings.INTERNAL_IPS and 'internal' or 'EXTERNAL'), request.path)
- try:
- request_repr = repr(request)
- except:
- request_repr = "Request repr() unavailable"
- message = "%s\n\n%s" % (self._get_traceback(exc_info), request_repr)
- mail_admins(subject, message, fail_silently=True)
+ logger.error('Internal Server Error: %s' % request.path,
+ exc_info=exc_info,
+ extra={
+ 'status_code': 500,
+ 'request':request
+ }
+ )
+
# If Http500 handler is not installed, re-raise last exception
if resolver.urlconf_module is None:
raise exc_info[1], None, exc_info[2]
@@ -179,11 +192,6 @@ class BaseHandler(object):
callback, param_dict = resolver.resolve500()
return callback(request, **param_dict)
- def _get_traceback(self, exc_info=None):
- "Helper function to return the traceback as a string"
- import traceback
- return '\n'.join(traceback.format_exception(*(exc_info or sys.exc_info())))
-
def apply_response_fixes(self, request, response):
"""
Applies each of the functions in self.response_fixes to the request and
diff --git a/django/core/handlers/modpython.py b/django/core/handlers/modpython.py
index 17e739600c..395ec65314 100644
--- a/django/core/handlers/modpython.py
+++ b/django/core/handlers/modpython.py
@@ -1,5 +1,7 @@
+import logging
import os
from pprint import pformat
+import sys
from warnings import warn
from django import http
@@ -9,6 +11,9 @@ from django.core.urlresolvers import set_script_prefix
from django.utils import datastructures
from django.utils.encoding import force_unicode, smart_str, iri_to_uri
+logger = logging.getLogger('django.request')
+
+
# NOTE: do *not* import settings (or any module which eventually imports
# settings) until after ModPythonHandler has been called; otherwise os.environ
# won't be set up correctly (with respect to settings).
@@ -200,6 +205,13 @@ class ModPythonHandler(BaseHandler):
try:
request = self.request_class(req)
except UnicodeDecodeError:
+ logger.warning('Bad Request (UnicodeDecodeError): %s' % request.path,
+ exc_info=sys.exc_info(),
+ extra={
+ 'status_code': 400,
+ 'request': request
+ }
+ )
response = http.HttpResponseBadRequest()
else:
response = self.get_response(request)
diff --git a/django/core/handlers/wsgi.py b/django/core/handlers/wsgi.py
index 927b098815..0978be0d6d 100644
--- a/django/core/handlers/wsgi.py
+++ b/django/core/handlers/wsgi.py
@@ -1,5 +1,7 @@
-from threading import Lock
+import logging
from pprint import pformat
+import sys
+from threading import Lock
try:
from cStringIO import StringIO
except ImportError:
@@ -12,6 +14,9 @@ from django.core.urlresolvers import set_script_prefix
from django.utils import datastructures
from django.utils.encoding import force_unicode, iri_to_uri
+logger = logging.getLogger('django.request')
+
+
# See http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
STATUS_CODE_TEXT = {
100: 'CONTINUE',
@@ -236,6 +241,13 @@ class WSGIHandler(base.BaseHandler):
try:
request = self.request_class(environ)
except UnicodeDecodeError:
+ logger.warning('Bad Request (UnicodeDecodeError): %s' % request.path,
+ exc_info=sys.exc_info(),
+ extra={
+ 'status_code': 400,
+ 'request': request
+ }
+ )
response = http.HttpResponseBadRequest()
else:
response = self.get_response(request)
diff --git a/django/db/backends/util.py b/django/db/backends/util.py
index 6f18c53fe3..fb5162dda1 100644
--- a/django/db/backends/util.py
+++ b/django/db/backends/util.py
@@ -1,9 +1,12 @@
import datetime
import decimal
+import logging
from time import time
from django.utils.hashcompat import md5_constructor
+logger = logging.getLogger('django.db.backends')
+
class CursorDebugWrapper(object):
def __init__(self, cursor, db):
self.cursor = cursor
@@ -15,11 +18,15 @@ class CursorDebugWrapper(object):
return self.cursor.execute(sql, params)
finally:
stop = time()
+ duration = stop - start
sql = self.db.ops.last_executed_query(self.cursor, sql, params)
self.db.queries.append({
'sql': sql,
- 'time': "%.3f" % (stop - start),
+ 'time': "%.3f" % duration,
})
+ logger.debug('(%.3f) %s; args=%s' % (duration, sql, params),
+ extra={'duration':duration, 'sql':sql, 'params':params}
+ )
def executemany(self, sql, param_list):
start = time()
@@ -27,10 +34,14 @@ class CursorDebugWrapper(object):
return self.cursor.executemany(sql, param_list)
finally:
stop = time()
+ duration = stop - start
self.db.queries.append({
'sql': '%s times: %s' % (len(param_list), sql),
- 'time': "%.3f" % (stop - start),
+ 'time': "%.3f" % duration,
})
+ logger.debug('(%.3f) %s; args=%s' % (duration, sql, param_list),
+ extra={'duration':duration, 'sql':sql, 'params':param_list}
+ )
def __getattr__(self, attr):
if attr in self.__dict__:
diff --git a/django/middleware/common.py b/django/middleware/common.py
index 309058870a..60af804925 100644
--- a/django/middleware/common.py
+++ b/django/middleware/common.py
@@ -1,3 +1,4 @@
+import logging
import re
from django.conf import settings
@@ -7,6 +8,9 @@ from django.utils.http import urlquote
from django.core import urlresolvers
from django.utils.hashcompat import md5_constructor
+logger = logging.getLogger('django.request')
+
+
class CommonMiddleware(object):
"""
"Common" middleware for taking care of some basic operations:
@@ -38,6 +42,12 @@ class CommonMiddleware(object):
if 'HTTP_USER_AGENT' in request.META:
for user_agent_regex in settings.DISALLOWED_USER_AGENTS:
if user_agent_regex.search(request.META['HTTP_USER_AGENT']):
+ logger.warning('Forbidden (User agent): %s' % request.path,
+ extra={
+ 'status_code': 403,
+ 'request': request
+ }
+ )
return http.HttpResponseForbidden('<h1>Forbidden</h1>')
# Check for a redirect based on settings.APPEND_SLASH
diff --git a/django/middleware/csrf.py b/django/middleware/csrf.py
index 5d3a871adb..ed2a4f0e98 100644
--- a/django/middleware/csrf.py
+++ b/django/middleware/csrf.py
@@ -6,6 +6,7 @@ against request forgeries from other sites.
"""
import itertools
+import logging
import re
import random
@@ -20,6 +21,8 @@ _POST_FORM_RE = \
_HTML_TYPES = ('text/html', 'application/xhtml+xml')
+logger = logging.getLogger('django.request')
+
# Use the system (hardware-based) random number generator if it exists.
if hasattr(random, 'SystemRandom'):
randrange = random.SystemRandom().randrange
@@ -169,14 +172,26 @@ class CsrfViewMiddleware(object):
# we can use strict Referer checking.
referer = request.META.get('HTTP_REFERER')
if referer is None:
+ logger.warning('Forbidden (%s): %s' % (REASON_NO_COOKIE, request.path),
+ extra={
+ 'status_code': 403,
+ 'request': request,
+ }
+ )
return reject(REASON_NO_REFERER)
# The following check ensures that the referer is HTTPS,
# the domains match and the ports match - the same origin policy.
good_referer = 'https://%s/' % request.get_host()
if not referer.startswith(good_referer):
- return reject(REASON_BAD_REFERER %
- (referer, good_referer))
+ reason = REASON_BAD_REFERER % (referer, good_referer)
+ logger.warning('Forbidden (%s): %s' % (reason, request.path),
+ extra={
+ 'status_code': 403,
+ 'request': request,
+ }
+ )
+ return reject(reason)
# If the user didn't already have a CSRF cookie, then fall back to
# the Django 1.1 method (hash of session ID), so a request is not
@@ -190,6 +205,12 @@ class CsrfViewMiddleware(object):
# No CSRF cookie and no session cookie. For POST requests,
# we insist on a CSRF cookie, and in this way we can avoid
# all CSRF attacks, including login CSRF.
+ logger.warning('Forbidden (%s): %s' % (REASON_NO_COOKIE, request.path),
+ extra={
+ 'status_code': 403,
+ 'request': request,
+ }
+ )
return reject(REASON_NO_COOKIE)
else:
csrf_token = request.META["CSRF_COOKIE"]
@@ -199,8 +220,20 @@ class CsrfViewMiddleware(object):
if request_csrf_token != csrf_token:
if cookie_is_new:
# probably a problem setting the CSRF cookie
+ logger.warning('Forbidden (%s): %s' % (REASON_NO_CSRF_COOKIE, request.path),
+ extra={
+ 'status_code': 403,
+ 'request': request,
+ }
+ )
return reject(REASON_NO_CSRF_COOKIE)
else:
+ logger.warning('Forbidden (%s): %s' % (REASON_BAD_TOKEN, request.path),
+ extra={
+ 'status_code': 403,
+ 'request': request,
+ }
+ )
return reject(REASON_BAD_TOKEN)
return accept()
diff --git a/django/utils/dictconfig.py b/django/utils/dictconfig.py
new file mode 100644
index 0000000000..42fbd9393a
--- /dev/null
+++ b/django/utils/dictconfig.py
@@ -0,0 +1,553 @@
+# This is a copy of the Python logging.config.dictconfig module,
+# reproduced with permission. It is provided here for backwards
+# compatibility for Python versions prior to 2.7.
+#
+# Copyright 2009-2010 by Vinay Sajip. All Rights Reserved.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose and without fee is hereby granted,
+# provided that the above copyright notice appear in all copies and that
+# both that copyright notice and this permission notice appear in
+# supporting documentation, and that the name of Vinay Sajip
+# not be used in advertising or publicity pertaining to distribution
+# of the software without specific, written prior permission.
+# VINAY SAJIP DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING
+# ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
+# VINAY SAJIP BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR
+# ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER
+# IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import logging.handlers
+import re
+import sys
+import types
+
+IDENTIFIER = re.compile('^[a-z_][a-z0-9_]*$', re.I)
+
+def valid_ident(s):
+ m = IDENTIFIER.match(s)
+ if not m:
+ raise ValueError('Not a valid Python identifier: %r' % s)
+ return True
+
+#
+# This function is defined in logging only in recent versions of Python
+#
+try:
+ from logging import _checkLevel
+except ImportError:
+ def _checkLevel(level):
+ if isinstance(level, int):
+ rv = level
+ elif str(level) == level:
+ if level not in logging._levelNames:
+ raise ValueError('Unknown level: %r' % level)
+ rv = logging._levelNames[level]
+ else:
+ raise TypeError('Level not an integer or a '
+ 'valid string: %r' % level)
+ return rv
+
+# The ConvertingXXX classes are wrappers around standard Python containers,
+# and they serve to convert any suitable values in the container. The
+# conversion converts base dicts, lists and tuples to their wrapped
+# equivalents, whereas strings which match a conversion format are converted
+# appropriately.
+#
+# Each wrapper should have a configurator attribute holding the actual
+# configurator to use for conversion.
+
+class ConvertingDict(dict):
+ """A converting dictionary wrapper."""
+
+ def __getitem__(self, key):
+ value = dict.__getitem__(self, key)
+ result = self.configurator.convert(value)
+ #If the converted value is different, save for next time
+ if value is not result:
+ self[key] = result
+ if type(result) in (ConvertingDict, ConvertingList,
+ ConvertingTuple):
+ result.parent = self
+ result.key = key
+ return result
+
+ def get(self, key, default=None):
+ value = dict.get(self, key, default)
+ result = self.configurator.convert(value)
+ #If the converted value is different, save for next time
+ if value is not result:
+ self[key] = result
+ if type(result) in (ConvertingDict, ConvertingList,
+ ConvertingTuple):
+ result.parent = self
+ result.key = key
+ return result
+
+ def pop(self, key, default=None):
+ value = dict.pop(self, key, default)
+ result = self.configurator.convert(value)
+ if value is not result:
+ if type(result) in (ConvertingDict, ConvertingList,
+ ConvertingTuple):
+ result.parent = self
+ result.key = key
+ return result
+
+class ConvertingList(list):
+ """A converting list wrapper."""
+ def __getitem__(self, key):
+ value = list.__getitem__(self, key)
+ result = self.configurator.convert(value)
+ #If the converted value is different, save for next time
+ if value is not result:
+ self[key] = result
+ if type(result) in (ConvertingDict, ConvertingList,
+ ConvertingTuple):
+ result.parent = self
+ result.key = key
+ return result
+
+ def pop(self, idx=-1):
+ value = list.pop(self, idx)
+ result = self.configurator.convert(value)
+ if value is not result:
+ if type(result) in (ConvertingDict, ConvertingList,
+ ConvertingTuple):
+ result.parent = self
+ return result
+
+class ConvertingTuple(tuple):
+ """A converting tuple wrapper."""
+ def __getitem__(self, key):
+ value = tuple.__getitem__(self, key)
+ result = self.configurator.convert(value)
+ if value is not result:
+ if type(result) in (ConvertingDict, ConvertingList,
+ ConvertingTuple):
+ result.parent = self
+ result.key = key
+ return result
+
+class BaseConfigurator(object):
+ """
+ The configurator base class which defines some useful defaults.
+ """
+
+ CONVERT_PATTERN = re.compile(r'^(?P<prefix>[a-z]+)://(?P<suffix>.*)$')
+
+ WORD_PATTERN = re.compile(r'^\s*(\w+)\s*')
+ DOT_PATTERN = re.compile(r'^\.\s*(\w+)\s*')
+ INDEX_PATTERN = re.compile(r'^\[\s*(\w+)\s*\]\s*')
+ DIGIT_PATTERN = re.compile(r'^\d+$')
+
+ value_converters = {
+ 'ext' : 'ext_convert',
+ 'cfg' : 'cfg_convert',
+ }
+
+ # We might want to use a different one, e.g. importlib
+ importer = __import__
+
+ def __init__(self, config):
+ self.config = ConvertingDict(config)
+ self.config.configurator = self
+
+ def resolve(self, s):
+ """
+ Resolve strings to objects using standard import and attribute
+ syntax.
+ """
+ name = s.split('.')
+ used = name.pop(0)
+ try:
+ found = self.importer(used)
+ for frag in name:
+ used += '.' + frag
+ try:
+ found = getattr(found, frag)
+ except AttributeError:
+ self.importer(used)
+ found = getattr(found, frag)
+ return found
+ except ImportError:
+ e, tb = sys.exc_info()[1:]
+ v = ValueError('Cannot resolve %r: %s' % (s, e))
+ v.__cause__, v.__traceback__ = e, tb
+ raise v
+
+ def ext_convert(self, value):
+ """Default converter for the ext:// protocol."""
+ return self.resolve(value)
+
+ def cfg_convert(self, value):
+ """Default converter for the cfg:// protocol."""
+ rest = value
+ m = self.WORD_PATTERN.match(rest)
+ if m is None:
+ raise ValueError("Unable to convert %r" % value)
+ else:
+ rest = rest[m.end():]
+ d = self.config[m.groups()[0]]
+ #print d, rest
+ while rest:
+ m = self.DOT_PATTERN.match(rest)
+ if m:
+ d = d[m.groups()[0]]
+ else:
+ m = self.INDEX_PATTERN.match(rest)
+ if m:
+ idx = m.groups()[0]
+ if not self.DIGIT_PATTERN.match(idx):
+ d = d[idx]
+ else:
+ try:
+ n = int(idx) # try as number first (most likely)
+ d = d[n]
+ except TypeError:
+ d = d[idx]
+ if m:
+ rest = rest[m.end():]
+ else:
+ raise ValueError('Unable to convert '
+ '%r at %r' % (value, rest))
+ #rest should be empty
+ return d
+
+ def convert(self, value):
+ """
+ Convert values to an appropriate type. dicts, lists and tuples are
+ replaced by their converting alternatives. Strings are checked to
+ see if they have a conversion format and are converted if they do.
+ """
+ if not isinstance(value, ConvertingDict) and isinstance(value, dict):
+ value = ConvertingDict(value)
+ value.configurator = self
+ elif not isinstance(value, ConvertingList) and isinstance(value, list):
+ value = ConvertingList(value)
+ value.configurator = self
+ elif not isinstance(value, ConvertingTuple) and\
+ isinstance(value, tuple):
+ value = ConvertingTuple(value)
+ value.configurator = self
+ elif isinstance(value, basestring): # str for py3k
+ m = self.CONVERT_PATTERN.match(value)
+ if m:
+ d = m.groupdict()
+ prefix = d['prefix']
+ converter = self.value_converters.get(prefix, None)
+ if converter:
+ suffix = d['suffix']
+ converter = getattr(self, converter)
+ value = converter(suffix)
+ return value
+
+ def configure_custom(self, config):
+ """Configure an object with a user-supplied factory."""
+ c = config.pop('()')
+ if not hasattr(c, '__call__') and hasattr(types, 'ClassType') and type(c) != types.ClassType:
+ c = self.resolve(c)
+ props = config.pop('.', None)
+ # Check for valid identifiers
+ kwargs = dict([(k, config[k]) for k in config if valid_ident(k)])
+ result = c(**kwargs)
+ if props:
+ for name, value in props.items():
+ setattr(result, name, value)
+ return result
+
+ def as_tuple(self, value):
+ """Utility function which converts lists to tuples."""
+ if isinstance(value, list):
+ value = tuple(value)
+ return value
+
+class DictConfigurator(BaseConfigurator):
+ """
+ Configure logging using a dictionary-like object to describe the
+ configuration.
+ """
+
+ def configure(self):
+ """Do the configuration."""
+
+ config = self.config
+ if 'version' not in config:
+ raise ValueError("dictionary doesn't specify a version")
+ if config['version'] != 1:
+ raise ValueError("Unsupported version: %s" % config['version'])
+ incremental = config.pop('incremental', False)
+ EMPTY_DICT = {}
+ logging._acquireLock()
+ try:
+ if incremental:
+ handlers = config.get('handlers', EMPTY_DICT)
+ # incremental handler config only if handler name
+ # ties in to logging._handlers (Python 2.7)
+ if sys.version_info[:2] == (2, 7):
+ for name in handlers:
+ if name not in logging._handlers:
+ raise ValueError('No handler found with '
+ 'name %r' % name)
+ else:
+ try:
+ handler = logging._handlers[name]
+ handler_config = handlers[name]
+ level = handler_config.get('level', None)
+ if level:
+ handler.setLevel(_checkLevel(level))
+ except StandardError, e:
+ raise ValueError('Unable to configure handler '
+ '%r: %s' % (name, e))
+ loggers = config.get('loggers', EMPTY_DICT)
+ for name in loggers:
+ try:
+ self.configure_logger(name, loggers[name], True)
+ except StandardError, e:
+ raise ValueError('Unable to configure logger '
+ '%r: %s' % (name, e))
+ root = config.get('root', None)
+ if root:
+ try:
+ self.configure_root(root, True)
+ except StandardError, e:
+ raise ValueError('Unable to configure root '
+ 'logger: %s' % e)
+ else:
+ disable_existing = config.pop('disable_existing_loggers', True)
+
+ logging._handlers.clear()
+ del logging._handlerList[:]
+
+ # Do formatters first - they don't refer to anything else
+ formatters = config.get('formatters', EMPTY_DICT)
+ for name in formatters:
+ try:
+ formatters[name] = self.configure_formatter(
+ formatters[name])
+ except StandardError, e:
+ raise ValueError('Unable to configure '
+ 'formatter %r: %s' % (name, e))
+ # Next, do filters - they don't refer to anything else, either
+ filters = config.get('filters', EMPTY_DICT)
+ for name in filters:
+ try:
+ filters[name] = self.configure_filter(filters[name])
+ except StandardError, e:
+ raise ValueError('Unable to configure '
+ 'filter %r: %s' % (name, e))
+
+ # Next, do handlers - they refer to formatters and filters
+ # As handlers can refer to other handlers, sort the keys
+ # to allow a deterministic order of configuration
+ handlers = config.get('handlers', EMPTY_DICT)
+ for name in sorted(handlers):
+ try:
+ handler = self.configure_handler(handlers[name])
+ handler.name = name
+ handlers[name] = handler
+ except StandardError, e:
+ raise ValueError('Unable to configure handler '
+ '%r: %s' % (name, e))
+ # Next, do loggers - they refer to handlers and filters
+
+ #we don't want to lose the existing loggers,
+ #since other threads may have pointers to them.
+ #existing is set to contain all existing loggers,
+ #and as we go through the new configuration we
+ #remove any which are configured. At the end,
+ #what's left in existing is the set of loggers
+ #which were in the previous configuration but
+ #which are not in the new configuration.
+ root = logging.root
+ existing = root.manager.loggerDict.keys()
+ #The list needs to be sorted so that we can
+ #avoid disabling child loggers of explicitly
+ #named loggers. With a sorted list it is easier
+ #to find the child loggers.
+ existing.sort()
+ #We'll keep the list of existing loggers
+ #which are children of named loggers here...
+ child_loggers = []
+ #now set up the new ones...
+ loggers = config.get('loggers', EMPTY_DICT)
+ for name in loggers:
+ if name in existing:
+ i = existing.index(name)
+ prefixed = name + "."
+ pflen = len(prefixed)
+ num_existing = len(existing)
+ i = i + 1 # look at the entry after name
+ while (i < num_existing) and\
+ (existing[i][:pflen] == prefixed):
+ child_loggers.append(existing[i])
+ i = i + 1
+ existing.remove(name)
+ try:
+ self.configure_logger(name, loggers[name])
+ except StandardError, e:
+ raise ValueError('Unable to configure logger '
+ '%r: %s' % (name, e))
+
+ #Disable any old loggers. There's no point deleting
+ #them as other threads may continue to hold references
+ #and by disabling them, you stop them doing any logging.
+ #However, don't disable children of named loggers, as that's
+ #probably not what was intended by the user.
+ for log in existing:
+ logger = root.manager.loggerDict[log]
+ if log in child_loggers:
+ logger.level = logging.NOTSET
+ logger.handlers = []
+ logger.propagate = True
+ elif disable_existing:
+ logger.disabled = True
+
+ # And finally, do the root logger
+ root = config.get('root', None)
+ if root:
+ try:
+ self.configure_root(root)
+ except StandardError, e:
+ raise ValueError('Unable to configure root '
+ 'logger: %s' % e)
+ finally:
+ logging._releaseLock()
+
+ def configure_formatter(self, config):
+ """Configure a formatter from a dictionary."""
+ if '()' in config:
+ factory = config['()'] # for use in exception handler
+ try:
+ result = self.configure_custom(config)
+ except TypeError, te:
+ if "'format'" not in str(te):
+ raise
+ #Name of parameter changed from fmt to format.
+ #Retry with old name.
+ #This is so that code can be used with older Python versions
+ #(e.g. by Django)
+ config['fmt'] = config.pop('format')
+ config['()'] = factory
+ result = self.configure_custom(config)
+ else:
+ fmt = config.get('format', None)
+ dfmt = config.get('datefmt', None)
+ result = logging.Formatter(fmt, dfmt)
+ return result
+
+ def configure_filter(self, config):
+ """Configure a filter from a dictionary."""
+ if '()' in config:
+ result = self.configure_custom(config)
+ else:
+ name = config.get('name', '')
+ result = logging.Filter(name)
+ return result
+
+ def add_filters(self, filterer, filters):
+ """Add filters to a filterer from a list of names."""
+ for f in filters:
+ try:
+ filterer.addFilter(self.config['filters'][f])
+ except StandardError, e:
+ raise ValueError('Unable to add filter %r: %s' % (f, e))
+
+ def configure_handler(self, config):
+ """Configure a handler from a dictionary."""
+ formatter = config.pop('formatter', None)
+ if formatter:
+ try:
+ formatter = self.config['formatters'][formatter]
+ except StandardError, e:
+ raise ValueError('Unable to set formatter '
+ '%r: %s' % (formatter, e))
+ level = config.pop('level', None)
+ filters = config.pop('filters', None)
+ if '()' in config:
+ c = config.pop('()')
+ if not hasattr(c, '__call__') and hasattr(types, 'ClassType') and type(c) != types.ClassType:
+ c = self.resolve(c)
+ factory = c
+ else:
+ klass = self.resolve(config.pop('class'))
+ #Special case for handler which refers to another handler
+ if issubclass(klass, logging.handlers.MemoryHandler) and\
+ 'target' in config:
+ try:
+ config['target'] = self.config['handlers'][config['target']]
+ except StandardError, e:
+ raise ValueError('Unable to set target handler '
+ '%r: %s' % (config['target'], e))
+ elif issubclass(klass, logging.handlers.SMTPHandler) and\
+ 'mailhost' in config:
+ config['mailhost'] = self.as_tuple(config['mailhost'])
+ elif issubclass(klass, logging.handlers.SysLogHandler) and\
+ 'address' in config:
+ config['address'] = self.as_tuple(config['address'])
+ factory = klass
+ kwargs = dict([(k, config[k]) for k in config if valid_ident(k)])
+ try:
+ result = factory(**kwargs)
+ except TypeError, te:
+ if "'stream'" not in str(te):
+ raise
+ #The argument name changed from strm to stream
+ #Retry with old name.
+ #This is so that code can be used with older Python versions
+ #(e.g. by Django)
+ kwargs['strm'] = kwargs.pop('stream')
+ result = factory(**kwargs)
+ if formatter:
+ result.setFormatter(formatter)
+ if level is not None:
+ result.setLevel(_checkLevel(level))
+ if filters:
+ self.add_filters(result, filters)
+ return result
+
+ def add_handlers(self, logger, handlers):
+ """Add handlers to a logger from a list of names."""
+ for h in handlers:
+ try:
+ logger.addHandler(self.config['handlers'][h])
+ except StandardError, e:
+ raise ValueError('Unable to add handler %r: %s' % (h, e))
+
+ def common_logger_config(self, logger, config, incremental=False):
+ """
+ Perform configuration which is common to root and non-root loggers.
+ """
+ level = config.get('level', None)
+ if level is not None:
+ logger.setLevel(_checkLevel(level))
+ if not incremental:
+ #Remove any existing handlers
+ for h in logger.handlers[:]:
+ logger.removeHandler(h)
+ handlers = config.get('handlers', None)
+ if handlers:
+ self.add_handlers(logger, handlers)
+ filters = config.get('filters', None)
+ if filters:
+ self.add_filters(logger, filters)
+
+ def configure_logger(self, name, config, incremental=False):
+ """Configure a non-root logger from a dictionary."""
+ logger = logging.getLogger(name)
+ self.common_logger_config(logger, config, incremental)
+ propagate = config.get('propagate', None)
+ if propagate is not None:
+ logger.propagate = propagate
+
+ def configure_root(self, config, incremental=False):
+ """Configure a root logger from a dictionary."""
+ root = logging.getLogger()
+ self.common_logger_config(root, config, incremental)
+
+dictConfigClass = DictConfigurator
+
+def dictConfig(config):
+ """Configure logging using a dictionary."""
+ dictConfigClass(config).configure()
diff --git a/django/utils/log.py b/django/utils/log.py
new file mode 100644
index 0000000000..aece33bfac
--- /dev/null
+++ b/django/utils/log.py
@@ -0,0 +1,56 @@
+import logging
+from django.core import mail
+
+# Make sure a NullHandler is available
+# This was added in Python 2.7/3.2
+try:
+ from logging import NullHandler
+except ImportError:
+ class NullHandler(logging.Handler):
+ def emit(self, record):
+ pass
+
+# Make sure that dictConfig is available
+# This was added in Python 2.7/3.2
+try:
+ from logging.config import dictConfig
+except ImportError:
+ from django.utils.dictconfig import dictConfig
+
+# Ensure the creation of the Django logger
+# with a null handler. This ensures we don't get any
+# 'No handlers could be found for logger "django"' messages
+logger = logging.getLogger('django')
+if not logger.handlers:
+ logger.addHandler(NullHandler())
+
+class AdminEmailHandler(logging.Handler):
+ """An exception log handler that emails log entries to site admins
+
+ If the request is passed as the first argument to the log record,
+ request data will be provided in the
+ """
+ def emit(self, record):
+ import traceback
+ from django.conf import settings
+
+ try:
+ request = record.request
+
+ subject = '%s (%s IP): %s' % (
+ record.levelname,
+ (request.META.get('REMOTE_ADDR') in settings.INTERNAL_IPS and 'internal' or 'EXTERNAL'),
+ request.path
+ )
+ request_repr = repr(request)
+ except:
+ subject = 'Error: Unknown URL'
+ request_repr = "Request repr() unavailable"
+
+ if record.exc_info:
+ stack_trace = '\n'.join(traceback.format_exception(*record.exc_info))
+ else:
+ stack_trace = 'No stack trace available'
+
+ message = "%s\n\n%s" % (stack_trace, request_repr)
+ mail.mail_admins(subject, message, fail_silently=True)
diff --git a/django/views/decorators/http.py b/django/views/decorators/http.py
index 220a60c4be..cb4c1aff21 100644
--- a/django/views/decorators/http.py
+++ b/django/views/decorators/http.py
@@ -10,15 +10,18 @@ except ImportError:
from calendar import timegm
from datetime import timedelta
from email.Utils import formatdate
+import logging
from django.utils.decorators import decorator_from_middleware, available_attrs
from django.utils.http import parse_etags, quote_etag
from django.middleware.http import ConditionalGetMiddleware
from django.http import HttpResponseNotAllowed, HttpResponseNotModified, HttpResponse
-
conditional_page = decorator_from_middleware(ConditionalGetMiddleware)
+logger = logging.getLogger('django.request')
+
+
def require_http_methods(request_method_list):
"""
Decorator to make a view only accept particular request methods. Usage::
@@ -33,6 +36,12 @@ def require_http_methods(request_method_list):
def decorator(func):
def inner(request, *args, **kwargs):
if request.method not in request_method_list:
+ logger.warning('Method Not Allowed (%s): %s' % (request.method, request.path),
+ extra={
+ 'status_code': 405,
+ 'request': request
+ }
+ )
return HttpResponseNotAllowed(request_method_list)
return func(request, *args, **kwargs)
return wraps(func, assigned=available_attrs(func))(inner)
@@ -111,9 +120,21 @@ def condition(etag_func=None, last_modified_func=None):
if request.method in ("GET", "HEAD"):
response = HttpResponseNotModified()
else:
+ logger.warning('Precondition Failed: %s' % request.path,
+ extra={
+ 'status_code': 412,
+ 'request': request
+ }
+ )
response = HttpResponse(status=412)
elif if_match and ((not res_etag and "*" in etags) or
(res_etag and res_etag not in etags)):
+ logger.warning('Precondition Failed: %s' % request.path,
+ extra={
+ 'status_code': 412,
+ 'request': request
+ }
+ )
response = HttpResponse(status=412)
elif (not if_none_match and if_modified_since and
request.method == "GET" and
diff --git a/django/views/generic/simple.py b/django/views/generic/simple.py
index 435cd7623d..abadef3629 100644
--- a/django/views/generic/simple.py
+++ b/django/views/generic/simple.py
@@ -1,6 +1,11 @@
+import logging
+
from django.template import loader, RequestContext
from django.http import HttpResponse, HttpResponseRedirect, HttpResponsePermanentRedirect, HttpResponseGone
+logger = logging.getLogger('django.request')
+
+
def direct_to_template(request, template, extra_context=None, mimetype=None, **kwargs):
"""
Render a given template with any extra URL parameters in the context as
@@ -46,4 +51,9 @@ def redirect_to(request, url, permanent=True, query_string=False, **kwargs):
klass = permanent and HttpResponsePermanentRedirect or HttpResponseRedirect
return klass(url % kwargs)
else:
+ logger.warning('Gone: %s' % request.path,
+ extra={
+ 'status_code': 410,
+ 'request': request
+ })
return HttpResponseGone()