Skip to content

Commit f59ffd0

Browse files
authored
Merge pull request #2 from aclark4life/main
Add MQL panel
2 parents 97dc363 + 9fbb4c1 commit f59ffd0

File tree

10 files changed

+267
-30
lines changed

10 files changed

+267
-30
lines changed

.github/workflows/django.yml

Lines changed: 0 additions & 30 deletions
This file was deleted.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
__pycache__

.pre-commit-config.yaml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# See https://pre-commit.com for more information
2+
# See https://pre-commit.com/hooks.html for more hooks
3+
repos:
4+
- repo: https://github.com/pre-commit/pre-commit-hooks
5+
rev: v3.2.0
6+
hooks:
7+
- id: trailing-whitespace
8+
- id: end-of-file-fixer
9+
- id: check-yaml
10+
- id: check-added-large-files
11+
- repo: https://github.com/astral-sh/ruff-pre-commit
12+
rev: v0.7.3
13+
hooks:
14+
- id: ruff
15+
args: [ --fix ]
16+
- id: ruff-format
17+
18+
- repo: https://github.com/djlint/djLint
19+
rev: v1.36.3
20+
hooks:
21+
- id: djlint-reformat-django
22+
- id: djlint-django

django_mongodb_extensions/__init__.py

Whitespace-only changes.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from django_mongodb_extensions.debug_toolbar.panels.mql.panel import MQLPanel
2+
3+
__all__ = ["MQLPanel"]
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
from django.db import connections
2+
from django.utils.translation import gettext_lazy as _, ngettext
3+
4+
from debug_toolbar.panels.sql.panel import SQLPanel
5+
from django_mongodb_extensions.debug_toolbar.panels.mql.tracking import (
6+
patch_get_collection,
7+
)
8+
9+
10+
class MQLPanel(SQLPanel):
11+
"""
12+
Panel that displays information about the MQL queries run while processing
13+
the request.
14+
"""
15+
16+
def __init__(self, *args, **kwargs):
17+
super().__init__(*args, **kwargs)
18+
self._sql_time = 0
19+
self._queries = []
20+
self._databases = {}
21+
22+
# Implement Panel API
23+
24+
nav_title = _("MQL")
25+
template = "debug_toolbar/panels/mql.html"
26+
27+
@property
28+
def nav_subtitle(self):
29+
query_count = len(self._queries)
30+
return ngettext(
31+
"%(query_count)d query in %(sql_time).2fms",
32+
"%(query_count)d queries in %(sql_time).2fms",
33+
query_count,
34+
) % {
35+
"query_count": query_count,
36+
"sql_time": self._sql_time,
37+
}
38+
39+
@property
40+
def title(self):
41+
count = len(self._databases)
42+
return ngettext(
43+
"MQL queries from %(count)d connection",
44+
"MQL queries from %(count)d connections",
45+
count,
46+
) % {"count": count}
47+
48+
def enable_instrumentation(self):
49+
# This is thread-safe because database connections are thread-local.
50+
for connection in connections.all():
51+
patch_get_collection(connection)
52+
connection._djdt_logger = self
53+
54+
def disable_instrumentation(self):
55+
for connection in connections.all():
56+
connection._djdt_logger = None
57+
58+
def generate_stats(self, request, response):
59+
self.record_stats(
60+
{
61+
"databases": sorted(self._databases.items()),
62+
"queries": self._queries,
63+
"sql_time": self._sql_time,
64+
}
65+
)
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import types
2+
3+
from django_mongodb_backend.utils import OperationDebugWrapper
4+
from pymongo.collection import Collection
5+
6+
7+
def patch_get_collection(connection):
8+
"""
9+
Patch the get_collection method of the connection to return a wrapped
10+
Collection object that logs queries for the debug toolbar.
11+
"""
12+
13+
def get_collection(self, name, **kwargs):
14+
return DebugToolbarWrapper(
15+
self, Collection(self.database, name, **kwargs), connection._djdt_logger
16+
)
17+
18+
connection.get_collection = types.MethodType(get_collection, connection)
19+
20+
21+
class DebugToolbarWrapper(OperationDebugWrapper):
22+
"""
23+
A wrapper around pymongo Collection objects that logs queries for the
24+
debug toolbar.
25+
"""
26+
27+
def __init__(self, db, collection, logger):
28+
super().__init__(db, collection)
29+
self.collection_name = collection.name
30+
self.logger = logger
31+
32+
def log(self, op, duration, args, kwargs=None):
33+
operation = f"db.{self.collection_name}{op}({args})"
34+
if self.logger:
35+
self.logger._queries.append(
36+
{
37+
"sql": operation,
38+
"time": "%.3f" % duration,
39+
}
40+
)
41+
self.logger._databases[self.db.alias] = {
42+
"num_queries": len(self.logger._queries),
43+
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
{% load i18n l10n %}
2+
<ul>
3+
{% for alias, info in databases %}
4+
<li>
5+
<strong><span class="djdt-color" data-djdt-styles="backgroundColor:rgb({{ info.rgb_color|join:', ' }})"></span> {{ alias }}</strong>
6+
{{ info.time_spent|floatformat:"2" }} ms ({% blocktrans count info.num_queries as num %}{{ num }} query{% plural %}{{ num }} queries{% endblocktrans %}
7+
{% if info.similar_count %}
8+
{% blocktrans with count=info.similar_count trimmed %}
9+
including <abbr title="Similar queries are queries with the same SQL, but potentially different parameters.">{{ count }} similar</abbr>
10+
{% endblocktrans %}
11+
{% if info.duplicate_count %}
12+
{% blocktrans with dupes=info.duplicate_count trimmed %}
13+
and <abbr title="Duplicate queries are identical to each other: they execute exactly the same SQL and parameters.">{{ dupes }} duplicates</abbr>
14+
{% endblocktrans %}
15+
{% endif %}
16+
{% endif %})
17+
</li>
18+
{% endfor %}
19+
</ul>
20+
21+
{% if queries %}
22+
<table>
23+
<colgroup>
24+
<col>
25+
<col>
26+
<col>
27+
<col class="djdt-width-30">
28+
<col>
29+
<col>
30+
</colgroup>
31+
<thead>
32+
<tr>
33+
<th></th>
34+
<th colspan="2">{% trans "Query" %}</th>
35+
<th>{% trans "Timeline" %}</th>
36+
<th>{% trans "Time (ms)" %}</th>
37+
<th>{% trans "Action" %}</th>
38+
</tr>
39+
</thead>
40+
<tbody>
41+
{% for query in queries %}
42+
<tr class="{% if query.is_slow %} djDebugRowWarning{% endif %}" id="sqlMain_{{ forloop.counter }}">
43+
<td><span class="djdt-color" data-djdt-styles="backgroundColor:rgb({{ query.rgb_color|join:', '}})"></span></td>
44+
<td class="djdt-toggle">
45+
<button type="button" class="djToggleSwitch" data-toggle-name="sqlMain" data-toggle-id="{{ forloop.counter }}">+</button>
46+
</td>
47+
<td>
48+
<div class="djDebugSql">{{ query.sql|safe }}</div>
49+
{% if query.similar_count %}
50+
<strong>
51+
<span class="djdt-color" data-djdt-styles="backgroundColor:{{ query.similar_color }}"></span>
52+
{% blocktrans with count=query.similar_count %}{{ count }} similar queries.{% endblocktrans %}
53+
</strong>
54+
{% endif %}
55+
{% if query.duplicate_count %}
56+
<strong>
57+
<span class="djdt-color" data-djdt-styles="backgroundColor:{{ query.duplicate_color }}"></span>
58+
{% blocktrans with dupes=query.duplicate_count %}Duplicated {{ dupes }} times.{% endblocktrans %}
59+
</strong>
60+
{% endif %}
61+
</td>
62+
<td>
63+
<svg class="djDebugLineChart{% if query.is_slow %} djDebugLineChartWarning{% endif %}{% if query.in_trans %} djDebugLineChartInTransaction{% endif %}" xmlns="http://www.w3.org/2000/svg" viewbox="0 0 100 5" preserveAspectRatio="none" aria-label="{{ query.width_ratio }}%">
64+
<rect x="{{ query.start_offset|unlocalize }}" y="0" height="5" width="{{ query.width_ratio|unlocalize }}" fill="{{ query.trace_color }}" />
65+
{% if query.starts_trans %}
66+
<line x1="{{ query.start_offset|unlocalize }}" y1="0" x2="{{ query.start_offset|unlocalize }}" y2="5" />
67+
{% endif %}
68+
{% if query.ends_trans %}
69+
<line x1="{{ query.end_offset|unlocalize }}" y1="0" x2="{{ query.end_offset|unlocalize }}" y2="5" />
70+
{% endif %}
71+
</svg>
72+
</td>
73+
<td class="djdt-time">
74+
{{ query.duration|floatformat:"2" }}
75+
</td>
76+
<td class="djdt-actions">
77+
{% if query.params %}
78+
{% if query.is_select %}
79+
<form method="post">
80+
{{ query.form.as_div }}
81+
<button formaction="{% url 'djdt:sql_select' %}" class="remoteCall">Sel</button>
82+
<button formaction="{% url 'djdt:sql_explain' %}" class="remoteCall">Expl</button>
83+
{% if query.vendor == 'mysql' %}
84+
<button formaction="{% url 'djdt:sql_profile' %}" class="remoteCall">Prof</button>
85+
{% endif %}
86+
</form>
87+
{% endif %}
88+
{% endif %}
89+
</td>
90+
</tr>
91+
<tr class="djUnselected {% if query.is_slow %} djDebugRowWarning{% endif %} djToggleDetails_{{ forloop.counter }}" id="sqlDetails_{{ forloop.counter }}">
92+
<td colspan="2"></td>
93+
<td colspan="4">
94+
<div class="djSQLDetailsDiv">
95+
<p><strong>{% trans "Connection:" %}</strong> {{ query.alias }}</p>
96+
{% if query.iso_level %}
97+
<p><strong>{% trans "Isolation level:" %}</strong> {{ query.iso_level }}</p>
98+
{% endif %}
99+
{% if query.trans_status %}
100+
<p><strong>{% trans "Transaction status:" %}</strong> {{ query.trans_status }}</p>
101+
{% endif %}
102+
{% if query.stacktrace %}
103+
<pre class="djdt-stack">{{ query.stacktrace }}</pre>
104+
{% endif %}
105+
{% if query.template_info %}
106+
<table>
107+
{% for line in query.template_info.context %}
108+
<tr>
109+
<td>{{ line.num }}</td>
110+
<td><code {% if line.highlight %}class="djdt-highlighted"{% endif %}>{{ line.content }}</code></td>
111+
</tr>
112+
{% endfor %}
113+
</table>
114+
<p><strong>{{ query.template_info.name|default:_("(unknown)") }}</strong></p>
115+
{% endif %}
116+
</div>
117+
</td>
118+
</tr>
119+
{% endfor %}
120+
</tbody>
121+
</table>
122+
{% else %}
123+
<p>{% trans "No MQL queries were recorded during this request." %}</p>
124+
{% endif %}

justfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
default:
2+
echo 'Hello, world!'

pyproject.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[build-system]
2+
requires = ["hatchling"]
3+
build-backend = "hatchling.build"
4+
5+
[project]
6+
name = "django-mongodb-extensions"
7+
version = "0.1.0"

0 commit comments

Comments
 (0)