Commit af131a3c by Harry Marr

Refactor tracker, add stack traces to queries

parent 330f1177
import functools import functools
import traceback
import time import time
import inspect
import os
import SocketServer
import django
from django.conf import settings
import pymongo
import pymongo.collection import pymongo.collection
import pymongo.cursor import pymongo.cursor
__all__ = ['queries', 'inserts', 'updates', 'removes', 'install_tracker',
'uninstall_tracker', 'reset']
class MongoOperationTracker(object):
"""Track operations sent to MongoDB via PyMongo.
"""
def __init__(self):
self.queries = []
self.inserts = []
self.updates = []
self.removes = []
self.active = False
def install(self): _original_methods = {
self.original_methods = {
'insert': pymongo.collection.Collection.insert, 'insert': pymongo.collection.Collection.insert,
'update': pymongo.collection.Collection.update, 'update': pymongo.collection.Collection.update,
'remove': pymongo.collection.Collection.remove, 'remove': pymongo.collection.Collection.remove,
'refresh': pymongo.cursor.Cursor._refresh, 'refresh': pymongo.cursor.Cursor._refresh,
} }
# Wrap Cursor._refresh for getting queries queries = []
@functools.wraps(self.original_methods['insert']) inserts = []
def insert(collection_self, doc_or_docs, manipulate=True, updates = []
removes = []
# Wrap Cursor._refresh for getting queries
@functools.wraps(_original_methods['insert'])
def _insert(collection_self, doc_or_docs, manipulate=True,
safe=False, check_keys=True, **kwargs): safe=False, check_keys=True, **kwargs):
start_time = time.time() start_time = time.time()
result = self.original_methods['insert']( result = _original_methods['insert'](
collection_self, collection_self,
doc_or_docs, doc_or_docs,
safe=safe, safe=safe,
...@@ -37,22 +42,19 @@ class MongoOperationTracker(object): ...@@ -37,22 +42,19 @@ class MongoOperationTracker(object):
) )
total_time = (time.time() - start_time) * 1000 total_time = (time.time() - start_time) * 1000
if self.active: inserts.append({
self.inserts.append({
'document': doc_or_docs, 'document': doc_or_docs,
'safe': safe, 'safe': safe,
'time': total_time, 'time': total_time,
}) })
return result return result
pymongo.collection.Collection.insert = insert # Wrap Cursor._refresh for getting queries
@functools.wraps(_original_methods['update'])
# Wrap Cursor._refresh for getting queries def _update(collection_self, spec, document, upsert=False,
@functools.wraps(self.original_methods['update'])
def update(collection_self, spec, document, upsert=False,
maniuplate=False, safe=False, multi=False, **kwargs): maniuplate=False, safe=False, multi=False, **kwargs):
start_time = time.time() start_time = time.time()
result = self.original_methods['update']( result = _original_methods['update'](
collection_self, collection_self,
spec, spec,
document, document,
...@@ -63,8 +65,7 @@ class MongoOperationTracker(object): ...@@ -63,8 +65,7 @@ class MongoOperationTracker(object):
) )
total_time = (time.time() - start_time) * 1000 total_time = (time.time() - start_time) * 1000
if self.active: updates.append({
self.updates.append({
'document': document, 'document': document,
'upsert': upsert, 'upsert': upsert,
'multi': multi, 'multi': multi,
...@@ -74,13 +75,11 @@ class MongoOperationTracker(object): ...@@ -74,13 +75,11 @@ class MongoOperationTracker(object):
}) })
return result return result
pymongo.collection.Collection.update = update # Wrap Cursor._refresh for getting queries
@functools.wraps(_original_methods['remove'])
# Wrap Cursor._refresh for getting queries def _remove(collection_self, spec_or_id, safe=False, **kwargs):
@functools.wraps(self.original_methods['remove'])
def remove(collection_self, spec_or_id, safe=False, **kwargs):
start_time = time.time() start_time = time.time()
result = self.original_methods['insert']( result = _original_methods['insert'](
collection_self, collection_self,
spec_or_id, spec_or_id,
safe=safe, safe=safe,
...@@ -88,33 +87,30 @@ class MongoOperationTracker(object): ...@@ -88,33 +87,30 @@ class MongoOperationTracker(object):
) )
total_time = (time.time() - start_time) * 1000 total_time = (time.time() - start_time) * 1000
if self.active: removes.append({
self.removes.append({
'spec_or_id': spec_or_id, 'spec_or_id': spec_or_id,
'safe': safe, 'safe': safe,
'time': total_time, 'time': total_time,
}) })
return result return result
pymongo.collection.Collection.remove = remove # Wrap Cursor._refresh for getting queries
@functools.wraps(_original_methods['refresh'])
# Wrap Cursor._refresh for getting queries def _cursor_refresh(cursor_self):
@functools.wraps(self.original_methods['refresh'])
def cursor_refresh(cursor_self):
# Look up __ private instance variables # Look up __ private instance variables
def privar(name): def privar(name):
return getattr(cursor_self, '_Cursor__{0}'.format(name)) return getattr(cursor_self, '_Cursor__{0}'.format(name))
if not self.active or privar('id') is not None: if privar('id') is not None:
# Inactive or getMore - move on # getMore not query - move on
return self.original_methods['refresh'](cursor_self) return _original_methods['refresh'](cursor_self)
# NOTE: See pymongo/cursor.py+557 [_refresh()] and # NOTE: See pymongo/cursor.py+557 [_refresh()] and
# pymongo/message.py for where information is stored # pymongo/message.py for where information is stored
# Time the actual query # Time the actual query
start_time = time.time() start_time = time.time()
result = self.original_methods['refresh'](cursor_self) result = _original_methods['refresh'](cursor_self)
total_time = (time.time() - start_time) * 1000 total_time = (time.time() - start_time) * 1000
query_son = privar('query_spec')() query_son = privar('query_spec')()
...@@ -122,6 +118,7 @@ class MongoOperationTracker(object): ...@@ -122,6 +118,7 @@ class MongoOperationTracker(object):
query_data = { query_data = {
'time': total_time, 'time': total_time,
'operation': 'query', 'operation': 'query',
'stack_trace': _tidy_stacktrace(reversed(inspect.stack())),
} }
# Collection in format <db_name>.<collection_name> # Collection in format <db_name>.<collection_name>
...@@ -143,31 +140,81 @@ class MongoOperationTracker(object): ...@@ -143,31 +140,81 @@ class MongoOperationTracker(object):
query_data['skip'] = privar('skip') query_data['skip'] = privar('skip')
query_data['limit'] = privar('limit') query_data['limit'] = privar('limit')
query_data['query'] = query_son['$query'] query_data['query'] = query_son['$query']
query_data['ordering'] = self._get_ordering(query_son) query_data['ordering'] = _get_ordering(query_son)
self.queries.append(query_data) queries.append(query_data)
return result return result
pymongo.cursor.Cursor._refresh = cursor_refresh def install_tracker():
if pymongo.collection.Collection.insert != _insert:
def _get_ordering(self, son): pymongo.collection.Collection.insert = _insert
if pymongo.collection.Collection.update != _update:
pymongo.collection.Collection.update = _update
if pymongo.collection.Collection.remove != _remove:
pymongo.collection.Collection.remove = _remove
if pymongo.cursor.Cursor._refresh != _cursor_refresh:
pymongo.cursor.Cursor._refresh = _cursor_refresh
def uninstall_tracker():
if pymongo.collection.Collection.insert == _insert:
pymongo.collection.Collection.insert = _original_methods['insert']
if pymongo.collection.Collection.update == _update:
pymongo.collection.Collection.update = _original_methods['update']
if pymongo.collection.Collection.remove == _remove:
pymongo.collection.Collection.remove = _original_methods['remove']
if pymongo.cursor.Cursor._refresh == _cursor_refresh:
pymongo.cursor.Cursor._refresh = _original_methods['cursor_refresh']
def reset():
global queries, inserts, updates, removes
queries = []
inserts = []
updates = []
removes = []
def _get_ordering(son):
"""Helper function to extract formatted ordering from dict.
"""
def fmt(field, direction): def fmt(field, direction):
return '{0}{1}'.format({-1: '-', 1: '+'}[direction], field) return '{0}{1}'.format({-1: '-', 1: '+'}[direction], field)
if '$orderby' in son: if '$orderby' in son:
return ', '.join(fmt(f, d) for f, d in son['$orderby'].items()) return ', '.join(fmt(f, d) for f, d in son['$orderby'].items())
def uninstall(self): # Taken from Django Debug Toolbar 0.8.6
pymongo.cursor.Cursor._refresh = self.original_methods['refresh'] def _tidy_stacktrace(stack):
pymongo.cursor.Collection.insert = self.original_methods['insert'] """
pymongo.cursor.Collection.update = self.original_methods['update'] Clean up stacktrace and remove all entries that:
1. Are part of Django (except contrib apps)
def reset(self): 2. Are part of SocketServer (used by Django's dev server)
self.queries = [] 3. Are the last entry (which is part of our stacktracing code)
def start(self): ``stack`` should be a list of frame tuples from ``inspect.stack()``
self.active = True """
django_path = os.path.realpath(os.path.dirname(django.__file__))
django_path = os.path.normpath(os.path.join(django_path, '..'))
socketserver_path = os.path.realpath(os.path.dirname(SocketServer.__file__))
pymongo_path = os.path.realpath(os.path.dirname(pymongo.__file__))
trace = []
for frame, path, line_no, func_name, text in (f[:5] for f in stack):
s_path = os.path.realpath(path)
# Support hiding of frames -- used in various utilities that provide
# inspection.
if '__traceback_hide__' in frame.f_locals:
continue
if getattr(settings, 'DEBUG_TOOLBAR_CONFIG', {}).get('HIDE_DJANGO_SQL', True) \
and django_path in s_path and not 'django/contrib' in s_path:
continue
if socketserver_path in s_path:
continue
if pymongo_path in s_path:
continue
if not text:
text = ''
else:
text = (''.join(text)).strip()
trace.append((path, line_no, func_name, text))
return trace
def stop(self):
self.active = False
...@@ -2,7 +2,7 @@ from django.template.loader import render_to_string ...@@ -2,7 +2,7 @@ from django.template.loader import render_to_string
from debug_toolbar.panels import DebugPanel from debug_toolbar.panels import DebugPanel
from operation_tracker import MongoOperationTracker import operation_tracker
class MongoDebugPanel(DebugPanel): class MongoDebugPanel(DebugPanel):
...@@ -13,23 +13,18 @@ class MongoDebugPanel(DebugPanel): ...@@ -13,23 +13,18 @@ class MongoDebugPanel(DebugPanel):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(self.__class__, self).__init__(*args, **kwargs) super(self.__class__, self).__init__(*args, **kwargs)
self.op_tracker = MongoOperationTracker() operation_tracker.install_tracker()
self.op_tracker.install()
def process_request(self, request): def process_request(self, request):
self.op_tracker.reset() operation_tracker.reset()
self.op_tracker.start()
def process_response(self, request, response):
self.op_tracker.stop()
def nav_title(self): def nav_title(self):
return 'MongoDB' return 'MongoDB'
def nav_subtitle(self): def nav_subtitle(self):
num_queries = len(self.op_tracker.queries) num_queries = len(operation_tracker.queries)
attrs = ['queries', 'inserts', 'updates', 'removes'] attrs = ['queries', 'inserts', 'updates', 'removes']
total_time = sum(sum(o['time'] for o in getattr(self.op_tracker, a)) total_time = sum(sum(o['time'] for o in getattr(operation_tracker, a))
for a in attrs) for a in attrs)
return '{0} operations in {1:.2f}ms'.format(num_queries, total_time) return '{0} operations in {1:.2f}ms'.format(num_queries, total_time)
...@@ -41,10 +36,10 @@ class MongoDebugPanel(DebugPanel): ...@@ -41,10 +36,10 @@ class MongoDebugPanel(DebugPanel):
def content(self): def content(self):
context = self.context.copy() context = self.context.copy()
context['queries'] = self.op_tracker.queries context['queries'] = operation_tracker.queries
context['inserts'] = self.op_tracker.inserts context['inserts'] = operation_tracker.inserts
context['updates'] = self.op_tracker.updates context['updates'] = operation_tracker.updates
context['removes'] = self.op_tracker.removes context['removes'] = operation_tracker.removes
return render_to_string('mongo-panel.html', context) return render_to_string('mongo-panel.html', context)
...@@ -5,11 +5,23 @@ pre.mongo-highlight, pre.mongo-highlight span { ...@@ -5,11 +5,23 @@ pre.mongo-highlight, pre.mongo-highlight span {
font-family: 'Consolas', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', 'Monaco', 'Courier New', monospace !important; font-family: 'Consolas', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', 'Monaco', 'Courier New', monospace !important;
font-size: 1.05em !important; font-size: 1.05em !important;
} }
pre.mongo-stack-trace {
line-height: 2.3em !important;
}
table.mongo-stack-trace th {
text-align: left !important;
}
table.mongo-stack-trace td.code, table.mongo-stack-trace td.code span, table.mongo-stack-trace td.function {
font-family: 'Consolas', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', 'Monaco', 'Courier New', monospace !important;
}
table.mongo-op-table tbody {
width: 100% !important;
}
</style> </style>
<h4>Queries</h4> <h4>Queries</h4>
{% if queries %} {% if queries %}
<table> <table class="mongo-op-table">
<thead> <thead>
<tr> <tr>
<th>Time (ms)</th> <th>Time (ms)</th>
...@@ -19,6 +31,7 @@ pre.mongo-highlight, pre.mongo-highlight span { ...@@ -19,6 +31,7 @@ pre.mongo-highlight, pre.mongo-highlight span {
<th>Ordering</th> <th>Ordering</th>
<th>Skip</th> <th>Skip</th>
<th>Limit</th> <th>Limit</th>
<th>Stack Trace</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
...@@ -29,12 +42,38 @@ pre.mongo-highlight, pre.mongo-highlight span { ...@@ -29,12 +42,38 @@ pre.mongo-highlight, pre.mongo-highlight span {
<td>{{ query.collection }}</td> <td>{{ query.collection }}</td>
<td> <td>
{% if query.query %} {% if query.query %}
<pre class="mongo-highlight">{{ query.query|format_dict|highlight_json|safe }}</pre> <pre class="mongo-highlight">{{ query.query|format_dict|highlight:"javascript"|safe }}</pre>
{% endif %} {% endif %}
</td> </td>
<td><pre class="mongo-highlight">{% if query.ordering %}{{ query.ordering }}{% endif %}</pre></td> <td><pre class="mongo-highlight">{% if query.ordering %}{{ query.ordering }}{% endif %}</pre></td>
<td>{% if query.skip %}{{ query.skip }}{% endif %}</td> <td>{% if query.skip %}{{ query.skip }}{% endif %}</td>
<td>{% if query.limit %}{{ query.limit }}{% endif %}</td> <td>{% if query.limit %}{{ query.limit }}{% endif %}</td>
<td><a href="javascript:void(0);" class="mongo-toggle-trace" data-row="{{ forloop.counter }}">Toggle</a></td>
</tr>
<tr class="{% cycle 'djDebugOdd' 'djDebugEven' %}">
<td colspan="8">
<table class="mongo-stack-trace "id="mongo-stack-trace-{{ forloop.counter }}">
<thead>
<tr>
<th>Line</th>
<th>File</th>
<th>Function</th>
<th>Code</th>
</tr>
</thead>
<tbody>
{% for line in query.stack_trace %}
<tr>
<td class="lineno">{{ line.1 }}</td>
<td class="file">{{ line.0 }}</td>
<td class="function">{{ line.2 }}</td>
<td class="code">{{ line.3|highlight:"python"|safe }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<!--<pre class="mongo-stack-trace">{{ query.stack_trace|format_stack_trace }}</pre>-->
</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
...@@ -45,7 +84,7 @@ pre.mongo-highlight, pre.mongo-highlight span { ...@@ -45,7 +84,7 @@ pre.mongo-highlight, pre.mongo-highlight span {
<h4>Inserts</h4> <h4>Inserts</h4>
{% if inserts %} {% if inserts %}
<table> <table class="mongo-op-table">
<thead> <thead>
<tr> <tr>
<th>Time (ms)</th> <th>Time (ms)</th>
...@@ -58,7 +97,7 @@ pre.mongo-highlight, pre.mongo-highlight span { ...@@ -58,7 +97,7 @@ pre.mongo-highlight, pre.mongo-highlight span {
<tr class="{% cycle 'djDebugOdd' 'djDebugEven' %}"> <tr class="{% cycle 'djDebugOdd' 'djDebugEven' %}">
<td>{{ insert.time|floatformat:"4" }}</td> <td>{{ insert.time|floatformat:"4" }}</td>
<td> <td>
<pre class="mongo-highlight">{{ insert.document|format_dict:120|highlight_json|safe }}</pre> <pre class="mongo-highlight">{{ insert.document|format_dict:120|highlight:"javascript"|safe }}</pre>
</td> </td>
<td>{{ insert.safe }}</td> <td>{{ insert.safe }}</td>
</tr> </tr>
...@@ -71,7 +110,7 @@ pre.mongo-highlight, pre.mongo-highlight span { ...@@ -71,7 +110,7 @@ pre.mongo-highlight, pre.mongo-highlight span {
<h4>Removes</h4> <h4>Removes</h4>
{% if removes %} {% if removes %}
<table> <table class="mongo-op-table">
<thead> <thead>
<tr> <tr>
<th>Time (ms)</th> <th>Time (ms)</th>
...@@ -84,7 +123,7 @@ pre.mongo-highlight, pre.mongo-highlight span { ...@@ -84,7 +123,7 @@ pre.mongo-highlight, pre.mongo-highlight span {
<tr class="{% cycle 'djDebugOdd' 'djDebugEven' %}"> <tr class="{% cycle 'djDebugOdd' 'djDebugEven' %}">
<td>{{ remove.time|floatformat:"4" }}</td> <td>{{ remove.time|floatformat:"4" }}</td>
<td> <td>
<pre class="mongo-highlight">{{ remove.spec_or_id|format_dict:120|highlight_json|safe }}</pre> <pre class="mongo-highlight">{{ remove.spec_or_id|format_dict:120|highlight:"javascript"|safe }}</pre>
</td> </td>
<td>{{ remove.safe }}</td> <td>{{ remove.safe }}</td>
</tr> </tr>
...@@ -97,7 +136,7 @@ pre.mongo-highlight, pre.mongo-highlight span { ...@@ -97,7 +136,7 @@ pre.mongo-highlight, pre.mongo-highlight span {
<h4>Updates</h4> <h4>Updates</h4>
{% if updates %} {% if updates %}
<table> <table class="mongo-op-table">
<thead> <thead>
<tr> <tr>
<th>Time (ms)</th> <th>Time (ms)</th>
...@@ -113,10 +152,10 @@ pre.mongo-highlight, pre.mongo-highlight span { ...@@ -113,10 +152,10 @@ pre.mongo-highlight, pre.mongo-highlight span {
<tr class="{% cycle 'djDebugOdd' 'djDebugEven' %}"> <tr class="{% cycle 'djDebugOdd' 'djDebugEven' %}">
<td>{{ update.time|floatformat:"4" }}</td> <td>{{ update.time|floatformat:"4" }}</td>
<td> <td>
<pre class="mongo-highlight">{{ update.spec|format_dict:120|highlight_json|safe }}</pre> <pre class="mongo-highlight">{{ update.spec|format_dict:120|highlight:"javascript"|safe }}</pre>
</td> </td>
<td> <td>
<pre class="mongo-highlight">{{ update.document|format_dict:120|highlight_json|safe }}</pre> <pre class="mongo-highlight">{{ update.document|format_dict:120|highlight:"javascript"|safe }}</pre>
</td> </td>
<td>{{ update.safe }}</td> <td>{{ update.safe }}</td>
<td>{{ update.multi }}</td> <td>{{ update.multi }}</td>
...@@ -129,3 +168,24 @@ pre.mongo-highlight, pre.mongo-highlight span { ...@@ -129,3 +168,24 @@ pre.mongo-highlight, pre.mongo-highlight span {
<p>No updates recorded</p> <p>No updates recorded</p>
{% endif %} {% endif %}
<script>
(function() {
var toggleButtons = document.querySelectorAll('a.mongo-toggle-trace');
for (var i = 0; i < toggleButtons.length; i++) {
(function() {
var button = toggleButtons[i];
var index = button.dataset.row;
var trace = document.getElementById('mongo-stack-trace-' + index);
trace.style.display = "none";
button.addEventListener("click", function() {
if (trace.style.display == "none") {
trace.style.display = "";
} else {
trace.style.display = "none";
}
}, true);
})();
}
})();
</script>
from django import template from django import template
from django.utils.html import escape
from django.utils.safestring import mark_safe
import pprint import pprint
register = template.Library() register = template.Library()
@register.filter @register.filter
def format_stack_trace(value):
stack_trace = []
fmt = (
'<span class="path">{0}/</span>'
'<span class="file">{1}</span> in <span class="func">{3}</span>'
'(<span class="lineno">{2}</span>) <span class="code">{4}</span>'
)
for frame in value:
params = map(escape, frame[0].rsplit('/', 1) + list(frame[1:]))
stack_trace.append(fmt.format(*params))
return mark_safe('\n'.join(stack_trace))
@register.filter
def format_dict(value, width=60): def format_dict(value, width=60):
return pprint.pformat(value, width=int(width)) return pprint.pformat(value, width=int(width))
@register.filter @register.filter
def highlight_json(value): def highlight(value, language):
try: try:
from pygments import highlight from pygments import highlight
from pygments.lexers import JavascriptLexer from pygments.lexers import get_lexer_by_name
from pygments.formatters import HtmlFormatter from pygments.formatters import HtmlFormatter
except ImportError: except ImportError:
return value return value
# Can't use class-based colouring because the debug toolbar's css rules # Can't use class-based colouring because the debug toolbar's css rules
# are more specific so take precedence # are more specific so take precedence
formatter = HtmlFormatter(style='friendly', nowrap=True, noclasses=True) formatter = HtmlFormatter(style='friendly', nowrap=True, noclasses=True)
return highlight(value, JavascriptLexer(), formatter) return highlight(value, get_lexer_by_name(language), formatter)
...@@ -37,10 +37,10 @@ DEBUG_TOOLBAR_PANELS = ( ...@@ -37,10 +37,10 @@ DEBUG_TOOLBAR_PANELS = (
'debug_toolbar.panels.headers.HeaderDebugPanel', 'debug_toolbar.panels.headers.HeaderDebugPanel',
'debug_toolbar.panels.request_vars.RequestVarsDebugPanel', 'debug_toolbar.panels.request_vars.RequestVarsDebugPanel',
'debug_toolbar.panels.template.TemplateDebugPanel', 'debug_toolbar.panels.template.TemplateDebugPanel',
'debug_toolbar_mongo.panel.MongoDebugPanel',
'debug_toolbar.panels.sql.SQLDebugPanel', 'debug_toolbar.panels.sql.SQLDebugPanel',
'debug_toolbar.panels.signals.SignalDebugPanel', 'debug_toolbar.panels.signals.SignalDebugPanel',
'debug_toolbar.panels.logger.LoggingPanel', 'debug_toolbar.panels.logger.LoggingPanel',
'debug_toolbar_mongo.panel.MongoDebugPanel',
) )
ROOT_URLCONF = 'example.urls' ROOT_URLCONF = 'example.urls'
......
...@@ -8,7 +8,9 @@ conn = pymongo.Connection() ...@@ -8,7 +8,9 @@ conn = pymongo.Connection()
db = conn.debug_test db = conn.debug_test
def index(request): def index(request):
list(db.test.find({'name': 'test'})) #list(db.test.find({'name': 'test'}))
db.test.find({'name': 'test'}).count()
db.test.find({'name': 'test'}).count()
list(db.test.find({'name': 'test', 'age': {'$lt': 134234}}).skip(1)) list(db.test.find({'name': 'test', 'age': {'$lt': 134234}}).skip(1))
db.test.find({'name': 'test'}).count() db.test.find({'name': 'test'}).count()
db.test.find({'name': 'test'}).skip(1).count(with_limit_and_skip=True) db.test.find({'name': 'test'}).skip(1).count(with_limit_and_skip=True)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment