django-devbar 0.1.2__tar.gz → 0.2.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.2 → django_devbar-0.2.0}/PKG-INFO +18 -22
- {django_devbar-0.1.2 → django_devbar-0.2.0}/README.md +17 -21
- {django_devbar-0.1.2 → django_devbar-0.2.0}/pyproject.toml +1 -1
- {django_devbar-0.1.2 → django_devbar-0.2.0}/src/django_devbar/__init__.py +0 -1
- django_devbar-0.2.0/src/django_devbar/conf.py +33 -0
- django_devbar-0.2.0/src/django_devbar/middleware.py +109 -0
- django_devbar-0.2.0/src/django_devbar/templates/django_devbar/devbar.html +92 -0
- django_devbar-0.2.0/src/django_devbar/templates/django_devbar/duplicates.html +1 -0
- django_devbar-0.1.2/src/django_devbar/conf.py +0 -39
- django_devbar-0.1.2/src/django_devbar/middleware.py +0 -153
- {django_devbar-0.1.2 → django_devbar-0.2.0}/src/django_devbar/tracker.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-devbar
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Lightweight performance devbar for Django
|
|
5
5
|
Keywords: django,debug,performance,monitoring,development
|
|
6
6
|
Author: amureki
|
|
@@ -33,11 +33,19 @@ Description-Content-Type: text/markdown
|
|
|
33
33
|
|
|
34
34
|
Lightweight performance devbar for Django. Shows DB query count, query duration, application time, and detects duplicate queries with visual severity indicators.
|
|
35
35
|
|
|
36
|
-

|
|
37
|
+
|
|
38
|
+

