plain.querystats
On-page database query stats in development and production.
On each page, the query stats will display how many database queries were performed and how long they took.
Clicking the stats in the toolbar will show the full SQL query log with tracebacks and timings. This is even designed to work in production, making it much easier to discover and debug performance issues on production data!
It will also point out duplicate queries,
which can typically be removed by using select_related
,
prefetch_related
, or otherwise refactoring your code.
Installation
# settings.py
INSTALLED_PACKAGES = [
# ...
"plain.admin.querystats",
]
MIDDLEWARE = [
"plain.sessions.middleware.SessionMiddleware",
"plain.auth.middleware.AuthenticationMiddleware",
"plain.admin.querystats.QueryStatsMiddleware",
# Put additional middleware below querystats
# ...
]
We strongly recommend using the plain-toolbar along with this, but if you aren't, you can add the querystats to your frontend templates with this include:
{% include "querystats/button.html" %}
Note that you will likely want to surround this with an if DEBUG
or is_admin
check.
To view querystats you need to send a POST request to ?querystats=store
(i.e. via a <form>
),
and the template include is the easiest way to do that.
Tailwind CSS
This package is styled with Tailwind CSS,
and pairs well with plain-tailwind
.
If you are using your own Tailwind implementation, you can modify the "content" in your Tailwind config to include any Plain packages:
// tailwind.config.js
module.exports = {
content: [
// ...
".venv/lib/python*/site-packages/plain*/**/*.{html,js}",
],
// ...
}
If you aren't using Tailwind, and don't intend to, open an issue to discuss other options.
plain.toolbar
The admin toolbar is enabled for every user who is_admin
.
Installation
Add plaintoolbar
to your INSTALLED_PACKAGES
,
and the {% toolbar %}
to your base template:
# settings.py
INSTALLED_PACKAGES += [
"plaintoolbar",
]
<!-- base.template.html -->
{% load toolbar %}
<!doctype html>
<html lang="en">
<head>
...
</head>
<body>
{% toolbar %}
...
</body>
More specific settings can be found below.
Tailwind CSS
This package is styled with Tailwind CSS,
and pairs well with plain-tailwind
.
If you are using your own Tailwind implementation, you can modify the "content" in your Tailwind config to include any Plain packages:
// tailwind.config.js
module.exports = {
content: [
// ...
".venv/lib/python*/site-packages/plain*/**/*.{html,js}",
],
// ...
}
If you aren't using Tailwind, and don't intend to, open an issue to discuss other options.
plain.requestlog
The request log stores a local history of HTTP requests and responses during plain work
(Django runserver).
The request history will make it easy to see redirects, 400 and 500 level errors, form submissions, API calls, webhooks, and more.
Requests can be re-submitted by clicking the "replay" button.
Installation
# settings.py
INSTALLED_PACKAGES += [
"plainrequestlog",
]
MIDDLEWARE = MIDDLEWARE + [
# ...
"plainrequestlog.RequestLogMiddleware",
]
The default settings can be customized if needed:
# settings.py
DEV_REQUESTS_IGNORE_PATHS = [
"/sw.js",
"/favicon.ico",
"/admin/jsi18n/",
]
DEV_REQUESTS_MAX = 50
Tailwind CSS
This package is styled with Tailwind CSS,
and pairs well with plain-tailwind
.
If you are using your own Tailwind implementation, you can modify the "content" in your Tailwind config to include any Plain packages:
// tailwind.config.js
module.exports = {
content: [
// ...
".venv/lib/python*/site-packages/plain*/**/*.{html,js}",
],
// ...
}
If you aren't using Tailwind, and don't intend to, open an issue to discuss other options.
1import json
2import logging
3import re
4import threading
5
6from plain.http import ResponseRedirect
7from plain.json import PlainJSONEncoder
8from plain.models import connection
9from plain.runtime import settings
10from plain.urls import reverse
11
12from .core import QueryStats
13
14try:
15 try:
16 import psycopg
17 except ImportError:
18 import psycopg2 as psycopg
19except ImportError:
20 psycopg = None
21
22logger = logging.getLogger(__name__)
23_local = threading.local()
24
25
26class QueryStatsJSONEncoder(PlainJSONEncoder):
27 def default(self, obj):
28 try:
29 return super().default(obj)
30 except TypeError:
31 if psycopg and isinstance(obj, psycopg._json.Json):
32 return obj.adapted
33 else:
34 raise
35
36
37class QueryStatsMiddleware:
38 def __init__(self, get_response):
39 self.get_response = get_response
40 self.ignore_url_patterns = [
41 re.compile(url) for url in settings.QUERYSTATS_IGNORE_URLS
42 ]
43
44 def should_ignore_request(self, request):
45 for url in self.ignore_url_patterns:
46 if url.match(request.path):
47 return True
48
49 return False
50
51 def __call__(self, request):
52 if request.GET.get("querystats") == "disable":
53 return self.get_response(request)
54
55 querystats = QueryStats(
56 # Only want these if we're getting ready to show it
57 include_tracebacks=request.GET.get("querystats") == "store"
58 )
59
60 with connection.execute_wrapper(querystats):
61 # Have to wrap this first call so it is included in the querystats,
62 # but we don't have to wrap everything else unless we are admin or debug
63 is_admin = self.is_admin_request(request)
64
65 if (settings.DEBUG or is_admin) and not self.should_ignore_request(request):
66 # Persist it on the thread
67 _local.querystats = querystats
68
69 with connection.execute_wrapper(_local.querystats):
70 response = self.get_response(request)
71
72 if settings.DEBUG:
73 # TODO logging settings
74 logger.debug("Querystats: %s", _local.querystats)
75
76 # Make current querystats available on the current page
77 # by using the server timing API which can be parsed client-side
78 response.headers["Server-Timing"] = _local.querystats.as_server_timing()
79
80 if request.GET.get("querystats") == "store":
81 request.session["querystats"] = json.dumps(
82 _local.querystats.as_context_dict(), cls=QueryStatsJSONEncoder
83 )
84 return ResponseRedirect(reverse("querystats:querystats"))
85
86 del _local.querystats
87
88 return response
89
90 else:
91 return self.get_response(request)
92
93 @staticmethod
94 def is_admin_request(request):
95 if getattr(request, "impersonator", None):
96 # Support for impersonation (still want the real admin user to see the querystats)
97 return request.impersonator and request.impersonator.is_admin
98
99 return hasattr(request, "user") and request.user and request.user.is_admin