django-devbar 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- django_devbar-0.1.0/PKG-INFO +82 -0
- django_devbar-0.1.0/README.md +51 -0
- django_devbar-0.1.0/pyproject.toml +52 -0
- django_devbar-0.1.0/src/django_devbar/__init__.py +4 -0
- django_devbar-0.1.0/src/django_devbar/conf.py +21 -0
- django_devbar-0.1.0/src/django_devbar/middleware.py +89 -0
- django_devbar-0.1.0/src/django_devbar/tracker.py +54 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: django-devbar
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Lightweight performance devbar for Django
|
|
5
|
+
Keywords: django,debug,performance,monitoring,development
|
|
6
|
+
Author: amureki
|
|
7
|
+
Author-email: amureki <oss@amureki.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Classifier: Programming Language :: Python
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Classifier: Framework :: Django
|
|
18
|
+
Classifier: Framework :: Django :: 4.2
|
|
19
|
+
Classifier: Framework :: Django :: 5.0
|
|
20
|
+
Classifier: Framework :: Django :: 5.1
|
|
21
|
+
Classifier: Framework :: Django :: 5.2
|
|
22
|
+
Classifier: Framework :: Django :: 6.0
|
|
23
|
+
Classifier: Topic :: Software Development
|
|
24
|
+
Classifier: Intended Audience :: Developers
|
|
25
|
+
Requires-Dist: django>=4.2
|
|
26
|
+
Requires-Python: >=3.10
|
|
27
|
+
Project-URL: Homepage, https://github.com/amureki/django-devbar
|
|
28
|
+
Project-URL: Issues, https://github.com/amureki/django-devbar/issues
|
|
29
|
+
Project-URL: Repository, https://github.com/amureki/django-devbar
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# django-devbar
|
|
33
|
+
|
|
34
|
+
Lightweight performance devbar for Django. Shows DB query count, query duration, and response time.
|
|
35
|
+
|
|
36
|
+

