Login

DebugFooter middleware

Author:
simon
Posted:
May 21, 2008
Language:
Python
Version:
.96
Score:
18 (after 18 ratings)

Adds a hidden footer to the bottom of every text/html page containing a list of SQL queries executed and templates that were loaded (including their full filesystem path to help debug complex template loading scenarios).

To use, drop in to a file called 'debug_middleware.py' on your Python path and add 'debug_middleware.DebugFooter' to your MIDDLEWARE_CLASSES setting.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
import time
from django.dispatch import dispatcher
from django.core.signals import request_started
from django.test.signals import template_rendered
from django.conf import settings
from django.db import connection
from django.utils.encoding import force_unicode

TEMPLATE = """
<div id="debug" style="clear:both;">
<a href="#debugbox"
    onclick="this.style.display = 'none';   
        document.getElementById('debugbox').style.display = 'block';
        return false;"
    style="font-size: small; color: red; text-decoration: none; display: block; margin: 12px;"
>+</a>

<div style="display: none;clear: both; border: 1px solid red; padding: 12px; margin: 12px; overflow: scroll" id="debugbox">

<p>Server-time taken: {{ server_time|floatformat:"5" }} seconds</p>
<p>Templates used:</p>
{% if templates %}
<ol>
    {% for template in templates %}
        <li><strong>{{ template.0 }}</strong> loaded from <samp>{{ template.1 }}</samp></li>
    {% endfor %}
</ol>
{% else %}
    None
{% endif %}
<p>Template path:</p> 
{% if template_dirs %}
    <ol>
    {% for template in template_dirs %}
        <li>{{ template }}</li>
    {% endfor %}
    </ol>
{% else %}
    None
{% endif %}
<p>SQL executed:</p>
{% if sql %}
<ol>
{% for query in sql %}
    <li><pre>{{ query.sql|linebreaksbr }}</pre><p>took {{ query.time|floatformat:"3" }} seconds</p></li>
{% endfor %}
</ol>
<p>Total SQL time: {{ sql_total }}</p>
{% else %}
    None
{% endif %}
</div>
</div>
</body>
"""

# Monkeypatch instrumented test renderer from django.test.utils - we could use
# django.test.utils.setup_test_environment for this but that would also set up
# e-mail interception, which we don't want
from django.test.utils import instrumented_test_render
from django.template import Template, Context
if Template.render != instrumented_test_render:
    Template.original_render = Template.render
    Template.render = instrumented_test_render
# MONSTER monkey-patch
old_template_init = Template.__init__
def new_template_init(self, template_string, origin=None, name='<Unknown Template>'):
    old_template_init(self, template_string, origin, name)
    self.origin = origin
Template.__init__ = new_template_init

class DebugFooter:
    def process_request(self, request):
        self.time_started = time.time()
        self.templates_used = []
        self.contexts_used = []
        self.sql_offset_start = len(connection.queries)
        dispatcher.connect(
            self._storeRenderedTemplates, signal=template_rendered
        )
        
    def process_response(self, request, response):
        # Only include debug info for text/html pages not accessed via Ajax
        if 'text/html' not in response['Content-Type']:
            return response
        if request.is_ajax():
            return response
        if not settings.DEBUG:
            return response
        if response.status_code != 200:
            return response
        
        templates = [
            (t.name, t.origin and t.origin.name or 'No origin')
            for t in self.templates_used
        ]
        sql_queries = connection.queries[self.sql_offset_start:]
        # Reformat sql queries a bit
        sql_total = 0.0
        for query in sql_queries:
            query['sql'] = reformat_sql(query['sql'])
            sql_total += float(query['time'])
        
        #import pdb; pdb.set_trace()
        
        
        debug_content = Template(TEMPLATE).render(Context({
            'server_time': time.time() - self.time_started,
            'templates': templates,
            'sql': sql_queries,
            'sql_total': sql_total,
            'template_dirs': settings.TEMPLATE_DIRS,
        }))
        
        content = response.content
        response.content = force_unicode(content).replace('</body>', debug_content)
        
        #import pdb; pdb.set_trace()
        return response
    
    def _storeRenderedTemplates(self, signal, sender, template, context):
        self.templates_used.append(template)
        self.contexts_used.append(context)

def reformat_sql(sql):
    sql = sql.replace('`,`', '`, `')
    sql = sql.replace('` FROM `', '` \n  FROM `')
    sql = sql.replace('` WHERE ', '` \n  WHERE ')
    sql = sql.replace(' ORDER BY ', ' \n  ORDER BY ')
    return sql

More like this

  1. Template tag - list punctuation for a list of items by shapiromatron 2 months, 2 weeks ago
  2. JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 2 months, 3 weeks ago
  3. Serializer factory with Django Rest Framework by julio 9 months, 2 weeks ago
  4. Image compression before saving the new model / work with JPG, PNG by Schleidens 10 months, 1 week ago
  5. Help text hyperlinks by sa2812 11 months ago

Comments

munhitsu (on May 21, 2008):

thanks - fantastic usability

#

kayamb (on May 21, 2008):

Fantastic snippet! However, it does not work with ajax requests, so here's the changes I've done:

  • Remove "</body>" from the template definition.
  • Remove the "if request.is_ajax(): return response" statements.
  • Replace the bottom of the process_response method with:
    if content.lower().count('`</body>`') > 0:
        response.content = force_unicode(content).replace('`</body>`', debug_content) + '`</body>`'
    else:
        response.content = force_unicode(content) + debug_content
    

#

mixtline (on May 23, 2008):
  1. use "if request.META.get('HTTP_X_REQUESTED_WITH') == 'XMLHttpRequest'" instead of "if request.is_ajax()"
  2. use

import re

p = re.compile('<\/body>', re.IGNORECASE)

response.content = p.sub(debug_content, force_unicode(content))

instead of line #128

#

crucialfelix (on July 7, 2008):

excellent. is there anyway to also know what view was executed ?

#

peterbe (on September 3, 2008):

This snippet has stopped working as of latest development version. Last line of the error is:

AttributeError: 'module' object has no attribute 'connect'

I can provide a full traceback if necessary. (I'm using mysql and revision 8922)

#

aaron08544 (on September 27, 2008):

If you don't care about the TextMate links, you can replace line 78: old: dispatcher.connect( self._storeRenderedTemplates, signal=template_rendered ) new: template_rendered.connect(self._storeRenderedTemplates)

and change the _storeRenderedTemplates function to

def _storeRenderedTemplates(self, **kwargs):
    #signal=signal, sender=sender, template=template, context=context):
    template = kwargs.get('template')
    if(template):
        self.templates_used.append(template)
    context = kwargs.get('context')
    if(context):
        self.contexts_used.append(context)

This is just a diff from 1033.

#

Please login first before commenting.