NewsBlur/utils/djangologging/middleware.py
2009-07-25 04:16:48 +00:00

339 lines
12 KiB
Python

import datetime
import inspect
import logging
import os
import re
import sys
import time
import urlparse
try:
import pygments
import pygments.lexers
import pygments.formatters
import pygments.styles
except ImportError:
pygments = None
import django
from django.conf import settings
from django.contrib import admin
from django.db import connection
from django.shortcuts import render_to_response
from django.template import loader
from django.utils.cache import add_never_cache_headers
from django.utils.html import escape
try:
from django.utils.encoding import smart_str, smart_unicode
except ImportError:
# Older versions of Django don't have smart_str, but because they don't
# require Unicode, we can simply fake it with an identify function.
smart_str = lambda s: s
try:
from django.utils.safestring import mark_safe
except ImportError:
# Older versions of Django don't have mark_safe, so we have to escape
# manually when required.
mark_safe = None
from django.utils.functional import curry
from djangologging import SUPPRESS_OUTPUT_ATTR, getLevelNames
from djangologging.handlers import ThreadBufferedHandler
""" Regex to find the closing head element in a (X)HTML document. """
close_head_re = re.compile("(</head>)", re.M | re.I)
""" Regex to find the closing body element in a (X)HTML document. """
close_body_re = re.compile("(</body>)", re.M | re.I)
# Initialise and register the handler
handler = ThreadBufferedHandler()
logging.root.setLevel(logging.NOTSET)
logging.root.addHandler(handler)
# Because this logging module isn't registered within INSTALLED_APPS, we have
# to use (or work out) an absolute file path to the templates and add it to
# TEMPLATE_DIRS.
try:
template_path = settings.LOGGING_TEMPLATE_DIR
except AttributeError:
template_path = os.path.join(os.path.dirname(__file__), 'templates')
settings.TEMPLATE_DIRS = (template_path,) + tuple(settings.TEMPLATE_DIRS)
try:
intercept_redirects = settings.LOGGING_INTERCEPT_REDIRECTS
except AttributeError:
intercept_redirects = False
try:
logging_output_enabled = settings.LOGGING_OUTPUT_ENABLED
except AttributeError:
logging_output_enabled = settings.DEBUG
logging.warn('DEBUG: %s' % logging_output_enabled)
try:
logging_show_metrics = settings.LOGGING_SHOW_METRICS
except AttributeError:
logging_show_metrics = True
try:
logging_show_hints = settings.LOGGING_SHOW_HINTS
except AttributeError:
logging_show_hints = True
try:
logging_log_sql = settings.LOGGING_LOG_SQL
except AttributeError:
logging_log_sql = False
try:
logging_rewrite_content_types = settings.LOGGING_REWRITE_CONTENT_TYPES
except AttributeError:
logging_rewrite_content_types = ('text/html',)
_django_path = os.path.realpath(os.path.dirname(django.__file__))
_admin_path = os.path.realpath(os.path.dirname(admin.__file__))
_logging_path = os.path.realpath(os.path.dirname(logging.__file__))
_djangologging_path = os.path.realpath(os.path.dirname(__file__))
def get_meaningful_frame():
"""
Try to find the meaningful frame, rather than just using one from
the innards of the Django or logging code.
"""
frame = inspect.currentframe().f_back
while frame.f_back:
filename = os.path.realpath(frame.f_code.co_filename)
if not (filename.startswith(_django_path) or \
filename.startswith(_logging_path) or \
filename.startswith(_djangologging_path)) or \
filename.startswith(_admin_path):
break
frame = frame.f_back
return frame
if logging_log_sql:
# Define a new logging level called SQL
logging.SQL = logging.DEBUG + 1
logging.addLevelName(logging.SQL, 'SQL')
# Define a custom function for creating log records
def make_sql_record(frame, original_makeRecord, sqltime, self, *args, **kwargs):
args = list(args)
len_args = len(args)
if len_args > 2:
args[2] = frame.f_code.co_filename
else:
kwargs['fn'] = frame.f_code.co_filename
if len_args > 3:
args[3] = frame.f_lineno
else:
kwargs['lno'] = frame.f_lineno
if len_args > 7:
args[7] = frame.f_code.co_name
elif 'func' in kwargs:
kwargs['func'] = frame.f_code.co_name
rv = original_makeRecord(self, *args, **kwargs)
rv.__dict__['sqltime'] = '%d' % sqltime
return rv
class SqlLoggingList(list):
def append(self, object):
frame = get_meaningful_frame()
sqltime = float(object['time']) * 1000
# Temporarily use make_sql_record for creating log records
original_makeRecord = logging.Logger.makeRecord
logging.Logger.makeRecord = curry(make_sql_record, frame, original_makeRecord, sqltime)
logging.getLogger().log(logging.SQL, object['sql'])
logging.Logger.makeRecord = original_makeRecord
list.append(self, object)
_makeRecord = logging.Logger.makeRecord
def enhanced_make_record(self, *args, **kwargs):
"""Enahnced makeRecord that captures the source code and local variables of
the code logging a message."""
rv = _makeRecord(self, *args, **kwargs)
frame = get_meaningful_frame()
source_lines = inspect.getsourcelines(frame)
lineno = frame.f_lineno - source_lines[1]
show = 5
start, stop = max(0, lineno - show), lineno + show + 1
rv.__dict__['source_lines'] = python_to_html(''.join(source_lines[0][start:stop]), source_lines[1] + start, [lineno - start + 1])
rv.__dict__['local_variables'] = frame.f_locals.items()
return rv
logging.Logger.makeRecord = enhanced_make_record
_redirect_statuses = {
301: 'Moved Permanently',
302: 'Found',
303: 'See Other',
307: 'Temporary Redirect'}
def format_time(record):
time = datetime.datetime.fromtimestamp(record.created)
return '%s,%03d' % (time.strftime('%H:%M:%S'), record.msecs)
def sql_to_html(sql):
if pygments:
try:
lexer = {
'mysql': pygments.lexers.MySqlLexer,
}[settings.DATABASE_ENGINE]
except KeyError:
lexer = pygments.lexers.SqlLexer
html = pygments.highlight(sql, lexer(),
pygments.formatters.HtmlFormatter(cssclass='sql_highlight'))
# Add some line breaks in appropriate places
html = html.replace('<span class="k">', '<br /><span class="k">')
html = re.sub(r'(<pre>\s*)<br />', r'\1', html)
html = re.sub(r'(<span class="k">[^<>]+</span>\s*)<br />(<span class="k">)', r'\1\2', html)
html = re.sub(r'<br />(<span class="k">(IN|LIKE)</span>)', r'\1', html)
# Add a space after commas to help with wrapping
html = re.sub(r'<span class="p">,</span>', '<span class="p">, </span>', html)
else:
html = '<div class="sql_highlight"><pre>%s</pre></div>' % escape(sql)
if mark_safe:
html = mark_safe(html)
return html
def python_to_html(python, linenostart=1, hl_lines=()):
if pygments:
html = pygments.highlight(python,
pygments.lexers.PythonLexer(),
pygments.formatters.HtmlFormatter(
linenos='inline', linenostart=linenostart, hl_lines=hl_lines
))
else:
lines = python.split('\n')
mx = len(str(linenostart + len(lines)))
python = '\n'.join(['%*d %s' % (mx, i+1, l) for i, l in enumerate(lines)])
html = '<pre>%s</pre>' % escape(python)
if mark_safe:
html = mark_safe(html)
return html
class LoggingMiddleware(object):
"""
Middleware that appends the messages logged during the request to the
response (if the response is HTML).
"""
def process_request(self, request):
handler.clear_records()
if logging_log_sql:
connection.queries = SqlLoggingList(connection.queries)
request.logging_start_time = time.time()
def process_response(self, request, response):
if logging_output_enabled and \
not getattr(response, SUPPRESS_OUTPUT_ATTR, False):
if intercept_redirects and \
response.status_code in _redirect_statuses and \
len(handler.get_records()):
response = self._handle_redirect(request, response)
for content_type in logging_rewrite_content_types:
if response['Content-Type'].startswith(content_type):
self._rewrite_html(request, response)
add_never_cache_headers(response)
break
return response
def _get_and_clear_records(self):
records = handler.get_records()
handler.clear_records()
for record in records:
record.formatted_timestamp = format_time(record)
message = record.getMessage()
if record.levelname == 'SQL':
record.formatted_message = sql_to_html(message)
else:
record.formatted_message = escape(message)
return records
def _rewrite_html(self, request, response):
if not hasattr(request, 'logging_start_time'):
return
hints = {
'pygments': logging_log_sql and not pygments,
}
context = {
'records': self._get_and_clear_records(),
'levels': getLevelNames(),
'elapsed_time': (time.time() - request.logging_start_time) * 1000, # milliseconds
'query_count': -1,
'logging_log_sql': logging_log_sql,
'logging_show_metrics': logging_show_metrics,
'logging_show_hints': logging_show_hints,
'hints': dict(filter(lambda (k, v): v, hints.items())),
}
if settings.DEBUG and logging_show_metrics:
context['query_count'] = len(connection.queries)
if context['query_count'] and context['elapsed_time']:
context['query_time'] = sum(map(lambda q: float(q['time']) * 1000, connection.queries))
context['query_percentage'] = context['query_time'] / context['elapsed_time'] * 100
header = smart_str(loader.render_to_string('logging.css'))
footer = smart_unicode(loader.render_to_string('logging.html', context))
if close_head_re.search(response.content) and close_body_re.search(response.content):
def safe_prepend(prependant):
def _prepend(match):
return '%s%s' % (prependant, match.group(0))
return _prepend
response.content = close_head_re.sub(safe_prepend(header), response.content)
response.content = close_body_re.sub(safe_prepend(footer), response.content)
else:
# Despite a Content-Type of text/html, the content doesn't seem to
# be sensible HTML, so just append the log to the end of the
# response and hope for the best!
response.write(footer)
def _handle_redirect(self, request, response):
if hasattr(request, 'build_absolute_url'):
location = request.build_absolute_uri(response['Location'])
else:
# Construct the URL manually in older versions of Django
request_protocol = request.is_secure() and 'https' or 'http'
request_url = '%s://%s%s' % (request_protocol,
request.META.get('HTTP_HOST'), request.path)
location = urlparse.urljoin(request_url, response['Location'])
data = {
'location': location,
'status_code': response.status_code,
'status_name': _redirect_statuses[response.status_code]}
response = render_to_response('redirect.html', data)
add_never_cache_headers(response)
return response
class SuppressLoggingOnAjaxRequestsMiddleware(object):
"""Suppress and log messages from being outputted to the browser on
requests that are made from AJAX. This relies on the
X-Requested-With header being set, which all the most popular libraries
seem to do."""
def process_response(self, request, response):
if request.META.get('HTTP_X_REQUESTED_WITH', '').lower() == 'xmlhttprequest':
setattr(response, SUPPRESS_OUTPUT_ATTR, True)
return response