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
This commit is contained in:
Russell Keith-Magee 2010-10-04 15:12:39 +00:00
parent 667d832e90
commit 24acca4139
19 changed files with 1292 additions and 21 deletions

View File

@ -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.

View File

@ -498,6 +498,34 @@ MESSAGE_STORAGE = 'django.contrib.messages.storage.user_messages.LegacyFallbackS
# Default values of MESSAGE_LEVEL and MESSAGE_TAGS are defined within
# 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 #
###########

View File

@ -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,
},
}
}

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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__:

View File

@ -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

View File

@ -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()

553
django/utils/dictconfig.py Normal file
View File

@ -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()

56
django/utils/log.py Normal file
View File

@ -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)

View File

@ -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

View File

@ -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()

View File

@ -176,6 +176,7 @@ Other batteries included
* :doc:`Internationalization <topics/i18n/index>`
* :doc:`Jython support <howto/jython>`
* :doc:`"Local flavor" <ref/contrib/localflavor>`
* :doc:`Logging <topics/logging>`
* :doc:`Messages <ref/contrib/messages>`
* :doc:`Pagination <topics/pagination>`
* :doc:`Redirects <ref/contrib/redirects>`

View File

@ -1008,6 +1008,36 @@ See :ref:`using-translations-in-your-own-projects`.
.. setting:: LOGIN_REDIRECT_URL
LOGGING
-------
Default: A logging configuration dictionary.
A data structure containing configuration information. The contents of
this data structure will be passed as the argument to the
configuration method described in :setting:`LOGGING_CONFIG`.
The default logging configuration passes HTTP 500 server errors to an
email log handler; all other log messages are given to a NullHandler.
.. versionadded:: 1.3
LOGGING_CONFIG
--------------
Default: ``'django.utils.log.dictConfig'``
A path to a callable that will be used to configure logging in the
Django project. Points at a instance of Python's `dictConfig`_
configuration method by default.
If you set :setting:`LOGGING_CONFIG` to ``None``, the logging
configuration process will be skipped.
.. _dictConfig: http://docs.python.org/library/logging.html#logging.dictConfig
.. versionadded:: 1.3
LOGIN_REDIRECT_URL
------------------

View File