|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install django-devbar
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Add to your middleware as early as possible, but after any middleware that encodes the response (e.g., `GZipMiddleware`):
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
MIDDLEWARE = [
|
|
48
|
+
"django.middleware.security.SecurityMiddleware",
|
|
49
|
+
"django_devbar.DevBarMiddleware",
|
|
50
|
+
# ...
|
|
51
|
+
]
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Configuration
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
# Position: bottom-right, bottom-left, top-right, top-left (default: bottom-right)
|
|
58
|
+
DEVBAR_POSITION = "top-left"
|
|
59
|
+
|
|
60
|
+
# Show HTML overlay (default: DEBUG)
|
|
61
|
+
DEVBAR_SHOW_BAR = True
|
|
62
|
+
|
|
63
|
+
# Add X-DevBar-* response headers (default: False)
|
|
64
|
+
DEVBAR_SHOW_HEADERS = True
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Response Headers
|
|
68
|
+
|
|
69
|
+
When `DEVBAR_SHOW_HEADERS = True`, performance metrics are added as HTTP response headers. This is useful for:
|
|
70
|
+
|
|
71
|
+
- **API endpoints** where the HTML overlay can't be displayed
|
|
72
|
+
- **Automated testing** to assert performance thresholds (e.g., fail CI if query count exceeds a limit)
|
|
73
|
+
- **Monitoring tools** that can capture and aggregate header values
|
|
74
|
+
|
|
75
|
+
Headers included:
|
|
76
|
+
|
|
77
|
+
| Header | Description |
|
|
78
|
+
|--------|-------------|
|
|
79
|
+
| `X-DevBar-Query-Count` | Number of database queries executed |
|
|
80
|
+
| `X-DevBar-Query-Duration` | Total time spent in database queries (ms) |
|
|
81
|
+
| `X-DevBar-Response-Time` | Total request-response cycle time (ms) |
|
|
82
|
+
| `X-DevBar-Has-Duplicates` | Present (value `1`) if duplicate queries detected |
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# django-devbar
|
|
2
|
+
|
|
3
|
+
Lightweight performance devbar for Django. Shows DB query count, query duration, and response time.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install django-devbar
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Add to your middleware as early as possible, but after any middleware that encodes the response (e.g., `GZipMiddleware`):
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
MIDDLEWARE = [
|
|
17
|
+
"django.middleware.security.SecurityMiddleware",
|
|
18
|
+
"django_devbar.DevBarMiddleware",
|
|
19
|
+
# ...
|
|
20
|
+
]
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Configuration
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
# Position: bottom-right, bottom-left, top-right, top-left (default: bottom-right)
|
|
27
|
+
DEVBAR_POSITION = "top-left"
|
|
28
|
+
|
|
29
|
+
# Show HTML overlay (default: DEBUG)
|
|
30
|
+
DEVBAR_SHOW_BAR = True
|
|
31
|
+
|
|
32
|
+
# Add X-DevBar-* response headers (default: False)
|
|
33
|
+
DEVBAR_SHOW_HEADERS = True
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Response Headers
|
|
37
|
+
|
|
38
|
+
When `DEVBAR_SHOW_HEADERS = True`, performance metrics are added as HTTP response headers. This is useful for:
|
|
39
|
+
|
|
40
|
+
- **API endpoints** where the HTML overlay can't be displayed
|
|
41
|
+
- **Automated testing** to assert performance thresholds (e.g., fail CI if query count exceeds a limit)
|
|
42
|
+
- **Monitoring tools** that can capture and aggregate header values
|
|
43
|
+
|
|
44
|
+
Headers included:
|
|
45
|
+
|
|
46
|
+
| Header | Description |
|
|
47
|
+
|--------|-------------|
|
|
48
|
+
| `X-DevBar-Query-Count` | Number of database queries executed |
|
|
49
|
+
| `X-DevBar-Query-Duration` | Total time spent in database queries (ms) |
|
|
50
|
+
| `X-DevBar-Response-Time` | Total request-response cycle time (ms) |
|
|
51
|
+
| `X-DevBar-Has-Duplicates` | Present (value `1`) if duplicate queries detected |
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "django-devbar"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Lightweight performance devbar for Django"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
authors = [
|
|
8
|
+
{ name = "amureki", email = "oss@amureki.com" },
|
|
9
|
+
]
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
dependencies = ["django>=4.2"]
|
|
12
|
+
keywords = ["django", "debug", "performance", "monitoring", "development"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Programming Language :: Python",
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
17
|
+
"Programming Language :: Python :: 3.10",
|
|
18
|
+
"Programming Language :: Python :: 3.11",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
"Programming Language :: Python :: 3.13",
|
|
21
|
+
"Programming Language :: Python :: 3.14",
|
|
22
|
+
"Framework :: Django",
|
|
23
|
+
"Framework :: Django :: 4.2",
|
|
24
|
+
"Framework :: Django :: 5.0",
|
|
25
|
+
"Framework :: Django :: 5.1",
|
|
26
|
+
"Framework :: Django :: 5.2",
|
|
27
|
+
"Framework :: Django :: 6.0",
|
|
28
|
+
"Topic :: Software Development",
|
|
29
|
+
"Intended Audience :: Developers",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://github.com/amureki/django-devbar"
|
|
34
|
+
Repository = "https://github.com/amureki/django-devbar"
|
|
35
|
+
Issues = "https://github.com/amureki/django-devbar/issues"
|
|
36
|
+
|
|
37
|
+
[build-system]
|
|
38
|
+
requires = ["uv_build>=0.9.16,<0.10.0"]
|
|
39
|
+
build-backend = "uv_build"
|
|
40
|
+
|
|
41
|
+
[dependency-groups]
|
|
42
|
+
dev = [
|
|
43
|
+
"pytest",
|
|
44
|
+
"pytest-django",
|
|
45
|
+
"ruff",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
[tool.pytest]
|
|
49
|
+
pythonpath = ["src", "."]
|
|
50
|
+
testpaths = ["tests"]
|
|
51
|
+
norecursedirs = [".venv", ".git"]
|
|
52
|
+
DJANGO_SETTINGS_MODULE = "tests.settings"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from django.conf import settings
|
|
2
|
+
|
|
3
|
+
POSITIONS = {
|
|
4
|
+
"bottom-right": "bottom:0;right:0",
|
|
5
|
+
"bottom-left": "bottom:0;left:0",
|
|
6
|
+
"top-right": "top:0;right:0",
|
|
7
|
+
"top-left": "top:0;left:0",
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_position():
|
|
12
|
+
key = getattr(settings, "DEVBAR_POSITION", "bottom-right")
|
|
13
|
+
return POSITIONS.get(key, POSITIONS["bottom-right"])
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_show_bar():
|
|
17
|
+
return getattr(settings, "DEVBAR_SHOW_BAR", settings.DEBUG)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_show_headers():
|
|
21
|
+
return getattr(settings, "DEVBAR_SHOW_HEADERS", False)
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from contextlib import ExitStack
|
|
3
|
+
from time import perf_counter
|
|
4
|
+
|
|
5
|
+
from django.db import connections
|
|
6
|
+
|
|
7
|
+
from . import tracker
|
|
8
|
+
from .conf import get_position, get_show_bar, get_show_headers
|
|
9
|
+
|
|
10
|
+
DEVBAR_HTML = (
|
|
11
|
+
'<div id="django-devbar" style="position:fixed;{position};'
|
|
12
|
+
"background:rgba(0,0,0,0.7);color:rgba(255,255,255,0.85);"
|
|
13
|
+
"padding:4px 8px;margin:8px;"
|
|
14
|
+
"font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;"
|
|
15
|
+
"font-size:10px;line-height:1.3;font-weight:500;letter-spacing:0.02em;"
|
|
16
|
+
"z-index:99999;border-radius:3px;"
|
|
17
|
+
'backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px)">'
|
|
18
|
+
'<span style="opacity:0.5">queries</span> {count}{duplicates} <span style="opacity:0.5">·</span> '
|
|
19
|
+
'<span style="opacity:0.5">db</span> {duration:.0f}ms <span style="opacity:0.5">·</span> '
|
|
20
|
+
'<span style="opacity:0.5">total</span> {response_time:.0f}ms</div>'
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
DUPLICATE_MARKER = (
|
|
24
|
+
' <span style="color:#f59e0b" title="Duplicate queries detected">(d)</span>'
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
BODY_CLOSE_RE = re.compile(rb"</body\s*>", re.IGNORECASE)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class DevBarMiddleware:
|
|
31
|
+
def __init__(self, get_response):
|
|
32
|
+
self.get_response = get_response
|
|
33
|
+
|
|
34
|
+
def __call__(self, request):
|
|
35
|
+
request_start = perf_counter()
|
|
36
|
+
tracker.reset()
|
|
37
|
+
|
|
38
|
+
with ExitStack() as stack:
|
|
39
|
+
for alias in connections:
|
|
40
|
+
stack.enter_context(
|
|
41
|
+
connections[alias].execute_wrapper(tracker.tracking_wrapper)
|
|
42
|
+
)
|
|
43
|
+
response = self.get_response(request)
|
|
44
|
+
|
|
45
|
+
response_time = (perf_counter() - request_start) * 1000
|
|
46
|
+
stats = tracker.get_stats()
|
|
47
|
+
|
|
48
|
+
if get_show_headers():
|
|
49
|
+
self._add_headers(response, stats, response_time)
|
|
50
|
+
|
|
51
|
+
if get_show_bar() and self._can_inject(response):
|
|
52
|
+
self._inject_devbar(response, stats, response_time)
|
|
53
|
+
|
|
54
|
+
return response
|
|
55
|
+
|
|
56
|
+
def _add_headers(self, response, stats, response_time):
|
|
57
|
+
response["X-DevBar-Query-Count"] = str(stats["count"])
|
|
58
|
+
response["X-DevBar-Query-Duration"] = f"{stats['duration']:.1f}"
|
|
59
|
+
response["X-DevBar-Response-Time"] = f"{response_time:.1f}"
|
|
60
|
+
if stats["has_duplicates"]:
|
|
61
|
+
response["X-DevBar-Has-Duplicates"] = "1"
|
|
62
|
+
|
|
63
|
+
def _can_inject(self, response):
|
|
64
|
+
if getattr(response, "streaming", False):
|
|
65
|
+
return False
|
|
66
|
+
content_type = response.get("Content-Type", "").lower()
|
|
67
|
+
if "text/html" not in content_type:
|
|
68
|
+
return False
|
|
69
|
+
if response.get("Content-Encoding"):
|
|
70
|
+
return False
|
|
71
|
+
return hasattr(response, "content")
|
|
72
|
+
|
|
73
|
+
def _inject_devbar(self, response, stats, response_time):
|
|
74
|
+
content = response.content
|
|
75
|
+
matches = list(BODY_CLOSE_RE.finditer(content))
|
|
76
|
+
if not matches:
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
devbar_html = DEVBAR_HTML.format(
|
|
80
|
+
position=get_position(),
|
|
81
|
+
count=stats["count"],
|
|
82
|
+
duplicates=DUPLICATE_MARKER if stats["has_duplicates"] else "",
|
|
83
|
+
duration=stats["duration"],
|
|
84
|
+
response_time=response_time,
|
|
85
|
+
).encode(response.charset or "utf-8")
|
|
86
|
+
|
|
87
|
+
idx = matches[-1].start()
|
|
88
|
+
response.content = content[:idx] + devbar_html + content[idx:]
|
|
89
|
+
response["Content-Length"] = str(len(response.content))
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from contextvars import ContextVar
|
|
2
|
+
from time import perf_counter
|
|
3
|
+
|
|
4
|
+
_query_count: ContextVar[int] = ContextVar("query_count", default=0)
|
|
5
|
+
_query_duration: ContextVar[float] = ContextVar("query_duration", default=0.0)
|
|
6
|
+
_seen_queries: ContextVar[dict] = ContextVar(
|
|
7
|
+
"seen_queries"
|
|
8
|
+
) # sql -> set of params hashes
|
|
9
|
+
_has_duplicates: ContextVar[bool] = ContextVar("has_duplicates", default=False)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def reset():
|
|
13
|
+
_query_count.set(0)
|
|
14
|
+
_query_duration.set(0.0)
|
|
15
|
+
_seen_queries.set({})
|
|
16
|
+
_has_duplicates.set(False)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_stats():
|
|
20
|
+
return {
|
|
21
|
+
"count": _query_count.get(),
|
|
22
|
+
"duration": _query_duration.get(),
|
|
23
|
+
"has_duplicates": _has_duplicates.get(),
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _hash_params(params):
|
|
28
|
+
try:
|
|
29
|
+
return hash(tuple(params)) if params else 0
|
|
30
|
+
except TypeError:
|
|
31
|
+
return hash(str(params))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _record(sql, params, duration):
|
|
35
|
+
_query_count.set(_query_count.get() + 1)
|
|
36
|
+
_query_duration.set(_query_duration.get() + duration)
|
|
37
|
+
|
|
38
|
+
seen = _seen_queries.get()
|
|
39
|
+
if sql in seen:
|
|
40
|
+
params_hash = _hash_params(params)
|
|
41
|
+
if params_hash in seen[sql]:
|
|
42
|
+
_has_duplicates.set(True)
|
|
43
|
+
else:
|
|
44
|
+
seen[sql].add(params_hash)
|
|
45
|
+
else:
|
|
46
|
+
seen[sql] = {_hash_params(params)}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def tracking_wrapper(execute, sql, params, many, context):
|
|
50
|
+
start = perf_counter()
|
|
51
|
+
try:
|
|
52
|
+
return execute(sql, params, many, context)
|
|
53
|
+
finally:
|
|
54
|
+
_record(sql, params, (perf_counter() - start) * 1000)
|