django-scope 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_scope-0.1.0/.gitignore +26 -0
- django_scope-0.1.0/LICENSE +21 -0
- django_scope-0.1.0/PKG-INFO +87 -0
- django_scope-0.1.0/README.md +50 -0
- django_scope-0.1.0/pyproject.toml +67 -0
- django_scope-0.1.0/src/telescope/__init__.py +35 -0
- django_scope-0.1.0/src/telescope/apps.py +15 -0
- django_scope-0.1.0/src/telescope/backends/__init__.py +0 -0
- django_scope-0.1.0/src/telescope/backends/cache_backend.py +97 -0
- django_scope-0.1.0/src/telescope/backends/mail_backend.py +13 -0
- django_scope-0.1.0/src/telescope/consumers/__init__.py +0 -0
- django_scope-0.1.0/src/telescope/consumers/telescope_consumer.py +39 -0
- django_scope-0.1.0/src/telescope/context.py +37 -0
- django_scope-0.1.0/src/telescope/db_router.py +31 -0
- django_scope-0.1.0/src/telescope/entry_type.py +34 -0
- django_scope-0.1.0/src/telescope/filtering.py +56 -0
- django_scope-0.1.0/src/telescope/management/__init__.py +0 -0
- django_scope-0.1.0/src/telescope/management/commands/__init__.py +0 -0
- django_scope-0.1.0/src/telescope/management/commands/telescope_clear.py +29 -0
- django_scope-0.1.0/src/telescope/management/commands/telescope_prune.py +21 -0
- django_scope-0.1.0/src/telescope/middleware/__init__.py +3 -0
- django_scope-0.1.0/src/telescope/middleware/telescope_middleware.py +64 -0
- django_scope-0.1.0/src/telescope/migrations/0001_initial.py +54 -0
- django_scope-0.1.0/src/telescope/migrations/__init__.py +0 -0
- django_scope-0.1.0/src/telescope/models.py +49 -0
- django_scope-0.1.0/src/telescope/patches/__init__.py +0 -0
- django_scope-0.1.0/src/telescope/patches/command_patch.py +2 -0
- django_scope-0.1.0/src/telescope/patches/http_client_patch.py +82 -0
- django_scope-0.1.0/src/telescope/patches/redis_patch.py +44 -0
- django_scope-0.1.0/src/telescope/pruning.py +25 -0
- django_scope-0.1.0/src/telescope/recorder.py +135 -0
- django_scope-0.1.0/src/telescope/routing.py +7 -0
- django_scope-0.1.0/src/telescope/serializers.py +128 -0
- django_scope-0.1.0/src/telescope/settings.py +54 -0
- django_scope-0.1.0/src/telescope/static/telescope/assets/CodeBlock.vue_vue_type_script_setup_true_lang-CWnwRjkl.js +1 -0
- django_scope-0.1.0/src/telescope/static/telescope/assets/DashboardPage-CDM_ThxH.js +1 -0
- django_scope-0.1.0/src/telescope/static/telescope/assets/DurationBadge.vue_vue_type_script_setup_true_lang-VTWOmC8g.js +1 -0
- django_scope-0.1.0/src/telescope/static/telescope/assets/EntryDetailPage-BBz7GhBO.js +1 -0
- django_scope-0.1.0/src/telescope/static/telescope/assets/EntryTable.vue_vue_type_script_setup_true_lang-BXU10mml.js +1 -0
- django_scope-0.1.0/src/telescope/static/telescope/assets/ExceptionDetailPage-BkhuNBvQ.js +1 -0
- django_scope-0.1.0/src/telescope/static/telescope/assets/ExceptionsPage-bnRg7Glp.js +1 -0
- django_scope-0.1.0/src/telescope/static/telescope/assets/KeyValueTable.vue_vue_type_script_setup_true_lang-DrdZmCRu.js +1 -0
- django_scope-0.1.0/src/telescope/static/telescope/assets/MailDetailPage-cGPREsmc.js +1 -0
- django_scope-0.1.0/src/telescope/static/telescope/assets/QueriesPage-BUwgnN6Y.js +1 -0
- django_scope-0.1.0/src/telescope/static/telescope/assets/RequestDetailPage-CNYh-4Uo.js +1 -0
- django_scope-0.1.0/src/telescope/static/telescope/assets/RequestsPage-DZ6R_AFj.js +1 -0
- django_scope-0.1.0/src/telescope/static/telescope/assets/SearchBar.vue_vue_type_script_setup_true_lang-BGTl9mYY.js +1 -0
- django_scope-0.1.0/src/telescope/static/telescope/assets/StatusBadge.vue_vue_type_script_setup_true_lang-DpEbUkiq.js +1 -0
- django_scope-0.1.0/src/telescope/static/telescope/assets/TypedListPage-B8mjAFfL.js +1 -0
- django_scope-0.1.0/src/telescope/static/telescope/assets/api-DCUZqYAI.js +1 -0
- django_scope-0.1.0/src/telescope/static/telescope/assets/index.css +1 -0
- django_scope-0.1.0/src/telescope/static/telescope/assets/main.js +30 -0
- django_scope-0.1.0/src/telescope/static/telescope/assets/useTimeAgo-BLzdtsQE.js +1 -0
- django_scope-0.1.0/src/telescope/templates/telescope/index.html +20 -0
- django_scope-0.1.0/src/telescope/truncation.py +38 -0
- django_scope-0.1.0/src/telescope/urls.py +32 -0
- django_scope-0.1.0/src/telescope/views/__init__.py +0 -0
- django_scope-0.1.0/src/telescope/views/api.py +189 -0
- django_scope-0.1.0/src/telescope/views/spa.py +27 -0
- django_scope-0.1.0/src/telescope/watchers/__init__.py +72 -0
- django_scope-0.1.0/src/telescope/watchers/base.py +19 -0
- django_scope-0.1.0/src/telescope/watchers/batch_watcher.py +51 -0
- django_scope-0.1.0/src/telescope/watchers/cache_watcher.py +29 -0
- django_scope-0.1.0/src/telescope/watchers/client_request_watcher.py +10 -0
- django_scope-0.1.0/src/telescope/watchers/command_watcher.py +54 -0
- django_scope-0.1.0/src/telescope/watchers/dump_watcher.py +9 -0
- django_scope-0.1.0/src/telescope/watchers/event_watcher.py +63 -0
- django_scope-0.1.0/src/telescope/watchers/exception_watcher.py +86 -0
- django_scope-0.1.0/src/telescope/watchers/gate_watcher.py +37 -0
- django_scope-0.1.0/src/telescope/watchers/log_watcher.py +55 -0
- django_scope-0.1.0/src/telescope/watchers/mail_watcher.py +36 -0
- django_scope-0.1.0/src/telescope/watchers/model_watcher.py +84 -0
- django_scope-0.1.0/src/telescope/watchers/notification_watcher.py +9 -0
- django_scope-0.1.0/src/telescope/watchers/query_watcher.py +102 -0
- django_scope-0.1.0/src/telescope/watchers/redis_watcher.py +10 -0
- django_scope-0.1.0/src/telescope/watchers/request_watcher.py +126 -0
- django_scope-0.1.0/src/telescope/watchers/schedule_watcher.py +44 -0
- django_scope-0.1.0/src/telescope/watchers/view_watcher.py +37 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.py[cod]
|
|
3
|
+
*$py.class
|
|
4
|
+
*.egg-info/
|
|
5
|
+
dist/
|
|
6
|
+
build/
|
|
7
|
+
.eggs/
|
|
8
|
+
*.egg
|
|
9
|
+
.venv/
|
|
10
|
+
venv/
|
|
11
|
+
.env
|
|
12
|
+
*.sqlite3
|
|
13
|
+
db.sqlite3
|
|
14
|
+
node_modules/
|
|
15
|
+
frontend/dist/
|
|
16
|
+
.DS_Store
|
|
17
|
+
*.swp
|
|
18
|
+
*.swo
|
|
19
|
+
.idea/
|
|
20
|
+
.vscode/
|
|
21
|
+
*.log
|
|
22
|
+
.ruff_cache/
|
|
23
|
+
.pytest_cache/
|
|
24
|
+
.mypy_cache/
|
|
25
|
+
htmlcov/
|
|
26
|
+
.coverage
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 MythicalCosmic
|
|
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,87 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: django-scope
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: An elegant debug assistant for Django, inspired by Laravel Telescope
|
|
5
|
+
Author: cosmic
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: debug,django,monitoring,profiling,telescope
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Framework :: Django
|
|
11
|
+
Classifier: Framework :: Django :: 4.2
|
|
12
|
+
Classifier: Framework :: Django :: 5.0
|
|
13
|
+
Classifier: Framework :: Django :: 5.1
|
|
14
|
+
Classifier: Framework :: Django :: 6
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Software Development :: Debuggers
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Requires-Dist: build>=1.4.2
|
|
25
|
+
Requires-Dist: channels>=4.0
|
|
26
|
+
Requires-Dist: daphne>=4.2.1
|
|
27
|
+
Requires-Dist: django>=4.2
|
|
28
|
+
Requires-Dist: setuptools>=82.0.1
|
|
29
|
+
Requires-Dist: twine>=6.2.0
|
|
30
|
+
Requires-Dist: wheel>=0.46.3
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
33
|
+
Requires-Dist: pytest-asyncio; extra == 'dev'
|
|
34
|
+
Requires-Dist: pytest-django; extra == 'dev'
|
|
35
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
36
|
+
Description-Content-Type: text/markdown
|
|
37
|
+
|
|
38
|
+
# django-telescope
|
|
39
|
+
|
|
40
|
+
An elegant debug assistant for Django, inspired by Laravel Telescope.
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install django-telescope
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Add to your Django settings:
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
INSTALLED_APPS = [
|
|
52
|
+
"daphne",
|
|
53
|
+
"telescope",
|
|
54
|
+
# ... your apps
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
TELESCOPE = {
|
|
58
|
+
"ENABLED": True,
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Add the middleware:
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
MIDDLEWARE = [
|
|
66
|
+
"telescope.middleware.TelescopeMiddleware",
|
|
67
|
+
# ... other middleware
|
|
68
|
+
]
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Add URL routing:
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from django.urls import include, path
|
|
75
|
+
|
|
76
|
+
urlpatterns = [
|
|
77
|
+
path("telescope/", include("telescope.urls")),
|
|
78
|
+
]
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Features
|
|
82
|
+
|
|
83
|
+
- Real-time monitoring via WebSockets
|
|
84
|
+
- 17 watchers: requests, queries, exceptions, models, logs, cache, mail, Redis, views, events, commands, dumps, HTTP client, gates, notifications, schedules, batches
|
|
85
|
+
- N+1 query detection
|
|
86
|
+
- Beautiful Vue.js dashboard with dark/light themes
|
|
87
|
+
- Request-scoped buffering with bulk writes
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# django-telescope
|
|
2
|
+
|
|
3
|
+
An elegant debug assistant for Django, inspired by Laravel Telescope.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install django-telescope
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Add to your Django settings:
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
INSTALLED_APPS = [
|
|
15
|
+
"daphne",
|
|
16
|
+
"telescope",
|
|
17
|
+
# ... your apps
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
TELESCOPE = {
|
|
21
|
+
"ENABLED": True,
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Add the middleware:
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
MIDDLEWARE = [
|
|
29
|
+
"telescope.middleware.TelescopeMiddleware",
|
|
30
|
+
# ... other middleware
|
|
31
|
+
]
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Add URL routing:
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from django.urls import include, path
|
|
38
|
+
|
|
39
|
+
urlpatterns = [
|
|
40
|
+
path("telescope/", include("telescope.urls")),
|
|
41
|
+
]
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Features
|
|
45
|
+
|
|
46
|
+
- Real-time monitoring via WebSockets
|
|
47
|
+
- 17 watchers: requests, queries, exceptions, models, logs, cache, mail, Redis, views, events, commands, dumps, HTTP client, gates, notifications, schedules, batches
|
|
48
|
+
- N+1 query detection
|
|
49
|
+
- Beautiful Vue.js dashboard with dark/light themes
|
|
50
|
+
- Request-scoped buffering with bulk writes
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "django-scope"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "An elegant debug assistant for Django, inspired by Laravel Telescope"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [{ name = "cosmic" }]
|
|
13
|
+
keywords = ["django", "debug", "telescope", "monitoring", "profiling"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Framework :: Django",
|
|
17
|
+
"Framework :: Django :: 4.2",
|
|
18
|
+
"Framework :: Django :: 5.0",
|
|
19
|
+
"Framework :: Django :: 5.1",
|
|
20
|
+
"Framework :: Django :: 6",
|
|
21
|
+
"Intended Audience :: Developers",
|
|
22
|
+
"License :: OSI Approved :: MIT License",
|
|
23
|
+
"Programming Language :: Python :: 3",
|
|
24
|
+
"Programming Language :: Python :: 3.10",
|
|
25
|
+
"Programming Language :: Python :: 3.11",
|
|
26
|
+
"Programming Language :: Python :: 3.12",
|
|
27
|
+
"Programming Language :: Python :: 3.13",
|
|
28
|
+
"Topic :: Software Development :: Debuggers",
|
|
29
|
+
]
|
|
30
|
+
dependencies = [
|
|
31
|
+
"django>=4.2",
|
|
32
|
+
"channels>=4.0",
|
|
33
|
+
"daphne>=4.2.1",
|
|
34
|
+
"setuptools>=82.0.1",
|
|
35
|
+
"wheel>=0.46.3",
|
|
36
|
+
"twine>=6.2.0",
|
|
37
|
+
"build>=1.4.2",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
[project.optional-dependencies]
|
|
41
|
+
dev = [
|
|
42
|
+
"pytest",
|
|
43
|
+
"pytest-django",
|
|
44
|
+
"pytest-asyncio",
|
|
45
|
+
"ruff",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
[tool.hatch.build.targets.wheel]
|
|
49
|
+
packages = ["src/telescope"]
|
|
50
|
+
|
|
51
|
+
[tool.hatch.build.targets.sdist]
|
|
52
|
+
include = ["src/telescope/**", "LICENSE", "README.md"]
|
|
53
|
+
|
|
54
|
+
[tool.pytest.ini_options]
|
|
55
|
+
DJANGO_SETTINGS_MODULE = "tests.settings"
|
|
56
|
+
pythonpath = ["src", "."]
|
|
57
|
+
|
|
58
|
+
[tool.ruff]
|
|
59
|
+
src = ["src"]
|
|
60
|
+
line-length = 120
|
|
61
|
+
|
|
62
|
+
[dependency-groups]
|
|
63
|
+
dev = [
|
|
64
|
+
"pytest>=9.0.2",
|
|
65
|
+
"pytest-asyncio>=1.3.0",
|
|
66
|
+
"pytest-django>=4.12.0",
|
|
67
|
+
]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
default_app_config = "telescope.apps.TelescopeConfig"
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def dump(value, *, label=None):
|
|
5
|
+
"""Public API: record a dump entry from user code."""
|
|
6
|
+
import inspect
|
|
7
|
+
from .entry_type import EntryType
|
|
8
|
+
from .recorder import Recorder
|
|
9
|
+
|
|
10
|
+
frame = inspect.currentframe().f_back
|
|
11
|
+
caller = f"{frame.f_code.co_filename}:{frame.f_lineno}"
|
|
12
|
+
|
|
13
|
+
Recorder.record(
|
|
14
|
+
entry_type=EntryType.DUMP,
|
|
15
|
+
content={
|
|
16
|
+
"dump": repr(value),
|
|
17
|
+
"label": label,
|
|
18
|
+
"caller": caller,
|
|
19
|
+
},
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def notify(notification_class, recipient, channels=None):
|
|
24
|
+
"""Public API: record a notification entry."""
|
|
25
|
+
from .entry_type import EntryType
|
|
26
|
+
from .recorder import Recorder
|
|
27
|
+
|
|
28
|
+
Recorder.record(
|
|
29
|
+
entry_type=EntryType.NOTIFICATION,
|
|
30
|
+
content={
|
|
31
|
+
"notification": notification_class.__name__ if isinstance(notification_class, type) else str(notification_class),
|
|
32
|
+
"recipient": str(recipient),
|
|
33
|
+
"channels": channels or [],
|
|
34
|
+
},
|
|
35
|
+
)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from django.apps import AppConfig
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class TelescopeConfig(AppConfig):
|
|
5
|
+
name = "telescope"
|
|
6
|
+
default_auto_field = "django.db.models.BigAutoField"
|
|
7
|
+
verbose_name = "Django Telescope"
|
|
8
|
+
|
|
9
|
+
def ready(self):
|
|
10
|
+
from .settings import get_config
|
|
11
|
+
|
|
12
|
+
if get_config("ENABLED"):
|
|
13
|
+
from .watchers import WatcherRegistry
|
|
14
|
+
|
|
15
|
+
WatcherRegistry.register_all()
|
|
File without changes
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import time
|
|
2
|
+
|
|
3
|
+
from django.core.cache.backends.locmem import LocMemCache
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TelescopeCacheBackend(LocMemCache):
|
|
7
|
+
"""Wrapping cache backend that records all cache operations."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, server, params):
|
|
10
|
+
# The actual backend class to wrap can be specified in OPTIONS
|
|
11
|
+
self._telescope_alias = params.get("telescope_alias", "default")
|
|
12
|
+
actual_backend = params.pop("TELESCOPE_BACKEND", None)
|
|
13
|
+
|
|
14
|
+
if actual_backend:
|
|
15
|
+
# Dynamically instantiate the real backend
|
|
16
|
+
from django.utils.module_loading import import_string
|
|
17
|
+
|
|
18
|
+
backend_cls = import_string(actual_backend)
|
|
19
|
+
self._real_cache = backend_cls(server, params)
|
|
20
|
+
else:
|
|
21
|
+
super().__init__(server, params)
|
|
22
|
+
self._real_cache = None
|
|
23
|
+
|
|
24
|
+
def _get_backend(self):
|
|
25
|
+
return self._real_cache if self._real_cache else self
|
|
26
|
+
|
|
27
|
+
def get(self, key, default=None, version=None):
|
|
28
|
+
start = time.perf_counter()
|
|
29
|
+
backend = self._get_backend()
|
|
30
|
+
if backend is self:
|
|
31
|
+
value = super().get(key, default, version)
|
|
32
|
+
else:
|
|
33
|
+
value = backend.get(key, default, version)
|
|
34
|
+
duration_ms = (time.perf_counter() - start) * 1000
|
|
35
|
+
|
|
36
|
+
from ..watchers.cache_watcher import CacheWatcher
|
|
37
|
+
|
|
38
|
+
CacheWatcher.record_operation(
|
|
39
|
+
operation="get",
|
|
40
|
+
key=key,
|
|
41
|
+
value=value,
|
|
42
|
+
hit=value is not default,
|
|
43
|
+
duration_ms=duration_ms,
|
|
44
|
+
backend_alias=self._telescope_alias,
|
|
45
|
+
)
|
|
46
|
+
return value
|
|
47
|
+
|
|
48
|
+
def set(self, key, value, timeout=None, version=None):
|
|
49
|
+
start = time.perf_counter()
|
|
50
|
+
backend = self._get_backend()
|
|
51
|
+
if backend is self:
|
|
52
|
+
result = super().set(key, value, timeout, version)
|
|
53
|
+
else:
|
|
54
|
+
result = backend.set(key, value, timeout, version)
|
|
55
|
+
duration_ms = (time.perf_counter() - start) * 1000
|
|
56
|
+
|
|
57
|
+
from ..watchers.cache_watcher import CacheWatcher
|
|
58
|
+
|
|
59
|
+
CacheWatcher.record_operation(
|
|
60
|
+
operation="set",
|
|
61
|
+
key=key,
|
|
62
|
+
value=value,
|
|
63
|
+
duration_ms=duration_ms,
|
|
64
|
+
backend_alias=self._telescope_alias,
|
|
65
|
+
)
|
|
66
|
+
return result
|
|
67
|
+
|
|
68
|
+
def delete(self, key, version=None):
|
|
69
|
+
start = time.perf_counter()
|
|
70
|
+
backend = self._get_backend()
|
|
71
|
+
if backend is self:
|
|
72
|
+
result = super().delete(key, version)
|
|
73
|
+
else:
|
|
74
|
+
result = backend.delete(key, version)
|
|
75
|
+
duration_ms = (time.perf_counter() - start) * 1000
|
|
76
|
+
|
|
77
|
+
from ..watchers.cache_watcher import CacheWatcher
|
|
78
|
+
|
|
79
|
+
CacheWatcher.record_operation(
|
|
80
|
+
operation="delete",
|
|
81
|
+
key=key,
|
|
82
|
+
duration_ms=duration_ms,
|
|
83
|
+
backend_alias=self._telescope_alias,
|
|
84
|
+
)
|
|
85
|
+
return result
|
|
86
|
+
|
|
87
|
+
def clear(self):
|
|
88
|
+
backend = self._get_backend()
|
|
89
|
+
if backend is self:
|
|
90
|
+
result = super().clear()
|
|
91
|
+
else:
|
|
92
|
+
result = backend.clear()
|
|
93
|
+
|
|
94
|
+
from ..watchers.cache_watcher import CacheWatcher
|
|
95
|
+
|
|
96
|
+
CacheWatcher.record_operation(operation="clear", key="*")
|
|
97
|
+
return result
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from django.core.mail.backends.smtp import EmailBackend
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class TelescopeMailBackend(EmailBackend):
|
|
5
|
+
"""Wrapping mail backend that records all sent emails."""
|
|
6
|
+
|
|
7
|
+
def send_messages(self, email_messages):
|
|
8
|
+
from ..watchers.mail_watcher import MailWatcher
|
|
9
|
+
|
|
10
|
+
for message in email_messages:
|
|
11
|
+
MailWatcher.record_mail(message)
|
|
12
|
+
|
|
13
|
+
return super().send_messages(email_messages)
|
|
File without changes
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
from channels.generic.websocket import AsyncJsonWebsocketConsumer
|
|
5
|
+
|
|
6
|
+
logger = logging.getLogger("telescope.consumers")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TelescopeConsumer(AsyncJsonWebsocketConsumer):
|
|
10
|
+
"""WebSocket consumer for real-time telescope entry updates."""
|
|
11
|
+
|
|
12
|
+
async def connect(self):
|
|
13
|
+
await self.channel_layer.group_add("telescope", self.channel_name)
|
|
14
|
+
await self.accept()
|
|
15
|
+
logger.debug("Telescope WebSocket connected: %s", self.channel_name)
|
|
16
|
+
|
|
17
|
+
async def disconnect(self, close_code):
|
|
18
|
+
await self.channel_layer.group_discard("telescope", self.channel_name)
|
|
19
|
+
logger.debug("Telescope WebSocket disconnected: %s", self.channel_name)
|
|
20
|
+
|
|
21
|
+
async def receive_json(self, content, **kwargs):
|
|
22
|
+
# Client can send filter preferences
|
|
23
|
+
msg_type = content.get("type")
|
|
24
|
+
if msg_type == "filter":
|
|
25
|
+
# Store filter preferences on the consumer instance
|
|
26
|
+
self._filters = content.get("filters", {})
|
|
27
|
+
|
|
28
|
+
async def telescope_entry(self, event):
|
|
29
|
+
"""Handle new telescope entry broadcast."""
|
|
30
|
+
entry = event.get("entry", {})
|
|
31
|
+
|
|
32
|
+
# Apply client-side filters if set
|
|
33
|
+
if hasattr(self, "_filters") and self._filters:
|
|
34
|
+
entry_type = entry.get("type_slug")
|
|
35
|
+
allowed_types = self._filters.get("types")
|
|
36
|
+
if allowed_types and entry_type not in allowed_types:
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
await self.send_json({"type": "entry", "data": entry})
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
from contextvars import ContextVar
|
|
3
|
+
|
|
4
|
+
# Unique ID grouping all entries from the same request/command
|
|
5
|
+
batch_id_var: ContextVar[str | None] = ContextVar("telescope_batch_id", default=None)
|
|
6
|
+
|
|
7
|
+
# Buffer of entries to be flushed at end of request
|
|
8
|
+
buffer_var: ContextVar[list | None] = ContextVar("telescope_buffer", default=None)
|
|
9
|
+
|
|
10
|
+
# Whether recording is active for this scope
|
|
11
|
+
recording_var: ContextVar[bool] = ContextVar("telescope_recording", default=True)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def start_scope():
|
|
15
|
+
"""Begin a new telescope scope (called at request start)."""
|
|
16
|
+
batch_id_var.set(str(uuid.uuid4()))
|
|
17
|
+
buffer_var.set([])
|
|
18
|
+
recording_var.set(True)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def end_scope():
|
|
22
|
+
"""End the current telescope scope."""
|
|
23
|
+
batch_id_var.set(None)
|
|
24
|
+
buffer_var.set(None)
|
|
25
|
+
recording_var.set(True)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_batch_id():
|
|
29
|
+
return batch_id_var.get()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_buffer():
|
|
33
|
+
return buffer_var.get()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def is_recording():
|
|
37
|
+
return recording_var.get()
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from .settings import get_config
|
|
2
|
+
|
|
3
|
+
APP_LABEL = "telescope"
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TelescopeRouter:
|
|
7
|
+
"""Routes telescope models to a separate database if configured."""
|
|
8
|
+
|
|
9
|
+
def _db(self):
|
|
10
|
+
return get_config("DB_CONNECTION")
|
|
11
|
+
|
|
12
|
+
def db_for_read(self, model, **hints):
|
|
13
|
+
if model._meta.app_label == APP_LABEL:
|
|
14
|
+
return self._db()
|
|
15
|
+
return None
|
|
16
|
+
|
|
17
|
+
def db_for_write(self, model, **hints):
|
|
18
|
+
if model._meta.app_label == APP_LABEL:
|
|
19
|
+
return self._db()
|
|
20
|
+
return None
|
|
21
|
+
|
|
22
|
+
def allow_relation(self, obj1, obj2, **hints):
|
|
23
|
+
if obj1._meta.app_label == APP_LABEL or obj2._meta.app_label == APP_LABEL:
|
|
24
|
+
return True
|
|
25
|
+
return None
|
|
26
|
+
|
|
27
|
+
def allow_migrate(self, db, app_label, model_name=None, **hints):
|
|
28
|
+
target = self._db() or "default"
|
|
29
|
+
if app_label == APP_LABEL:
|
|
30
|
+
return db == target
|
|
31
|
+
return None
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from enum import IntEnum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class EntryType(IntEnum):
|
|
5
|
+
REQUEST = 1
|
|
6
|
+
QUERY = 2
|
|
7
|
+
EXCEPTION = 3
|
|
8
|
+
MODEL = 4
|
|
9
|
+
LOG = 5
|
|
10
|
+
CACHE = 6
|
|
11
|
+
REDIS = 7
|
|
12
|
+
MAIL = 8
|
|
13
|
+
VIEW = 9
|
|
14
|
+
EVENT = 10
|
|
15
|
+
COMMAND = 11
|
|
16
|
+
DUMP = 12
|
|
17
|
+
CLIENT_REQUEST = 13
|
|
18
|
+
GATE = 14
|
|
19
|
+
NOTIFICATION = 15
|
|
20
|
+
SCHEDULE = 16
|
|
21
|
+
BATCH = 17
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def label(self):
|
|
25
|
+
return self.name.replace("_", " ").title()
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def slug(self):
|
|
29
|
+
return self.name.lower().replace("_", "-")
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def from_slug(cls, slug):
|
|
33
|
+
name = slug.upper().replace("-", "_")
|
|
34
|
+
return cls[name]
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from django.db.models import Q
|
|
2
|
+
|
|
3
|
+
from .entry_type import EntryType
|
|
4
|
+
from .models import TelescopeEntry
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def apply_filters(queryset, params):
|
|
8
|
+
"""Apply query filters from request parameters."""
|
|
9
|
+
|
|
10
|
+
# Filter by entry type
|
|
11
|
+
entry_type = params.get("type")
|
|
12
|
+
if entry_type:
|
|
13
|
+
try:
|
|
14
|
+
et = EntryType.from_slug(entry_type)
|
|
15
|
+
queryset = queryset.filter(type=et.value)
|
|
16
|
+
except (KeyError, ValueError):
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
# Filter by tag
|
|
20
|
+
tag = params.get("tag")
|
|
21
|
+
if tag:
|
|
22
|
+
queryset = queryset.filter(tags__tag=tag)
|
|
23
|
+
|
|
24
|
+
# Filter by batch_id
|
|
25
|
+
batch_id = params.get("batch_id")
|
|
26
|
+
if batch_id:
|
|
27
|
+
queryset = queryset.filter(batch_id=batch_id)
|
|
28
|
+
|
|
29
|
+
# Filter by family_hash
|
|
30
|
+
family_hash = params.get("family_hash")
|
|
31
|
+
if family_hash:
|
|
32
|
+
queryset = queryset.filter(family_hash=family_hash)
|
|
33
|
+
|
|
34
|
+
# Search in content (JSON contains)
|
|
35
|
+
search = params.get("search")
|
|
36
|
+
if search:
|
|
37
|
+
queryset = queryset.filter(
|
|
38
|
+
Q(content__icontains=search) | Q(tags__tag__icontains=search)
|
|
39
|
+
).distinct()
|
|
40
|
+
|
|
41
|
+
# Cursor-based pagination (before=<id>)
|
|
42
|
+
before = params.get("before")
|
|
43
|
+
if before:
|
|
44
|
+
try:
|
|
45
|
+
queryset = queryset.filter(id__lt=int(before))
|
|
46
|
+
except (ValueError, TypeError):
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
# Limit
|
|
50
|
+
limit = params.get("limit", 50)
|
|
51
|
+
try:
|
|
52
|
+
limit = min(int(limit), 100)
|
|
53
|
+
except (ValueError, TypeError):
|
|
54
|
+
limit = 50
|
|
55
|
+
|
|
56
|
+
return queryset[:limit]
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from django.core.management.base import BaseCommand
|
|
2
|
+
|
|
3
|
+
from telescope.entry_type import EntryType
|
|
4
|
+
from telescope.pruning import clear_entries
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Command(BaseCommand):
|
|
8
|
+
help = "Clear all telescope entries"
|
|
9
|
+
|
|
10
|
+
def add_arguments(self, parser):
|
|
11
|
+
parser.add_argument(
|
|
12
|
+
"--type",
|
|
13
|
+
type=str,
|
|
14
|
+
default=None,
|
|
15
|
+
help="Only clear entries of this type (e.g. 'request', 'query')",
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
def handle(self, *args, **options):
|
|
19
|
+
entry_type = None
|
|
20
|
+
type_slug = options["type"]
|
|
21
|
+
if type_slug:
|
|
22
|
+
try:
|
|
23
|
+
entry_type = EntryType.from_slug(type_slug).value
|
|
24
|
+
except (KeyError, ValueError):
|
|
25
|
+
self.stderr.write(self.style.ERROR(f"Invalid type: {type_slug}"))
|
|
26
|
+
return
|
|
27
|
+
|
|
28
|
+
count = clear_entries(entry_type=entry_type)
|
|
29
|
+
self.stdout.write(self.style.SUCCESS(f"Cleared {count} telescope entries"))
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from django.core.management.base import BaseCommand
|
|
2
|
+
|
|
3
|
+
from telescope.pruning import prune_entries
|
|
4
|
+
from telescope.settings import get_config
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Command(BaseCommand):
|
|
8
|
+
help = "Prune old telescope entries"
|
|
9
|
+
|
|
10
|
+
def add_arguments(self, parser):
|
|
11
|
+
parser.add_argument(
|
|
12
|
+
"--hours",
|
|
13
|
+
type=int,
|
|
14
|
+
default=None,
|
|
15
|
+
help=f"Delete entries older than N hours (default: {get_config('ENTRY_LIFETIME_HOURS')})",
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
def handle(self, *args, **options):
|
|
19
|
+
hours = options["hours"]
|
|
20
|
+
count = prune_entries(hours=hours)
|
|
21
|
+
self.stdout.write(self.style.SUCCESS(f"Pruned {count} telescope entries"))
|