@ -109,3 +109,13 @@ encouraged you redeploy your Django instances using :doc:`mod_wsgi
What's new in Django 1.3
========================
Logging
~~~~~~~
Django 1.3 adds framework-level support for Python's logging module.
This means you can now esaily configure and control logging as part of
your Django project. A number of logging handlers and logging calls
have been added to Django's own code as well -- most notably, the
error emails sent on a HTTP 500 server error are now handled as a
logging activity. See :doc:`the documentation on Django's logging
interface </topics/logging>` for more details.

View File

@ -20,6 +20,7 @@ Introductions to all the key parts of Django you'll need to know:
conditional-view-processing
email
i18n/index
logging
pagination
serialization
settings

442
docs/topics/logging.txt Normal file
View File

@ -0,0 +1,442 @@
=======
Logging
=======
.. versionadded:: 1.3
.. module:: django.utils.log
:synopsis: Logging tools for Django applications
A quick logging primer
======================
Django uses Python's builtin logging module to perform system logging.
The usage of the logging module is discussed in detail in `Python's
own documentation`_. However, if you've never used Python's logging
framework (or even if you have), here's a quick primer.
.. _Python's own documentation: http://docs.python.org/library/logging.html
The cast of players
-------------------
A Python logging configuration consists of four parts:
* :ref:`topic-logging-parts-loggers`
* :ref:`topic-logging-parts-handlers`
* :ref:`topic-logging-parts-filters`
* :ref:`topic-logging-parts-formatters`
.. _topic-logging-parts-loggers:
Loggers
~~~~~~~
A logger is the entry point into the logging system. Each logger is
a named bucket to which messages can be written for processing.
A logger is configured to have *log level*. This log level describes
the severity of the messages that the logger will handle. Python
defines the following log levels:
* ``DEBUG``: Low level system information for debugging purposes
* ``INFO``: General system information
* ``WARNING``: Information describing a minor problem that has
occurred.
* ``ERROR``: Information describing a major problem that has
occurred.
* ``CRITICAL``: Information describing a critical problem that has
occurred.
Each message that is written to the logger is a *Log Record*. Each log
record also has a *log level* indicating the severity of that specific
message. A log record can also contain useful metadata that describes
the event that is being logged. This can include details such as a
stack trace or an error code.
When a message is given to the logger, the log level of the message is
compare to the log level of the logger. If the log level of the
message meets or exceeds the log level of the logger itself, the
message will undergo further processing. If it doesn't, the message
will be ignored.
Once a logger has determined that a message needs to be processed,
it is passed to a *Handler*.
.. _topic-logging-parts-handlers:
Handlers
~~~~~~~~
The handler is the engine that determines what happens to each message
in a logger. It describes a particular logging behavior, such as
writing a message to the screen, to a file, or to a network socket.
Like loggers, handlers also have a log level. If the log level of a
log record doesn't meet or exceed the level of the handler, the
handler will ignore the message.
A logger can have multiple handlers, and each handler can have a
different log level. In this way, it is possible to provide different
forms of notification depending on the importance of a message. For
example, you could install one handler that forwards ``ERROR`` and
``CRITICIAL`` messages to a paging service, while a second handler
logs all messages (including ``ERROR`` and ``CRITICAL`` messages) to a
file for later analysis.
.. _topic-logging-parts-filters:
Filters
~~~~~~~
A filter is used to provide additional control over which log records
are passed from logger to handler.
By default, any log message that meets log level requirements will be
handled. However, by installing a filter, you can place additional
criteria on the logging process. For example, you could install a
filter that only allows ``ERROR`` messages from a particular source to
be emitted.
Filters can also be used to modify the logging record prior to being
emitted. For example, you could write a filter that downgrades
``ERROR`` log records to ``WARNING`` records if a particular set of
criteria are met.
Filters can be installed on loggers or on handlers; multiple filters
can be used in a chain to perform multiple filtering actions.
.. _topic-logging-parts-formatters:
Formatters
~~~~~~~~~~
Ultimately, a log record needs to be rendered as text. Formatters
describe the exact format of that text. A formatter usually consists
of a Python formatting string; however, you can also write custom
formatters to implement specific formatting behavior.
Using logging
=============
Once you have configured your loggers, handlers, filters and
formatters, you need to place logging calls into your code. Using the
logging framework is very simple. Here's an example::
# import the logging library
import logging
# Get an instance of a logger
logger = logging.getLogger(__name__)
def my_view(request, arg1, arg):
...
if bad_mojo:
# Log an error message
logger.error('Something went wrong!')
And that's it! Every time the ``bad_mojo`` condition is activated, an
error log record will be written.
Naming loggers
~~~~~~~~~~~~~~
The call to :meth:`logging.getLogger()` obtains (creating, if
necessary) an instance of a logger. The logger instance is identified
by a name. This name is used to identify the logger for configuration
purposes.
By convention, the logger name is usually ``__name__``, the name of
the python module that contains the logger. This allows you to filter
and handle logging calls on a per-module basis. However, if you have
some other way of organizing your logging messages, you can provide
any dot-separated name to identify your logger::
# Get an instance of a specfic named logger
logger = logging.getLogger('project.interesting.stuff')
The dotted paths of logger names define a hierarchy. The
``project.interesting`` logger is considered to be a parent of the
``project.interesting.stuff`` logger; the ``project`` logger
is a parent of the ``project.interesting`` logger.
Why is the hierarchy important? Well, because loggers can be set to
*propagate* their logging calls to their parents. In this way, you can
define a single set of handlers at the root of a logger tree, and
capture all logging calls in the subtree of loggers. A logging handler
defined in the ``project`` namespace will catch all logging messages
issued on the ``project.interesting`` and
``project.interesting.stuff`` loggers.
This propagation can be controlled on a per-logger basis. If
you don't want a particular logger to propagate to it's parents, you
can turn off this behavior.
Making logging calls
~~~~~~~~~~~~~~~~~~~~
The logger instance contains an entry method for each of the default
log levels:
* ``logger.critical()``
* ``logger.error()``
* ``logger.warning()``
* ``logger.info()``
* ``logger.debug()``
There are two other logging calls available:
* ``logger.log()``: manually a logging message with a specific
log level.
* ``logger.exception()``: create a ``ERRORR`` level logging
message wrapping the current exception stack frame.
Configuring logging
===================
Of course, it isn't enough to just put logging calls into your code.
You also need to configure the loggers, handlers, filters and
formatters to ensure that logging output is output in a useful way.
Python's logging library provides several techniques to configure
logging, ranging from a programatic interface to configuration files.
By default, Django uses the `dictConfig format`_.
.. note::
``logging.dictConfig`` is a builtin library in Python 2.7. In
order to make this library available for users of earlier Python
versions, Django includes a copy as part of ``django.utils.log``.
If you have Python 2.7, the system native library will be used; if
you have Python 2.6 or earlier, Django's copy will be used.
In order to configure logging, you use :setting:`LOGGING` to define a
dictionary of logging settings. These settings describes the loggers,
handlers, filters and formatters that you want in your logging setup,
and the log levels and other properties that you want those components
to have.
Logging is configured immediately after settings have been loaded.
Since the loading of settings is one of the first things that Django
does, you can be certain that loggers are always ready for use in your
project code.
.. _dictConfig format: http://docs.python.org/library/logging.html#configuration-dictionary-schema
.. _a third-party library: http://bitbucket.org/vinay.sajip/dictconfig
An example
----------
The full documentation for `dictConfig format`_ is the best source of
information about logging configuration dictionaries. However, to give
you a taste of what is possible, here is an example of a fairly
complex logging setup, configured using :meth:`logging.dictConfig`::
LOGGING = {
'version': 1,
'disable_existing_loggers': True,
'formatters': {
'explicit': {
'format': '%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s'
},
'simple': {
'format': '%(levelname)s %(message)s'
},
},
'filters': {
'special': {
'()': 'project.logging.SpecialFilter',
'foo': 'bar',
}
},
'handlers': {
'null': {
'level':'DEBUG',
'class':'django.utils.log.NullHandler',
},
'console':{
'level':'DEBUG',
'class':'logging.StreamHandler',
'formatter': 'simple'
},
'mail_admins': {
'level': 'ERROR',
'class': 'django.utils.log.AdminEmailHandler'
'filters': ['special']
}
},
'loggers': {
'django': {
'handlers':['null'],
'propagate': True,
'level':'INFO',
},
'django.request': {
'handlers': ['mail_admins'],
'level': 'ERROR',
'propagate': False,
},
'myproject.custom': {
'handlers: ['console', 'mail_admins'],
'level': 'INFO',
'filters': ['special']
}
}
}
This logging configuration does the following things:
* Identifies the configuration as being in 'dictConfig version 1'
format. At present, this is the only dictConfig format version.
* Disables all existing logging configurations.
* Defines two formatters:
* ``simple``, that just outputs the log level name (e.g.,
``DEBUG``) and the log message.
The `format` string is a normal Python formatting string
describing the details that are to be output on each logging
line. The full list of detail that can be output can be
found in the `formatter documentation`_.
* ``verbose``, that outputs the log level name, the log
message, plus the time, process, thread and module that
generate the log message.
* Defines one filter -- :class:`project.logging.SpecialFilter`,
using the alias ``special``. If this filter required additional
arguments at time of construction, they can be provided as
additional keys in the filter configuration dictionary. In this
case, the argument ``foo`` will be given a value of ``bar`` when
instantiating the :class:`SpecialFilter`.
* Defines three handlers:
* ``null``, a NullHandler, which will pass any `DEBUG` or
higher message to ``/dev/null``.
* ``console``, a StreamHandler, which will print any `DEBUG`
message to stdout. This handler uses the `simple` output
format.
* ``mail_admins``, an AdminEmailHandler, which will email any
`ERROR` level message to the site admins. This handler uses
the ``special`` filter.
* Configures three loggers:
* ``django``, which passes all messages at ``INFO`` or higher
to the ``null`` handler.
* ``django.request``, which passes all ``ERROR`` messages to
the ``mail_admins`` handler. In addition, this logger is
marked to *not* propagate messages. This means that log
messages written to ``django.request`` will not be handled
by the ``django`` logger.
* ``myproject.custom``, which passes all messages at ``INFO``
or higher that also pass the ``special`` filter to two
handlers -- the ``console``, and ``mail_admins``. This
means that all ``INFO`` level messages (or higher) will be
printed to the console; ``ERROR`` and ``CRITICIAL``
messages will also be output via e-mail.
.. _formatter documentation: http://docs.python.org/library/logging.html#formatter-objects
Custom logging configuration
----------------------------
If you don't want to use Python's dictConfig format to configure your
logger, you can specify your own configuration scheme.
The :setting:`LOGGING_CONFIG` setting defines the callable that will
be used to configure Django's loggers. By default, it points at
Python's :meth:`logging.dictConfig()` method. However, if you want to
use a different configuration process, you can use any other callable
that takes a single argument. The contents of :setting:`LOGGING` will
be provided as the value of that argument when logging is configured.
Disabling logging configuration
-------------------------------
If you don't want to configure logging at all (or you want to manually
configure logging using your own approach), you can set
:setting:`LOGGING_CONFIG` to ``None``. This will disable the
configuration process.
.. note::
Setting :setting:`LOGGING_CONFIG` to ``None`` only means that the
configuration process is disabled, not logging itself. If you
disable the configuration process, Django will still make logging
calls, falling back to whatever default logging behavior is
defined.
Django's logging extensions
===========================
Django provides a number of utilities to handle the unique
requirements of logging in webserver environment.
Loggers
-------
Django provides three built-in loggers.
``django``
~~~~~~~~~~
``django`` is the catch-all logger. No messages are posted directly to
this logger.
``django.requests``
~~~~~~~~~~~~~~~~~~~
Log messages related to the handling of requests. 5XX responses are
raised as ``ERROR`` messages; 4XX responses are raised as ``WARNING``
messages.
Messages to this logger have the following extra context:
* ``status_code``: The HTTP response code associated with the
request.
* ``request``: The request object that generated the logging
message.
``django.db.backends``
~~~~~~~~~~~~~~~~~~~~~~
Messages relating to the interaction of code with the database.
For example, every SQL statement executed by a request is logged
at the ``DEBUG`` level to this logger.
Messages to this logger have the following extra context:
* ``duration``: The time taken to execute the SQL statement.
* ``sql``: The SQL statement that was executed.
* ``params``: The parameters that were used in the SQL call.
Handlers
--------
Django provides one log handler in addition to those provided by the
Python logging module.
.. class:: AdminEmailHandler()
This handler sends an email to the site admins for each log
message it receives.
If the log record contains a 'request' attribute, the full details
of the request will be included in the email.
If the log record contains stack trace information, that stack
trace will be included in the email.

View File

@ -10,7 +10,6 @@ class Advertisment(models.Model):
__test__ = {'API_TESTS': """
>>> from models.publication import Publication
>>> from models.article import Article
>>> from django.contrib.auth.views import Site
>>> p = Publication(title="FooBar")
>>> p.save()