django-api-profiler 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_api_profiler-0.1.0/.env +6 -0
- django_api_profiler-0.1.0/.gitignore +10 -0
- django_api_profiler-0.1.0/.python-version +1 -0
- django_api_profiler-0.1.0/Dockerfile +21 -0
- django_api_profiler-0.1.0/LICENSE +21 -0
- django_api_profiler-0.1.0/PKG-INFO +118 -0
- django_api_profiler-0.1.0/README.md +96 -0
- django_api_profiler-0.1.0/config/__init__.py +3 -0
- django_api_profiler-0.1.0/config/asgi.py +7 -0
- django_api_profiler-0.1.0/config/celery.py +10 -0
- django_api_profiler-0.1.0/config/settings.py +114 -0
- django_api_profiler-0.1.0/config/urls.py +6 -0
- django_api_profiler-0.1.0/config/wsgi.py +7 -0
- django_api_profiler-0.1.0/docker-compose.yml +71 -0
- django_api_profiler-0.1.0/manage.py +22 -0
- django_api_profiler-0.1.0/profiler/__init__.py +0 -0
- django_api_profiler-0.1.0/profiler/admin.py +80 -0
- django_api_profiler-0.1.0/profiler/apps.py +5 -0
- django_api_profiler-0.1.0/profiler/conf.py +31 -0
- django_api_profiler-0.1.0/profiler/db_wrapper.py +33 -0
- django_api_profiler-0.1.0/profiler/middleware.py +58 -0
- django_api_profiler-0.1.0/profiler/migrations/0001_initial.py +25 -0
- django_api_profiler-0.1.0/profiler/migrations/0002_requestmetric_query_count_and_more.py +23 -0
- django_api_profiler-0.1.0/profiler/migrations/0003_requestmetric_exception_message_and_more.py +28 -0
- django_api_profiler-0.1.0/profiler/migrations/0004_requestmetric_is_slow.py +19 -0
- django_api_profiler-0.1.0/profiler/migrations/0005_requestmetric_ip_address_requestmetric_request_id_and_more.py +58 -0
- django_api_profiler-0.1.0/profiler/migrations/0006_requestmetric_has_n_plus_one_and_more.py +23 -0
- django_api_profiler-0.1.0/profiler/migrations/0007_endpointsummary.py +35 -0
- django_api_profiler-0.1.0/profiler/migrations/__init__.py +0 -0
- django_api_profiler-0.1.0/profiler/models/__init__.py +2 -0
- django_api_profiler-0.1.0/profiler/models/endpoint_summary.py +28 -0
- django_api_profiler-0.1.0/profiler/models/request_metric.py +39 -0
- django_api_profiler-0.1.0/profiler/services/__init__.py +10 -0
- django_api_profiler-0.1.0/profiler/services/analytics.py +88 -0
- django_api_profiler-0.1.0/profiler/services/n_plus_one.py +31 -0
- django_api_profiler-0.1.0/profiler/services/regression.py +61 -0
- django_api_profiler-0.1.0/profiler/services/request_metric.py +50 -0
- django_api_profiler-0.1.0/profiler/tasks.py +16 -0
- django_api_profiler-0.1.0/profiler/tests/__init__.py +5 -0
- django_api_profiler-0.1.0/profiler/tests/build_metric_payload.py +14 -0
- django_api_profiler-0.1.0/profiler/tests/n_plus_one.py +51 -0
- django_api_profiler-0.1.0/profiler/utils/__init__.py +1 -0
- django_api_profiler-0.1.0/profiler/utils/ingestion.py +33 -0
- django_api_profiler-0.1.0/pyproject.toml +52 -0
- django_api_profiler-0.1.0/uv.lock +359 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.12
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
FROM python:3.12-slim
|
|
2
|
+
|
|
3
|
+
ENV PYTHONDONTWRITEBYTECODE=1
|
|
4
|
+
ENV PYTHONUNBUFFERED=1
|
|
5
|
+
|
|
6
|
+
WORKDIR /src
|
|
7
|
+
|
|
8
|
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
9
|
+
libpq-dev \
|
|
10
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
11
|
+
|
|
12
|
+
RUN pip install uv
|
|
13
|
+
|
|
14
|
+
COPY pyproject.toml uv.lock .python-version ./
|
|
15
|
+
RUN uv sync --frozen
|
|
16
|
+
|
|
17
|
+
COPY . .
|
|
18
|
+
|
|
19
|
+
EXPOSE 8000
|
|
20
|
+
|
|
21
|
+
CMD ["uv", "run", "python", "manage.py", "runserver", "0.0.0.0:8000"]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ahmed
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: django-api-profiler
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Lightweight API profiling and endpoint analytics for Django
|
|
5
|
+
Author: Ahmed
|
|
6
|
+
License: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: analytics,django,monitoring,performance,profiling
|
|
9
|
+
Classifier: Framework :: Django
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Requires-Python: >=3.10
|
|
13
|
+
Requires-Dist: django>=4.2
|
|
14
|
+
Provides-Extra: async
|
|
15
|
+
Requires-Dist: celery>=5.6.3; extra == 'async'
|
|
16
|
+
Requires-Dist: redis>=7.4.0; extra == 'async'
|
|
17
|
+
Provides-Extra: beat
|
|
18
|
+
Requires-Dist: django-celery-beat>=2.9.0; extra == 'beat'
|
|
19
|
+
Provides-Extra: postgres
|
|
20
|
+
Requires-Dist: psycopg2-binary>=2.9.12; extra == 'postgres'
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# Django API Profiler
|
|
24
|
+
|
|
25
|
+
Lightweight API profiling and endpoint analytics for Django.
|
|
26
|
+
|
|
27
|
+
## Features
|
|
28
|
+
|
|
29
|
+
- Request latency tracking
|
|
30
|
+
- Endpoint summaries
|
|
31
|
+
- p95 response metrics
|
|
32
|
+
- Error rate monitoring
|
|
33
|
+
- Slow request detection
|
|
34
|
+
- N+1 query detection
|
|
35
|
+
- Django admin integration
|
|
36
|
+
- Optional async ingestion with Celery
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install django-api-profiler
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Quickstart
|
|
49
|
+
|
|
50
|
+
Add the app:
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
INSTALLED_APPS = [
|
|
54
|
+
...
|
|
55
|
+
"django_api_profiler",
|
|
56
|
+
]
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Add the middleware:
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
MIDDLEWARE = [
|
|
63
|
+
...
|
|
64
|
+
"django_api_profiler.middleware.ApiProfilerMiddleware",
|
|
65
|
+
]
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Run migrations:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
python manage.py migrate
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Configuration
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
PROFILER = {
|
|
80
|
+
"ASYNC": False,
|
|
81
|
+
"SLOW_REQUEST_THRESHOLD_MS": 1000,
|
|
82
|
+
"AGGREGATION_WINDOW_MINUTES": 60,
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Admin Dashboard
|
|
89
|
+
|
|
90
|
+
The package provides Django admin integration for:
|
|
91
|
+
- endpoint summaries
|
|
92
|
+
- latency metrics
|
|
93
|
+
- error tracking
|
|
94
|
+
- regression detection
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Optional Async Support
|
|
99
|
+
|
|
100
|
+
Install async dependencies:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
pip install django-api-profiler[async]
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Then enable:
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
PROFILER = {
|
|
110
|
+
"ASYNC": True,
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## License
|
|
117
|
+
|
|
118
|
+
MIT
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# Django API Profiler
|
|
2
|
+
|
|
3
|
+
Lightweight API profiling and endpoint analytics for Django.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Request latency tracking
|
|
8
|
+
- Endpoint summaries
|
|
9
|
+
- p95 response metrics
|
|
10
|
+
- Error rate monitoring
|
|
11
|
+
- Slow request detection
|
|
12
|
+
- N+1 query detection
|
|
13
|
+
- Django admin integration
|
|
14
|
+
- Optional async ingestion with Celery
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install django-api-profiler
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Quickstart
|
|
27
|
+
|
|
28
|
+
Add the app:
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
INSTALLED_APPS = [
|
|
32
|
+
...
|
|
33
|
+
"django_api_profiler",
|
|
34
|
+
]
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Add the middleware:
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
MIDDLEWARE = [
|
|
41
|
+
...
|
|
42
|
+
"django_api_profiler.middleware.ApiProfilerMiddleware",
|
|
43
|
+
]
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Run migrations:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
python manage.py migrate
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Configuration
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
PROFILER = {
|
|
58
|
+
"ASYNC": False,
|
|
59
|
+
"SLOW_REQUEST_THRESHOLD_MS": 1000,
|
|
60
|
+
"AGGREGATION_WINDOW_MINUTES": 60,
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Admin Dashboard
|
|
67
|
+
|
|
68
|
+
The package provides Django admin integration for:
|
|
69
|
+
- endpoint summaries
|
|
70
|
+
- latency metrics
|
|
71
|
+
- error tracking
|
|
72
|
+
- regression detection
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Optional Async Support
|
|
77
|
+
|
|
78
|
+
Install async dependencies:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
pip install django-api-profiler[async]
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Then enable:
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
PROFILER = {
|
|
88
|
+
"ASYNC": True,
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## License
|
|
95
|
+
|
|
96
|
+
MIT
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from dotenv import load_dotenv
|
|
3
|
+
from celery.schedules import crontab
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
load_dotenv()
|
|
7
|
+
|
|
8
|
+
BASE_DIR = Path(__file__).resolve().parent.parent
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
SECRET_KEY = os.getenv("SECRET_KEY")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
DEBUG = True
|
|
15
|
+
|
|
16
|
+
ALLOWED_HOSTS = ['*']
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
INSTALLED_APPS = [
|
|
21
|
+
'django.contrib.admin',
|
|
22
|
+
'django.contrib.auth',
|
|
23
|
+
'django.contrib.contenttypes',
|
|
24
|
+
'django.contrib.sessions',
|
|
25
|
+
'django.contrib.messages',
|
|
26
|
+
'django.contrib.staticfiles',
|
|
27
|
+
'django_celery_beat',
|
|
28
|
+
'profiler',
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
MIDDLEWARE = [
|
|
32
|
+
'django.middleware.security.SecurityMiddleware',
|
|
33
|
+
'django.contrib.sessions.middleware.SessionMiddleware',
|
|
34
|
+
'django.middleware.common.CommonMiddleware',
|
|
35
|
+
'django.middleware.csrf.CsrfViewMiddleware',
|
|
36
|
+
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
|
37
|
+
'django.contrib.messages.middleware.MessageMiddleware',
|
|
38
|
+
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
|
39
|
+
'profiler.middleware.ApiProfilerMiddleware',
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
ROOT_URLCONF = 'config.urls'
|
|
43
|
+
|
|
44
|
+
TEMPLATES = [
|
|
45
|
+
{
|
|
46
|
+
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
|
47
|
+
'DIRS': [],
|
|
48
|
+
'APP_DIRS': True,
|
|
49
|
+
'OPTIONS': {
|
|
50
|
+
'context_processors': [
|
|
51
|
+
'django.template.context_processors.request',
|
|
52
|
+
'django.contrib.auth.context_processors.auth',
|
|
53
|
+
'django.contrib.messages.context_processors.messages',
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
WSGI_APPLICATION = 'config.wsgi.application'
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
DATABASES = {
|
|
63
|
+
'default': {
|
|
64
|
+
'ENGINE': 'django.db.backends.postgresql',
|
|
65
|
+
'NAME': os.environ.get('DB_NAME'),
|
|
66
|
+
'USER': os.environ.get('DB_USER'),
|
|
67
|
+
'PASSWORD': os.environ.get('DB_PASSWORD'),
|
|
68
|
+
'HOST': os.environ.get('DB_HOST', 'localhost'),
|
|
69
|
+
'PORT': os.environ.get('DB_PORT', '5432'),
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
AUTH_PASSWORD_VALIDATORS = [
|
|
75
|
+
{
|
|
76
|
+
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
|
86
|
+
},
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
LANGUAGE_CODE = 'en-us'
|
|
91
|
+
|
|
92
|
+
TIME_ZONE = 'UTC'
|
|
93
|
+
|
|
94
|
+
USE_I18N = True
|
|
95
|
+
|
|
96
|
+
USE_TZ = True
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
STATIC_URL = 'static/'
|
|
100
|
+
|
|
101
|
+
CELERY_BROKER_URL = os.environ.get('CELERY_BROKER_URL', 'redis://localhost:6379/0')
|
|
102
|
+
CELERY_RESULT_BACKEND = os.environ.get('CELERY_RESULT_BACKEND', 'redis://localhost:6379/0')
|
|
103
|
+
CELERY_TIMEZONE = 'UTC'
|
|
104
|
+
|
|
105
|
+
CELERY_BEAT_SCHEDULER = 'django_celery_beat.schedulers:DatabaseScheduler'
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
CELERY_BEAT_SCHEDULE = {
|
|
110
|
+
'compute-endpoint-summaries': {
|
|
111
|
+
'task': 'profiler.tasks.run_aggregation',
|
|
112
|
+
'schedule': crontab(minute=0),
|
|
113
|
+
},
|
|
114
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
services:
|
|
2
|
+
|
|
3
|
+
db:
|
|
4
|
+
image: postgres:16-alpine
|
|
5
|
+
environment:
|
|
6
|
+
POSTGRES_DB: ${DB_NAME}
|
|
7
|
+
POSTGRES_USER: ${DB_USER}
|
|
8
|
+
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
|
9
|
+
volumes:
|
|
10
|
+
- postgres_data:/var/lib/postgresql/data
|
|
11
|
+
healthcheck:
|
|
12
|
+
test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"]
|
|
13
|
+
interval: 10s
|
|
14
|
+
timeout: 5s
|
|
15
|
+
retries: 5
|
|
16
|
+
|
|
17
|
+
redis:
|
|
18
|
+
image: redis:7-alpine
|
|
19
|
+
healthcheck:
|
|
20
|
+
test: ["CMD", "redis-cli", "ping"]
|
|
21
|
+
interval: 10s
|
|
22
|
+
timeout: 5s
|
|
23
|
+
retries: 5
|
|
24
|
+
|
|
25
|
+
web:
|
|
26
|
+
build: .
|
|
27
|
+
image: django-api-profiler:latest
|
|
28
|
+
restart: unless-stopped
|
|
29
|
+
command: >
|
|
30
|
+
sh -c "/src/.venv/bin/python manage.py migrate &&
|
|
31
|
+
/src/.venv/bin/gunicorn config.wsgi:application --bind 0.0.0.0:8000 --workers 3"
|
|
32
|
+
volumes:
|
|
33
|
+
- .:/src
|
|
34
|
+
ports:
|
|
35
|
+
- "8000:8000"
|
|
36
|
+
env_file:
|
|
37
|
+
- .env
|
|
38
|
+
depends_on:
|
|
39
|
+
db:
|
|
40
|
+
condition: service_healthy
|
|
41
|
+
redis:
|
|
42
|
+
condition: service_healthy
|
|
43
|
+
|
|
44
|
+
celery:
|
|
45
|
+
image: django-api-profiler:latest
|
|
46
|
+
command: uv run celery -A config worker --loglevel=info
|
|
47
|
+
volumes:
|
|
48
|
+
- .:/src
|
|
49
|
+
env_file:
|
|
50
|
+
- .env
|
|
51
|
+
depends_on:
|
|
52
|
+
db:
|
|
53
|
+
condition: service_healthy
|
|
54
|
+
redis:
|
|
55
|
+
condition: service_healthy
|
|
56
|
+
|
|
57
|
+
celery_beat:
|
|
58
|
+
image: django-api-profiler:latest
|
|
59
|
+
command: uv run celery -A config beat --loglevel=info --scheduler django_celery_beat.schedulers:DatabaseScheduler
|
|
60
|
+
volumes:
|
|
61
|
+
- .:/src
|
|
62
|
+
env_file:
|
|
63
|
+
- .env
|
|
64
|
+
depends_on:
|
|
65
|
+
db:
|
|
66
|
+
condition: service_healthy
|
|
67
|
+
redis:
|
|
68
|
+
condition: service_healthy
|
|
69
|
+
|
|
70
|
+
volumes:
|
|
71
|
+
postgres_data:
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""Django's command-line utility for administrative tasks."""
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def main():
|
|
8
|
+
"""Run administrative tasks."""
|
|
9
|
+
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
|
10
|
+
try:
|
|
11
|
+
from django.core.management import execute_from_command_line
|
|
12
|
+
except ImportError as exc:
|
|
13
|
+
raise ImportError(
|
|
14
|
+
"Couldn't import Django. Are you sure it's installed and "
|
|
15
|
+
"available on your PYTHONPATH environment variable? Did you "
|
|
16
|
+
"forget to activate a virtual environment?"
|
|
17
|
+
) from exc
|
|
18
|
+
execute_from_command_line(sys.argv)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
if __name__ == '__main__':
|
|
22
|
+
main()
|
|
File without changes
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from django.contrib import admin
|
|
2
|
+
from django.utils.html import format_html
|
|
3
|
+
from .models import RequestMetric , EndpointSummary
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@admin.register(RequestMetric)
|
|
7
|
+
class RequestMetricAdmin(admin.ModelAdmin):
|
|
8
|
+
list_display = (
|
|
9
|
+
"path",
|
|
10
|
+
"method",
|
|
11
|
+
"status_code",
|
|
12
|
+
"colored_response_time",
|
|
13
|
+
"query_count",
|
|
14
|
+
"total_query_time_ms",
|
|
15
|
+
"exception_type",
|
|
16
|
+
"exception_message",
|
|
17
|
+
"created_at",
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
list_filter = (
|
|
21
|
+
"method",
|
|
22
|
+
"status_code",
|
|
23
|
+
"created_at"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
readonly_fields = list_display
|
|
27
|
+
|
|
28
|
+
ordering = ("-created_at",)
|
|
29
|
+
|
|
30
|
+
def colored_response_time(self, obj):
|
|
31
|
+
|
|
32
|
+
if obj.response_time_ms > 1000:
|
|
33
|
+
color = "red"
|
|
34
|
+
|
|
35
|
+
elif obj.response_time_ms > 500:
|
|
36
|
+
color = "orange"
|
|
37
|
+
|
|
38
|
+
else:
|
|
39
|
+
color = "lightgreen"
|
|
40
|
+
|
|
41
|
+
formatted_time = f"{obj.response_time_ms:.2f} ms"
|
|
42
|
+
|
|
43
|
+
return format_html(
|
|
44
|
+
'<span style="color: {}; font-weight: bold;">{}</span>',
|
|
45
|
+
color,
|
|
46
|
+
formatted_time
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@admin.register(EndpointSummary)
|
|
51
|
+
class EndpointSummaryAdmin(admin.ModelAdmin):
|
|
52
|
+
|
|
53
|
+
list_display = (
|
|
54
|
+
"route",
|
|
55
|
+
"window_start",
|
|
56
|
+
"total_requests",
|
|
57
|
+
"avg_response_ms",
|
|
58
|
+
"p95_response_ms",
|
|
59
|
+
"error_count",
|
|
60
|
+
"slow_count",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
list_filter = (
|
|
64
|
+
"window_start",
|
|
65
|
+
"computed_at",
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
search_fields = ("route",)
|
|
69
|
+
|
|
70
|
+
ordering = ("-window_start",)
|
|
71
|
+
|
|
72
|
+
readonly_fields = [field.name for field in EndpointSummary._meta.fields]
|
|
73
|
+
|
|
74
|
+
date_hierarchy = "window_start"
|
|
75
|
+
|
|
76
|
+
def has_add_permission(self, request):
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
def has_delete_permission(self, request, obj=None):
|
|
80
|
+
return False
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from django.conf import settings
|
|
2
|
+
|
|
3
|
+
_DEFAULTS = {
|
|
4
|
+
"ASYNC": False,
|
|
5
|
+
"SLOW_REQUEST_THRESHOLD_MS": 1000,
|
|
6
|
+
"AGGREGATION_WINDOW_MINUTES": 60,
|
|
7
|
+
"IGNORED_PATHS": ["/admin", "/static", "/favicon.ico"],
|
|
8
|
+
"N_PLUS_ONE_THRESHOLD": 3,
|
|
9
|
+
"SLOW_QUERY_THRESHOLD_MS": 100,
|
|
10
|
+
"REGRESSION_RESPONSE_TIME_FACTOR": 2.0,
|
|
11
|
+
"REGRESSION_ERROR_RATE_DELTA": 0.1,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ProfilerSettings:
|
|
16
|
+
|
|
17
|
+
def __init__(self):
|
|
18
|
+
self._user_settings = getattr(settings, "PROFILER", {})
|
|
19
|
+
|
|
20
|
+
def __getattr__(self, name):
|
|
21
|
+
if name not in _DEFAULTS:
|
|
22
|
+
raise AttributeError(f"Invalid profiler setting: '{name}'")
|
|
23
|
+
|
|
24
|
+
default = _DEFAULTS[name]
|
|
25
|
+
user_value = self._user_settings.get(name, default)
|
|
26
|
+
|
|
27
|
+
return user_value
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
profiler_settings = ProfilerSettings()
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from django.db.backends.utils import CursorWrapper
|
|
2
|
+
import threading
|
|
3
|
+
|
|
4
|
+
_local = threading.local()
|
|
5
|
+
|
|
6
|
+
def reset_query_log():
|
|
7
|
+
_local.queries = []
|
|
8
|
+
|
|
9
|
+
def get_query_log():
|
|
10
|
+
return list(getattr(_local, "queries", []))
|
|
11
|
+
|
|
12
|
+
class ProfilingCursorWrapper(CursorWrapper):
|
|
13
|
+
def execute(self, sql, params=None):
|
|
14
|
+
import time
|
|
15
|
+
start = time.perf_counter()
|
|
16
|
+
try:
|
|
17
|
+
return super().execute(sql, params)
|
|
18
|
+
finally:
|
|
19
|
+
duration = (time.perf_counter() - start) * 1000
|
|
20
|
+
if not hasattr(_local, "queries"):
|
|
21
|
+
_local.queries = []
|
|
22
|
+
_local.queries.append({"sql": sql, "time_ms": duration})
|
|
23
|
+
|
|
24
|
+
def executemany(self, sql, param_list):
|
|
25
|
+
import time
|
|
26
|
+
start = time.perf_counter()
|
|
27
|
+
try:
|
|
28
|
+
return super().executemany(sql, param_list)
|
|
29
|
+
finally:
|
|
30
|
+
duration = (time.perf_counter() - start) * 1000
|
|
31
|
+
if not hasattr(_local, "queries"):
|
|
32
|
+
_local.queries = []
|
|
33
|
+
_local.queries.append({"sql": sql, "time_ms": duration})
|