changeset 101:7e2908e2a8e6

Add debug footer with SQL stats
author Sylvain Beucler <beuc@beuc.net>
date Sat, 01 Aug 2009 11:00:18 +0200
parents b2d75b8b5fd2
children eb952fbc0d01
files src/savane/middleware/__init__.py src/savane/middleware/debug.py src/settings.py
diffstat 3 files changed, 176 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
new file mode 100644
new file mode 100644
--- /dev/null
+++ b/src/savane/middleware/debug.py
@@ -0,0 +1,175 @@
+# http://www.djangosnippets.org/snippets/766/
+# http://www.djangosnippets.org/snippets/1033/
+# Author: simon (May 21, 2008)
+# Author: crucialfelix (September 7, 2008)
+# Copyright (C) 2009  Sylvain Beucler
+# 
+# I hate legal-speak as much as anybody, but on a site which is geared
+# toward sharing code there has to be at least a little bit of it, so
+# here goes:
+# 
+# By creating an account here you agree to three things:
+# 
+#   1. That you will only post code which you wrote yourself and that
+#   you have the legal right to release under these terms.
+# 
+#   2. That you grant any third party who sees the code you post a
+#   royalty-free, non-exclusive license to copy and distribute that
+#   code and to make and distribute derivative works based on that
+#   code. You may include license terms in snippets you post, if you
+#   wish to use a particular license (such as the BSD license or GNU
+#   GPL), but that license must permit royalty-free copying,
+#   distribution and modification of the code to which it is applied.
+# 
+#   3. That if you post code of which you are not the author or for
+#   which you do not have the legal right to distribute according to
+#   these terms, you will indemnify and hold harmless the operators of
+#   this site and any third parties who are exposed to liability as a
+#   result of your actions.
+#
+# If you can't legally agree to these terms, or don't want to, you
+# cannot create an account here.
+
+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
+from django.utils.safestring import mark_safe
+
+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>View: <strong>{{view}}</strong></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)
+        template_rendered.connect(self._storeRenderedTemplates)
+
+    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'])
+
+        from django.core.urlresolvers import resolve
+        view_func, args, kwargs = resolve(request.META['PATH_INFO'])
+
+        if hasattr(view_func,'view_func'):
+            # it the view_func has a view_func then its a decorator
+            co = view_func.view_func.func_code
+        else:
+            co = view_func.func_code
+            
+        view =  '%s.%s' % (view_func.__module__, view_func.__name__)
+        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,
+            'view': view
+        }))
+
+        content = response.content
+        response.content = force_unicode(content).replace('</body>', debug_content)
+
+        return response
+
+    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)
+
+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
--- a/src/settings.py
+++ b/src/settings.py
@@ -56,6 +56,7 @@
     'django.middleware.common.CommonMiddleware',
     'django.contrib.sessions.middleware.SessionMiddleware',
     'django.contrib.auth.middleware.AuthenticationMiddleware',
+    'savane.middleware.debug.DebugFooter',
 )
 
 ROOT_URLCONF = 'urls'