|
|
39
|
+
|
|
40
|
+
> **Note:** This package is experimental and may introduce breaking changes in minor versions.
|
|
37
41
|
|
|
38
42
|
## Installation
|
|
39
43
|
|
|
40
44
|
```bash
|
|
45
|
+
# Using uv (recommended)
|
|
46
|
+
uv add --dev django-devbar
|
|
47
|
+
|
|
48
|
+
# Or using pip
|
|
41
49
|
pip install django-devbar
|
|
42
50
|
```
|
|
43
51
|
|
|
@@ -53,34 +61,22 @@ MIDDLEWARE = [
|
|
|
53
61
|
|
|
54
62
|
## Configuration
|
|
55
63
|
|
|
56
|
-
|
|
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 DevBar-* response headers (default: False)
|
|
64
|
-
DEVBAR_SHOW_HEADERS = True
|
|
64
|
+
All settings are optional. Configure via a `DEVBAR` dict in your Django settings:
|
|
65
65
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
#
|
|
70
|
-
|
|
71
|
-
"time_warning": 500, # ms
|
|
72
|
-
"time_critical": 1500, # ms
|
|
73
|
-
"count_warning": 20, # queries
|
|
74
|
-
"count_critical": 50, # queries
|
|
66
|
+
```python
|
|
67
|
+
DEVBAR = {
|
|
68
|
+
'POSITION': 'bottom-right', # bottom-right, bottom-left, top-right, top-left
|
|
69
|
+
'SHOW_BAR': None, # None (default) = follows DEBUG, or True/False to override
|
|
70
|
+
'SHOW_HEADERS': False, # Add X-DevBar-* headers to responses
|
|
75
71
|
}
|
|
76
72
|
```
|
|
77
73
|
|
|
78
74
|
## Response Headers
|
|
79
75
|
|
|
80
|
-
When `
|
|
76
|
+
When `DEVBAR = {'SHOW_HEADERS': True}`, performance metrics are added as HTTP response headers. This is useful for:
|
|
81
77
|
|
|
82
78
|
- **API endpoints** where the HTML overlay can't be displayed
|
|
83
|
-
- **Automated testing** to assert performance
|
|
79
|
+
- **Automated testing** to assert performance metrics (e.g., fail CI if query count exceeds a limit)
|
|
84
80
|
- **Monitoring tools** that can capture and aggregate header values
|
|
85
81
|
|
|
86
82
|
Headers included:
|
|
@@ -2,11 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
Lightweight performance devbar for Django. Shows DB query count, query duration, application time, and detects duplicate queries with visual severity indicators.
|
|
4
4
|
|
|
5
|
-

|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
> **Note:** This package is experimental and may introduce breaking changes in minor versions.
|
|
6
10
|
|
|
7
11
|
## Installation
|
|
8
12
|
|
|
9
13
|
```bash
|
|
14
|
+
# Using uv (recommended)
|
|
15
|
+
uv add --dev django-devbar
|
|
16
|
+
|
|
17
|
+
# Or using pip
|
|
10
18
|
pip install django-devbar
|
|
11
19
|
```
|
|
12
20
|
|
|
@@ -22,34 +30,22 @@ MIDDLEWARE = [
|
|
|
22
30
|
|
|
23
31
|
## Configuration
|
|
24
32
|
|
|
25
|
-
|
|
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 DevBar-* response headers (default: False)
|
|
33
|
-
DEVBAR_SHOW_HEADERS = True
|
|
33
|
+
All settings are optional. Configure via a `DEVBAR` dict in your Django settings:
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
#
|
|
39
|
-
|
|
40
|
-
"time_warning": 500, # ms
|
|
41
|
-
"time_critical": 1500, # ms
|
|
42
|
-
"count_warning": 20, # queries
|
|
43
|
-
"count_critical": 50, # queries
|
|
35
|
+
```python
|
|
36
|
+
DEVBAR = {
|
|
37
|
+
'POSITION': 'bottom-right', # bottom-right, bottom-left, top-right, top-left
|
|
38
|
+
'SHOW_BAR': None, # None (default) = follows DEBUG, or True/False to override
|
|
39
|
+
'SHOW_HEADERS': False, # Add X-DevBar-* headers to responses
|
|
44
40
|
}
|
|
45
41
|
```
|
|
46
42
|
|
|
47
43
|
## Response Headers
|
|
48
44
|
|
|
49
|
-
When `
|
|
45
|
+
When `DEVBAR = {'SHOW_HEADERS': True}`, performance metrics are added as HTTP response headers. This is useful for:
|
|
50
46
|
|
|
51
47
|
- **API endpoints** where the HTML overlay can't be displayed
|
|
52
|
-
- **Automated testing** to assert performance
|
|
48
|
+
- **Automated testing** to assert performance metrics (e.g., fail CI if query count exceeds a limit)
|
|
53
49
|
- **Monitoring tools** that can capture and aggregate header values
|
|
54
50
|
|
|
55
51
|
Headers included:
|
|
@@ -0,0 +1,33 @@
|
|
|
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_config():
|
|
12
|
+
return {
|
|
13
|
+
"POSITION": "bottom-right",
|
|
14
|
+
"SHOW_BAR": None, # None = use settings.DEBUG
|
|
15
|
+
"SHOW_HEADERS": False,
|
|
16
|
+
**getattr(settings, "DEVBAR", {}),
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_position():
|
|
21
|
+
config = get_config()
|
|
22
|
+
key = config["POSITION"]
|
|
23
|
+
return POSITIONS.get(key, POSITIONS["bottom-right"])
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_show_bar():
|
|
27
|
+
config = get_config()
|
|
28
|
+
show_bar = config["SHOW_BAR"]
|
|
29
|
+
return settings.DEBUG if show_bar is None else show_bar
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_show_headers():
|
|
33
|
+
return get_config()["SHOW_HEADERS"]
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from contextlib import ExitStack
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from time import perf_counter
|
|
5
|
+
|
|
6
|
+
from django.db import connections
|
|
7
|
+
from django.template import Context, Engine
|
|
8
|
+
|
|
9
|
+
from . import tracker
|
|
10
|
+
from .conf import (
|
|
11
|
+
get_position,
|
|
12
|
+
get_show_bar,
|
|
13
|
+
get_show_headers,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
BODY_CLOSE_RE = re.compile(rb"</body\s*>", re.IGNORECASE)
|
|
17
|
+
|
|
18
|
+
_template_engine = Engine(
|
|
19
|
+
dirs=[Path(__file__).parent / "templates"],
|
|
20
|
+
autoescape=True,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class DevBarMiddleware:
|
|
25
|
+
def __init__(self, get_response):
|
|
26
|
+
self.get_response = get_response
|
|
27
|
+
|
|
28
|
+
def __call__(self, request):
|
|
29
|
+
tracker.reset()
|
|
30
|
+
request_start = perf_counter()
|
|
31
|
+
|
|
32
|
+
with ExitStack() as stack:
|
|
33
|
+
for alias in connections:
|
|
34
|
+
stack.enter_context(
|
|
35
|
+
connections[alias].execute_wrapper(tracker.tracking_wrapper)
|
|
36
|
+
)
|
|
37
|
+
response = self.get_response(request)
|
|
38
|
+
|
|
39
|
+
total_time = (perf_counter() - request_start) * 1000
|
|
40
|
+
stats = tracker.get_stats()
|
|
41
|
+
|
|
42
|
+
db_time = stats["duration"]
|
|
43
|
+
python_time = max(0, total_time - db_time)
|
|
44
|
+
|
|
45
|
+
stats["python_time"] = python_time
|
|
46
|
+
stats["total_time"] = total_time
|
|
47
|
+
|
|
48
|
+
level = "warn" if stats["has_duplicates"] else "ok"
|
|
49
|
+
|
|
50
|
+
if get_show_headers():
|
|
51
|
+
self._add_headers(response, stats)
|
|
52
|
+
|
|
53
|
+
if get_show_bar() and self._can_inject(response):
|
|
54
|
+
self._inject_devbar(response, stats, level)
|
|
55
|
+
|
|
56
|
+
return response
|
|
57
|
+
|
|
58
|
+
def _add_headers(self, response, stats):
|
|
59
|
+
response["DevBar-Query-Count"] = str(stats["count"])
|
|
60
|
+
response["DevBar-DB-Time"] = f"{stats['duration']:.0f}ms"
|
|
61
|
+
response["DevBar-App-Time"] = f"{stats['python_time']:.0f}ms"
|
|
62
|
+
if stats["has_duplicates"]:
|
|
63
|
+
response["DevBar-Duplicates"] = str(len(stats["duplicate_queries"]))
|
|
64
|
+
|
|
65
|
+
def _can_inject(self, response):
|
|
66
|
+
if getattr(response, "streaming", False):
|
|
67
|
+
return False
|
|
68
|
+
content_type = response.get("Content-Type", "").lower()
|
|
69
|
+
if "html" not in content_type:
|
|
70
|
+
return False
|
|
71
|
+
if response.get("Content-Encoding"):
|
|
72
|
+
return False
|
|
73
|
+
return hasattr(response, "content")
|
|
74
|
+
|
|
75
|
+
def _inject_devbar(self, response, stats, level):
|
|
76
|
+
content = response.content
|
|
77
|
+
matches = list(BODY_CLOSE_RE.finditer(content))
|
|
78
|
+
if not matches:
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
duplicates_html = self._build_duplicates_html(
|
|
82
|
+
stats.get("duplicate_queries", [])
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
template = _template_engine.get_template("django_devbar/devbar.html")
|
|
86
|
+
html = template.render(
|
|
87
|
+
Context(
|
|
88
|
+
{
|
|
89
|
+
"position": get_position(),
|
|
90
|
+
"level": level,
|
|
91
|
+
"db_time": stats["duration"],
|
|
92
|
+
"app_time": stats["python_time"],
|
|
93
|
+
"query_count": stats["count"],
|
|
94
|
+
"duplicates_html": duplicates_html,
|
|
95
|
+
}
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
payload = html.encode(response.charset or "utf-8")
|
|
100
|
+
|
|
101
|
+
idx = matches[-1].start()
|
|
102
|
+
response.content = content[:idx] + payload + content[idx:]
|
|
103
|
+
response["Content-Length"] = str(len(response.content))
|
|
104
|
+
|
|
105
|
+
def _build_duplicates_html(self, duplicates):
|
|
106
|
+
if not duplicates:
|
|
107
|
+
return ""
|
|
108
|
+
template = _template_engine.get_template("django_devbar/duplicates.html")
|
|
109
|
+
return template.render(Context({"duplicates": duplicates}))
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
<style>
|
|
2
|
+
#django-devbar {
|
|
3
|
+
position: fixed; {{ position }}; z-index: 999999999;
|
|
4
|
+
display: flex; align-items: center; gap: 5px;
|
|
5
|
+
font-family: -apple-system, system-ui, sans-serif;
|
|
6
|
+
font-size: 11px; font-weight: 500;
|
|
7
|
+
padding: 4px 8px; margin: 8px; border-radius: 4px;
|
|
8
|
+
backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
|
|
9
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.15), 0 1px 2px rgba(0,0,0,0.2);
|
|
10
|
+
transition: all 0.2s ease;
|
|
11
|
+
cursor: default;
|
|
12
|
+
line-height: 1.3;
|
|
13
|
+
background: rgba(20, 20, 20, 0.92);
|
|
14
|
+
color: #f5f5f5;
|
|
15
|
+
}
|
|
16
|
+
#django-devbar.level-warn { border-left: 3px solid #f59e0b; }
|
|
17
|
+
#django-devbar span { opacity: 0.7; }
|
|
18
|
+
#django-devbar strong { opacity: 1; font-weight: 600; }
|
|
19
|
+
#django-devbar details { display: inline; position: relative; }
|
|
20
|
+
#django-devbar summary {
|
|
21
|
+
all: revert;
|
|
22
|
+
color: #f59e0b; font-weight: 600; cursor: pointer; list-style: none;
|
|
23
|
+
}
|
|
24
|
+
#django-devbar summary::-webkit-details-marker { display: none; }
|
|
25
|
+
#django-devbar details[open] > .dup-list {
|
|
26
|
+
position: absolute; bottom: 100%; right: -8px; margin-bottom: 8px;
|
|
27
|
+
background: rgba(20, 20, 20, 0.92); border-radius: 4px; padding: 6px;
|
|
28
|
+
width: min(500px, 90vw); max-height: 200px; overflow-y: auto; overflow-x: hidden;
|
|
29
|
+
box-sizing: border-box;
|
|
30
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.15), 0 1px 2px rgba(0,0,0,0.2);
|
|
31
|
+
}
|
|
32
|
+
#django-devbar .dup-list details {
|
|
33
|
+
display: block;
|
|
34
|
+
margin: 0 0 6px 0;
|
|
35
|
+
font-size: 11px;
|
|
36
|
+
color: #f5f5f5;
|
|
37
|
+
}
|
|
38
|
+
#django-devbar .dup-list details:last-child { margin-bottom: 0; }
|
|
39
|
+
#django-devbar .dup-list summary {
|
|
40
|
+
all: revert;
|
|
41
|
+
cursor: pointer;
|
|
42
|
+
list-style: none;
|
|
43
|
+
padding: 6px 8px;
|
|
44
|
+
background: rgba(251, 191, 36, 0.1);
|
|
45
|
+
border-radius: 3px;
|
|
46
|
+
border-left: 2px solid #fbbf24;
|
|
47
|
+
transition: background 0.15s ease;
|
|
48
|
+
}
|
|
49
|
+
#django-devbar .dup-list summary:hover {
|
|
50
|
+
background: rgba(251, 191, 36, 0.2);
|
|
51
|
+
}
|
|
52
|
+
#django-devbar .dup-list summary code {
|
|
53
|
+
white-space: nowrap;
|
|
54
|
+
overflow: hidden;
|
|
55
|
+
text-overflow: ellipsis;
|
|
56
|
+
display: inline-block;
|
|
57
|
+
max-width: 420px;
|
|
58
|
+
vertical-align: bottom;
|
|
59
|
+
}
|
|
60
|
+
#django-devbar .dup-list summary::before {
|
|
61
|
+
content: '▶ ';
|
|
62
|
+
display: inline-block;
|
|
63
|
+
transition: transform 0.15s ease;
|
|
64
|
+
color: #fbbf24;
|
|
65
|
+
font-size: 8px;
|
|
66
|
+
margin-right: 6px;
|
|
67
|
+
}
|
|
68
|
+
#django-devbar .dup-list details[open] > summary::before {
|
|
69
|
+
transform: rotate(90deg);
|
|
70
|
+
}
|
|
71
|
+
#django-devbar .dup-list code {
|
|
72
|
+
all: revert;
|
|
73
|
+
color: #fbbf24; font-family: 'SF Mono', Consolas, 'Liberation Mono', Menlo, monospace;
|
|
74
|
+
overflow-wrap: break-word;
|
|
75
|
+
}
|
|
76
|
+
#django-devbar .dup-list details[open] > code {
|
|
77
|
+
display: block;
|
|
78
|
+
padding: 8px;
|
|
79
|
+
background: rgba(0, 0, 0, 0.3);
|
|
80
|
+
border-radius: 3px;
|
|
81
|
+
margin-top: 6px;
|
|
82
|
+
white-space: pre-wrap;
|
|
83
|
+
line-height: 1.5;
|
|
84
|
+
font-size: 10px;
|
|
85
|
+
}
|
|
86
|
+
@media (max-width: 640px) { #django-devbar { display: none; } }
|
|
87
|
+
</style>
|
|
88
|
+
<div id="django-devbar" class="level-{{ level }}">
|
|
89
|
+
<span>db</span> <strong>{{ db_time|floatformat:0 }}ms</strong> <span>·</span>
|
|
90
|
+
<span>app</span> <strong>{{ app_time|floatformat:0 }}ms</strong> <span>·</span>
|
|
91
|
+
<span>queries</span> <strong>{{ query_count }}</strong>{{ duplicates_html|safe }}
|
|
92
|
+
</div>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{% if duplicates %}<details><summary>({{ duplicates|length }}d)</summary><div class="dup-list">{% for dup in duplicates %}<details><summary><code>{{ dup.sql }}</code></summary><code>{{ dup.sql }}</code></details>{% endfor %}</div></details>{% endif %}
|
|
@@ -1,39 +0,0 @@
|
|
|
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
|
-
DEFAULT_THRESHOLDS = {
|
|
11
|
-
"time_warning": 500,
|
|
12
|
-
"time_critical": 1500,
|
|
13
|
-
"count_warning": 20,
|
|
14
|
-
"count_critical": 50,
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def get_position():
|
|
19
|
-
key = getattr(settings, "DEVBAR_POSITION", "bottom-right")
|
|
20
|
-
return POSITIONS.get(key, POSITIONS["bottom-right"])
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def get_show_bar():
|
|
24
|
-
return getattr(settings, "DEVBAR_SHOW_BAR", settings.DEBUG)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def get_show_headers():
|
|
28
|
-
return getattr(settings, "DEVBAR_SHOW_HEADERS", False)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def get_enable_console():
|
|
32
|
-
return getattr(settings, "DEVBAR_ENABLE_CONSOLE", True)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def get_thresholds():
|
|
36
|
-
user_thresholds = getattr(settings, "DEVBAR_THRESHOLDS", {})
|
|
37
|
-
final = DEFAULT_THRESHOLDS.copy()
|
|
38
|
-
final.update(user_thresholds)
|
|
39
|
-
return final
|
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
import re
|
|
3
|
-
from contextlib import ExitStack
|
|
4
|
-
from time import perf_counter
|
|
5
|
-
|
|
6
|
-
from django.db import connections
|
|
7
|
-
|
|
8
|
-
from . import tracker
|
|
9
|
-
from .conf import (
|
|
10
|
-
get_position,
|
|
11
|
-
get_show_bar,
|
|
12
|
-
get_show_headers,
|
|
13
|
-
get_thresholds,
|
|
14
|
-
get_enable_console,
|
|
15
|
-
)
|
|
16
|
-
|
|
17
|
-
STYLE_BLOCK = """<style>
|
|
18
|
-
#django-devbar {
|
|
19
|
-
position: fixed; %s; z-index: 99999;
|
|
20
|
-
font-family: -apple-system, system-ui, sans-serif;
|
|
21
|
-
font-size: 11px; font-weight: 500;
|
|
22
|
-
padding: 4px 8px; border-radius: 4px;
|
|
23
|
-
backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px);
|
|
24
|
-
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
|
|
25
|
-
transition: all 0.2s ease;
|
|
26
|
-
cursor: default;
|
|
27
|
-
line-height: 1.3;
|
|
28
|
-
background: rgba(20, 20, 20, 0.85);
|
|
29
|
-
color: #e5e5e5;
|
|
30
|
-
}
|
|
31
|
-
#django-devbar.level-warn { border-left: 3px solid #f59e0b; }
|
|
32
|
-
#django-devbar.level-crit { border-left: 3px solid #dc2626; }
|
|
33
|
-
#django-devbar span { opacity: 0.7; margin-right: 2px; }
|
|
34
|
-
#django-devbar strong { opacity: 1; font-weight: 600; margin-right: 6px; }
|
|
35
|
-
</style>"""
|
|
36
|
-
|
|
37
|
-
BAR_TEMPLATE = """<div id="django-devbar" class="level-%s">
|
|
38
|
-
<span>db</span> <strong>%.0fms</strong>
|
|
39
|
-
<span>py</span> <strong>%.0fms</strong>
|
|
40
|
-
<span>count</span> <strong>%d</strong>%s
|
|
41
|
-
</div>"""
|
|
42
|
-
|
|
43
|
-
SCRIPT_TEMPLATE = """<script>
|
|
44
|
-
(function() {
|
|
45
|
-
const stats = %s;
|
|
46
|
-
if (stats.duplicates && stats.duplicates.length > 0) {
|
|
47
|
-
console.groupCollapsed(`⚠️ Django DevBar: Duplicate Queries Detected (${stats.duplicates.length})`);
|
|
48
|
-
console.table(stats.duplicates.map(d => ({SQL: d.sql, Parameters: d.params, Duration_ms: d.duration})));
|
|
49
|
-
console.groupEnd();
|
|
50
|
-
}
|
|
51
|
-
})();
|
|
52
|
-
</script>"""
|
|
53
|
-
|
|
54
|
-
BODY_CLOSE_RE = re.compile(rb"</body\s*>", re.IGNORECASE)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
class DevBarMiddleware:
|
|
58
|
-
def __init__(self, get_response):
|
|
59
|
-
self.get_response = get_response
|
|
60
|
-
|
|
61
|
-
def __call__(self, request):
|
|
62
|
-
tracker.reset()
|
|
63
|
-
request_start = perf_counter()
|
|
64
|
-
|
|
65
|
-
with ExitStack() as stack:
|
|
66
|
-
for alias in connections:
|
|
67
|
-
stack.enter_context(
|
|
68
|
-
connections[alias].execute_wrapper(tracker.tracking_wrapper)
|
|
69
|
-
)
|
|
70
|
-
response = self.get_response(request)
|
|
71
|
-
|
|
72
|
-
total_time = (perf_counter() - request_start) * 1000
|
|
73
|
-
stats = tracker.get_stats()
|
|
74
|
-
|
|
75
|
-
db_time = stats["duration"]
|
|
76
|
-
python_time = max(0, total_time - db_time)
|
|
77
|
-
|
|
78
|
-
stats["python_time"] = python_time
|
|
79
|
-
stats["total_time"] = total_time
|
|
80
|
-
|
|
81
|
-
thresholds = get_thresholds()
|
|
82
|
-
level = self._determine_level(stats, total_time, thresholds)
|
|
83
|
-
|
|
84
|
-
if get_show_headers():
|
|
85
|
-
self._add_headers(response, stats)
|
|
86
|
-
|
|
87
|
-
if get_show_bar() and self._can_inject(response):
|
|
88
|
-
self._inject_devbar(response, stats, level)
|
|
89
|
-
|
|
90
|
-
return response
|
|
91
|
-
|
|
92
|
-
def _determine_level(self, stats, total_time, thresholds):
|
|
93
|
-
if (
|
|
94
|
-
total_time > thresholds["time_critical"]
|
|
95
|
-
or stats["count"] > thresholds["count_critical"]
|
|
96
|
-
):
|
|
97
|
-
return "crit"
|
|
98
|
-
if (
|
|
99
|
-
stats["has_duplicates"]
|
|
100
|
-
or total_time > thresholds["time_warning"]
|
|
101
|
-
or stats["count"] > thresholds["count_warning"]
|
|
102
|
-
):
|
|
103
|
-
return "warn"
|
|
104
|
-
return "ok"
|
|
105
|
-
|
|
106
|
-
def _add_headers(self, response, stats):
|
|
107
|
-
response["DevBar-Query-Count"] = str(stats["count"])
|
|
108
|
-
response["DevBar-DB-Time"] = f"{stats['duration']:.0f}ms"
|
|
109
|
-
response["DevBar-App-Time"] = f"{stats['python_time']:.0f}ms"
|
|
110
|
-
if stats["has_duplicates"]:
|
|
111
|
-
response["DevBar-Duplicates"] = str(len(stats["duplicate_queries"]))
|
|
112
|
-
|
|
113
|
-
def _can_inject(self, response):
|
|
114
|
-
if getattr(response, "streaming", False):
|
|
115
|
-
return False
|
|
116
|
-
content_type = response.get("Content-Type", "").lower()
|
|
117
|
-
if "html" not in content_type:
|
|
118
|
-
return False
|
|
119
|
-
if response.get("Content-Encoding"):
|
|
120
|
-
return False
|
|
121
|
-
return hasattr(response, "content")
|
|
122
|
-
|
|
123
|
-
def _inject_devbar(self, response, stats, level):
|
|
124
|
-
content = response.content
|
|
125
|
-
matches = list(BODY_CLOSE_RE.finditer(content))
|
|
126
|
-
if not matches:
|
|
127
|
-
return
|
|
128
|
-
|
|
129
|
-
dup_marker = (
|
|
130
|
-
' <strong style="color:#f59e0b">(D)</strong>'
|
|
131
|
-
if stats["has_duplicates"]
|
|
132
|
-
else ""
|
|
133
|
-
)
|
|
134
|
-
|
|
135
|
-
css = STYLE_BLOCK % get_position()
|
|
136
|
-
html = BAR_TEMPLATE % (
|
|
137
|
-
level,
|
|
138
|
-
stats["duration"],
|
|
139
|
-
stats["python_time"],
|
|
140
|
-
stats["count"],
|
|
141
|
-
dup_marker,
|
|
142
|
-
)
|
|
143
|
-
|
|
144
|
-
script = ""
|
|
145
|
-
if get_enable_console() and stats.get("duplicate_queries"):
|
|
146
|
-
console_data = {"duplicates": stats["duplicate_queries"]}
|
|
147
|
-
script = SCRIPT_TEMPLATE % json.dumps(console_data)
|
|
148
|
-
|
|
149
|
-
payload = (css + html + script).encode(response.charset or "utf-8")
|
|
150
|
-
|
|
151
|
-
idx = matches[-1].start()
|
|
152
|
-
response.content = content[:idx] + payload + content[idx:]
|
|
153
|
-
response["Content-Length"] = str(len(response.content))
|
|
File without changes
|