varanus 0.1.0.dev7__tar.gz → 0.1.1__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.
- varanus-0.1.1/PKG-INFO +46 -0
- varanus-0.1.1/README.md +25 -0
- {varanus-0.1.0.dev7 → varanus-0.1.1}/pyproject.toml +11 -10
- varanus-0.1.1/src/varanus/.DS_Store +0 -0
- varanus-0.1.1/src/varanus/__init__.py +6 -0
- varanus-0.1.1/src/varanus/client/.DS_Store +0 -0
- {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/client/client.py +11 -1
- {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/client/context.py +11 -0
- varanus-0.1.1/src/varanus/client/transport/.DS_Store +0 -0
- {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/client/transport/database.py +2 -0
- {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/events.py +3 -2
- varanus-0.1.1/src/varanus/search/.DS_Store +0 -0
- {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/search/fields.py +3 -2
- varanus-0.1.1/src/varanus/search/templates/search/.DS_Store +0 -0
- varanus-0.1.1/src/varanus/server/.DS_Store +0 -0
- {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/admin.py +34 -4
- varanus-0.1.1/src/varanus/server/histogram.py +149 -0
- varanus-0.1.1/src/varanus/server/management/commands/.DS_Store +0 -0
- varanus-0.1.1/src/varanus/server/management/commands/maintenance.py +21 -0
- varanus-0.1.1/src/varanus/server/migrations/0002_drop_scheduledtask.py +13 -0
- varanus-0.1.1/src/varanus/server/migrations/0003_site_retention.py +21 -0
- varanus-0.1.1/src/varanus/server/migrations/0004_node_version.py +17 -0
- {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/models.py +17 -1
- {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/settings.py +26 -7
- varanus-0.1.1/src/varanus/server/static/.DS_Store +0 -0
- varanus-0.1.1/src/varanus/server/static/css/.DS_Store +0 -0
- varanus-0.1.1/src/varanus/server/static/css/varanus.css +30 -0
- varanus-0.1.1/src/varanus/server/static/js/.DS_Store +0 -0
- varanus-0.1.1/src/varanus/server/static/js/varanus.js +85 -0
- {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/tasks.py +9 -0
- varanus-0.1.1/src/varanus/server/templates/.DS_Store +0 -0
- {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/templates/dashboard.html +2 -0
- varanus-0.1.1/src/varanus/server/templates/site/.DS_Store +0 -0
- {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/templates/site/base.html +12 -6
- {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/templates/site/details/metric.html +3 -3
- {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/templates/site/errors.html +1 -0
- varanus-0.1.1/src/varanus/server/templates/site/histogram.html +23 -0
- {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/templates/site/logs.html +1 -0
- {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/templates/site/metrics.html +4 -3
- {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/templates/site/overview.html +13 -3
- {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/templates/site/queries.html +1 -0
- {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/templates/site/requests.html +1 -0
- varanus-0.1.1/src/varanus/server/templatetags/varanus.py +50 -0
- varanus-0.1.1/src/varanus/server/views/.DS_Store +0 -0
- {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/views/site.py +8 -0
- varanus-0.1.0.dev7/PKG-INFO +0 -19
- varanus-0.1.0.dev7/README.md +0 -3
- varanus-0.1.0.dev7/src/varanus/server/management/commands/serve.py +0 -64
- varanus-0.1.0.dev7/src/varanus/server/static/css/varanus.css +0 -4
- varanus-0.1.0.dev7/src/varanus/server/static/js/varanus.js +0 -0
- varanus-0.1.0.dev7/src/varanus/server/templatetags/varanus.py +0 -20
- varanus-0.1.0.dev7/src/varanus/tasks/__init__.py +0 -0
- varanus-0.1.0.dev7/src/varanus/tasks/admin.py +0 -15
- varanus-0.1.0.dev7/src/varanus/tasks/apps.py +0 -0
- varanus-0.1.0.dev7/src/varanus/tasks/backend.py +0 -62
- varanus-0.1.0.dev7/src/varanus/tasks/management/__init__.py +0 -0
- varanus-0.1.0.dev7/src/varanus/tasks/management/commands/__init__.py +0 -0
- varanus-0.1.0.dev7/src/varanus/tasks/management/commands/tasker.py +0 -20
- varanus-0.1.0.dev7/src/varanus/tasks/migrations/0001_initial.py +0 -66
- varanus-0.1.0.dev7/src/varanus/tasks/migrations/__init__.py +0 -0
- varanus-0.1.0.dev7/src/varanus/tasks/models.py +0 -131
- varanus-0.1.0.dev7/src/varanus/tasks/runner.py +0 -107
- {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/client/__init__.py +0 -0
- {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/client/apps.py +0 -0
- {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/client/loggers.py +0 -0
- {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/client/middleware.py +0 -0
- {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/client/transport/__init__.py +0 -0
- {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/client/transport/base.py +0 -0
- {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/client/transport/http.py +0 -0
- {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/search/__init__.py +0 -0
- {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/search/base.py +0 -0
- {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/search/templates/search/daterange.html +0 -0
- {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/search/templates/search/filter.html +0 -0
- {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/search/templates/search/hidden.html +0 -0
- {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/search/templates/search/multifacet.html +0 -0
- {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/search/templates/search/search.html +0 -0
- {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/search/utils.py +0 -0
- {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/__init__.py +0 -0
- {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/__main__.py +0 -0
- {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/apps.py +0 -0
- {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/asgi.py +0 -0
- {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/context_processors.py +0 -0
- {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/integrations/__init__.py +0 -0
- {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/integrations/base.py +0 -0
- {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/integrations/squish.py +0 -0
- {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/management/__init__.py +0 -0
- {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/management/commands/__init__.py +0 -0
- {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/management/commands/migrateall.py +0 -0
- {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/middleware.py +0 -0
- {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/migrations/0001_initial.py +0 -0
- {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/migrations/__init__.py +0 -0
- {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/router.py +0 -0
- {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/templates/base.html +0 -0
- {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/templates/registration/login.html +0 -0
- {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/templates/site/details/environment_nodes.html +0 -0
- {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/templates/site/details/error.html +0 -0
- {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/templates/site/details/log.html +0 -0
- {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/templates/site/details/node_env.html +0 -0
- {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/templates/site/details/node_environments.html +0 -0
- {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/templates/site/details/node_packages.html +0 -0
- {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/templates/site/details/node_settings.html +0 -0
- {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/templates/site/details/query.html +0 -0
- {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/templates/site/details/request.html +0 -0
- {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/templatetags/__init__.py +0 -0
- {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/urls.py +0 -0
- {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/utils.py +0 -0
- {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/views/__init__.py +0 -0
- {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/views/api.py +0 -0
- {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/views/base.py +0 -0
- {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/views/dashboard.py +0 -0
- {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/wsgi.py +0 -0
- {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/utils.py +0 -0
- /varanus-0.1.0.dev7/src/varanus/__init__.py → /varanus-0.1.1/src/varanus/version.py +0 -0
varanus-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: varanus
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Django application monitoring.
|
|
5
|
+
Author: Dan Watson
|
|
6
|
+
Author-email: Dan Watson <watsond@imsweb.com>
|
|
7
|
+
Classifier: Intended Audience :: Developers
|
|
8
|
+
Classifier: Programming Language :: Python
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Requires-Dist: httpx>=0.27.0
|
|
11
|
+
Requires-Dist: msgspec>=0.19.0
|
|
12
|
+
Requires-Dist: cconf>=1.0.0 ; extra == 'server'
|
|
13
|
+
Requires-Dist: django>=6.0 ; python_full_version >= '3.12' and extra == 'server'
|
|
14
|
+
Requires-Dist: django-dbtasks[serve]>=0.4.0 ; python_full_version >= '3.12' and extra == 'server'
|
|
15
|
+
Requires-Dist: django-passkey-auth>=0.2.0 ; extra == 'server'
|
|
16
|
+
Requires-Dist: psycopg[binary]>=3.2.1 ; extra == 'server'
|
|
17
|
+
Requires-Dist: whitenoise>=6.7.0 ; extra == 'server'
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Provides-Extra: server
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# Varanus
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
pip install varanus
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Quickstart for Django
|
|
31
|
+
|
|
32
|
+
In your `settings.py`:
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
try:
|
|
36
|
+
import varanus.client
|
|
37
|
+
varanus.client.setup(
|
|
38
|
+
"https://APIKEY@varanus.example.com",
|
|
39
|
+
environment=os.getenv("VARANUS_ENV", "local"),
|
|
40
|
+
install=MIDDLEWARE,
|
|
41
|
+
)
|
|
42
|
+
except ImportError:
|
|
43
|
+
pass
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
For more information about how to configure the Varanus client, see [the configuration docs](docs/configuration.md). If nothing else, note that when using uWSGI, you'll need to enable threads with the [enable-threads](https://uwsgi-docs.readthedocs.io/en/latest/Options.html#enable-threads) option.
|
varanus-0.1.1/README.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Varanus
|
|
2
|
+
|
|
3
|
+
## Installation
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
pip install varanus
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Quickstart for Django
|
|
10
|
+
|
|
11
|
+
In your `settings.py`:
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
try:
|
|
15
|
+
import varanus.client
|
|
16
|
+
varanus.client.setup(
|
|
17
|
+
"https://APIKEY@varanus.example.com",
|
|
18
|
+
environment=os.getenv("VARANUS_ENV", "local"),
|
|
19
|
+
install=MIDDLEWARE,
|
|
20
|
+
)
|
|
21
|
+
except ImportError:
|
|
22
|
+
pass
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
For more information about how to configure the Varanus client, see [the configuration docs](docs/configuration.md). If nothing else, note that when using uWSGI, you'll need to enable threads with the [enable-threads](https://uwsgi-docs.readthedocs.io/en/latest/Options.html#enable-threads) option.
|
|
@@ -1,20 +1,28 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "varanus"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.1"
|
|
4
4
|
description = "Django application monitoring."
|
|
5
|
+
authors = [
|
|
6
|
+
{ name = "Dan Watson", email = "watsond@imsweb.com" }
|
|
7
|
+
]
|
|
5
8
|
readme = "README.md"
|
|
6
9
|
requires-python = ">=3.11"
|
|
7
10
|
dependencies = [
|
|
8
11
|
"httpx>=0.27.0",
|
|
9
12
|
"msgspec>=0.19.0",
|
|
10
13
|
]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"Programming Language :: Python",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
]
|
|
11
19
|
|
|
12
20
|
[project.optional-dependencies]
|
|
13
21
|
server = [
|
|
14
22
|
"cconf>=1.0.0",
|
|
15
23
|
"django>=6.0; python_version>='3.12'",
|
|
24
|
+
"django-dbtasks[serve]>=0.4.0; python_version>='3.12'",
|
|
16
25
|
"django-passkey-auth>=0.2.0",
|
|
17
|
-
"granian[reload]>=2.4.2",
|
|
18
26
|
"psycopg[binary]>=3.2.1",
|
|
19
27
|
"whitenoise>=6.7.0",
|
|
20
28
|
]
|
|
@@ -22,17 +30,10 @@ server = [
|
|
|
22
30
|
[dependency-groups]
|
|
23
31
|
dev = []
|
|
24
32
|
|
|
25
|
-
[project.scripts]
|
|
26
|
-
manage = "varanus.server.__main__:manage"
|
|
27
|
-
|
|
28
33
|
[build-system]
|
|
29
|
-
requires = ["uv_build>=0.
|
|
34
|
+
requires = ["uv_build>=0.11.0,<0.12.0"]
|
|
30
35
|
build-backend = "uv_build"
|
|
31
36
|
|
|
32
37
|
[tool.ruff.lint]
|
|
33
38
|
extend-select = ["I"]
|
|
34
39
|
isort.known-first-party = ["varanus"]
|
|
35
|
-
|
|
36
|
-
[tool.pytest.ini_options]
|
|
37
|
-
addopts = "--tb=short -s"
|
|
38
|
-
DJANGO_SETTINGS_MODULE = "varanus.server.settings"
|
|
Binary file
|
|
Binary file
|
|
@@ -1,13 +1,16 @@
|
|
|
1
|
+
import logging
|
|
1
2
|
import os
|
|
2
3
|
import platform
|
|
4
|
+
import warnings
|
|
3
5
|
from importlib.metadata import distributions
|
|
4
6
|
from typing import Iterable
|
|
5
7
|
from urllib.parse import urlsplit
|
|
6
8
|
|
|
9
|
+
import varanus
|
|
7
10
|
from varanus.events import Context, NodeInfo
|
|
8
11
|
|
|
9
12
|
from ..utils import import_string
|
|
10
|
-
from .context import VaranusContext, current_context
|
|
13
|
+
from .context import LoggingTimer, VaranusContext, current_context
|
|
11
14
|
from .loggers import QueryLogger
|
|
12
15
|
from .transport.base import BaseTransport
|
|
13
16
|
|
|
@@ -61,6 +64,7 @@ class VaranusClient:
|
|
|
61
64
|
transport_class: str | type[BaseTransport] | None = None,
|
|
62
65
|
request_attr: str = "varanus",
|
|
63
66
|
logger_name: str = "varanus.request",
|
|
67
|
+
log_warnings: bool = True,
|
|
64
68
|
tags: dict | None = None,
|
|
65
69
|
include_headers: Iterable[str] | bool = False,
|
|
66
70
|
exclude_headers: Iterable[str] | None = None,
|
|
@@ -133,6 +137,9 @@ class VaranusClient:
|
|
|
133
137
|
except ImportError:
|
|
134
138
|
pass
|
|
135
139
|
self.send_all = send_all
|
|
140
|
+
if log_warnings:
|
|
141
|
+
warnings.simplefilter("default")
|
|
142
|
+
logging.captureWarnings(True)
|
|
136
143
|
self.configured = True
|
|
137
144
|
if install:
|
|
138
145
|
if "django.contrib.auth.middleware.AuthenticationMiddleware" in install:
|
|
@@ -181,6 +188,7 @@ class VaranusClient:
|
|
|
181
188
|
self.transport.ping(
|
|
182
189
|
NodeInfo(
|
|
183
190
|
name=self.node,
|
|
191
|
+
version=varanus.__version__,
|
|
184
192
|
platform=platform.platform(),
|
|
185
193
|
language=platform.python_implementation(),
|
|
186
194
|
language_version=platform.python_version(),
|
|
@@ -212,6 +220,8 @@ class VaranusClient:
|
|
|
212
220
|
def timer(self, name: str, tags: dict | None = None):
|
|
213
221
|
if ctx := current_context.get():
|
|
214
222
|
return ctx.timer(name, tags=tags)
|
|
223
|
+
else:
|
|
224
|
+
return LoggingTimer(name, tags=tags)
|
|
215
225
|
|
|
216
226
|
def context(self, name: str, tags: dict | None = None):
|
|
217
227
|
if ctx := current_context.get():
|
|
@@ -13,6 +13,7 @@ if TYPE_CHECKING:
|
|
|
13
13
|
from .client import VaranusClient
|
|
14
14
|
|
|
15
15
|
ONE_MS = timedelta(milliseconds=1)
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
class VaranusContext:
|
|
@@ -154,3 +155,13 @@ current_context: ContextVar[VaranusContext | None] = ContextVar(
|
|
|
154
155
|
"current_context",
|
|
155
156
|
default=None,
|
|
156
157
|
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@contextlib.contextmanager
|
|
161
|
+
def LoggingTimer(name: str, tags: dict | None = None):
|
|
162
|
+
start = time.monotonic()
|
|
163
|
+
try:
|
|
164
|
+
yield None
|
|
165
|
+
finally:
|
|
166
|
+
elapsed_ms = int((time.monotonic() - start) * 1000.0)
|
|
167
|
+
logger.debug("Timer %s (tags=%s) finished in %d ms", name, tags, elapsed_ms)
|
|
Binary file
|
|
@@ -26,6 +26,7 @@ class NodeInfo(Struct):
|
|
|
26
26
|
packages: dict[str, str]
|
|
27
27
|
settings: dict[str, str]
|
|
28
28
|
environment: dict[str, str]
|
|
29
|
+
version: str = ""
|
|
29
30
|
|
|
30
31
|
|
|
31
32
|
class StackLine(Struct):
|
|
@@ -153,8 +154,8 @@ class Metric(Event):
|
|
|
153
154
|
agg_count: int = 0
|
|
154
155
|
agg_sum: float = 0.0
|
|
155
156
|
agg_avg: float = 0.0
|
|
156
|
-
agg_min: float =
|
|
157
|
-
agg_max: float =
|
|
157
|
+
agg_min: float = float("inf")
|
|
158
|
+
agg_max: float = float("-inf")
|
|
158
159
|
|
|
159
160
|
def update(self, value: float):
|
|
160
161
|
self.agg_count += 1
|
|
Binary file
|
|
@@ -104,8 +104,9 @@ class DateRange(SearchField):
|
|
|
104
104
|
today = datetime.date.today()
|
|
105
105
|
buttons = [
|
|
106
106
|
(_("Today"), today - datetime.timedelta(days=0)),
|
|
107
|
-
(_("
|
|
108
|
-
(_("
|
|
107
|
+
(_("7 Days"), today - datetime.timedelta(days=7)),
|
|
108
|
+
(_("30 Days"), today - datetime.timedelta(days=30)),
|
|
109
|
+
(_("Year"), today - datetime.timedelta(days=365)),
|
|
109
110
|
]
|
|
110
111
|
return {
|
|
111
112
|
**super().get_context(queryset, field_data, request=request),
|
|
Binary file
|
|
Binary file
|
|
@@ -20,28 +20,47 @@ from .models import (
|
|
|
20
20
|
)
|
|
21
21
|
|
|
22
22
|
|
|
23
|
+
class SiteAuthMixIn:
|
|
24
|
+
"""
|
|
25
|
+
Allows anyone with access to the Site object (in SiteAdmin.get_queryset) full admin
|
|
26
|
+
capabilities.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def has_view_permission(self, request, obj=None):
|
|
30
|
+
return True
|
|
31
|
+
|
|
32
|
+
def has_add_permission(self, request, obj=None):
|
|
33
|
+
return True
|
|
34
|
+
|
|
35
|
+
def has_change_permission(self, request, obj=None):
|
|
36
|
+
return True
|
|
37
|
+
|
|
38
|
+
def has_delete_permission(self, request, obj=None):
|
|
39
|
+
return True
|
|
40
|
+
|
|
41
|
+
|
|
23
42
|
class SaveDefaultModelForm(forms.ModelForm):
|
|
24
43
|
def has_changed(self):
|
|
25
44
|
return self.instance._state.adding or super().has_changed()
|
|
26
45
|
|
|
27
46
|
|
|
28
|
-
class SiteMemberInline(admin.TabularInline):
|
|
47
|
+
class SiteMemberInline(SiteAuthMixIn, admin.TabularInline):
|
|
29
48
|
model = SiteMember
|
|
30
49
|
extra = 0
|
|
31
50
|
|
|
32
51
|
|
|
33
|
-
class SiteKeyInline(admin.TabularInline):
|
|
52
|
+
class SiteKeyInline(SiteAuthMixIn, admin.TabularInline):
|
|
34
53
|
model = SiteKey
|
|
35
54
|
form = SaveDefaultModelForm
|
|
36
55
|
extra = 0
|
|
37
56
|
|
|
38
57
|
|
|
39
|
-
class SiteIntegrationInline(admin.StackedInline):
|
|
58
|
+
class SiteIntegrationInline(SiteAuthMixIn, admin.StackedInline):
|
|
40
59
|
model = SiteIntegration
|
|
41
60
|
extra = 0
|
|
42
61
|
|
|
43
62
|
|
|
44
|
-
class SiteAdmin(admin.ModelAdmin):
|
|
63
|
+
class SiteAdmin(SiteAuthMixIn, admin.ModelAdmin):
|
|
45
64
|
list_display = ["name", "slug", "schema_name"]
|
|
46
65
|
inlines = [SiteMemberInline, SiteKeyInline, SiteIntegrationInline]
|
|
47
66
|
prepopulated_fields = {"slug": ["name"], "schema_name": ["name"]}
|
|
@@ -53,6 +72,7 @@ class SiteAdmin(admin.ModelAdmin):
|
|
|
53
72
|
"name",
|
|
54
73
|
"slug",
|
|
55
74
|
"schema_name",
|
|
75
|
+
"retention",
|
|
56
76
|
"module_filter",
|
|
57
77
|
]
|
|
58
78
|
},
|
|
@@ -71,6 +91,15 @@ class SiteAdmin(admin.ModelAdmin):
|
|
|
71
91
|
),
|
|
72
92
|
]
|
|
73
93
|
|
|
94
|
+
def get_queryset(self, request):
|
|
95
|
+
qs = Site.objects.all()
|
|
96
|
+
if not request.user.is_superuser:
|
|
97
|
+
qs = qs.filter(members__user=request.user, members__is_admin=True)
|
|
98
|
+
return qs
|
|
99
|
+
|
|
100
|
+
def has_module_permission(self, request):
|
|
101
|
+
return True
|
|
102
|
+
|
|
74
103
|
|
|
75
104
|
class RequestAdmin(admin.ModelAdmin):
|
|
76
105
|
list_display = [
|
|
@@ -145,6 +174,7 @@ class NodeAdmin(admin.ModelAdmin):
|
|
|
145
174
|
"name",
|
|
146
175
|
"site",
|
|
147
176
|
"environment",
|
|
177
|
+
"version",
|
|
148
178
|
"platform",
|
|
149
179
|
"language",
|
|
150
180
|
"language_version",
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
from enum import StrEnum
|
|
3
|
+
|
|
4
|
+
from django.db.models import Count, Max, Min, QuerySet
|
|
5
|
+
from django.db.models.functions import TruncDay, TruncHour, TruncMonth, TruncWeek
|
|
6
|
+
from django.utils import timezone
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Granularity(StrEnum):
|
|
10
|
+
HOUR = "hour"
|
|
11
|
+
DAY = "day"
|
|
12
|
+
WEEK = "week"
|
|
13
|
+
MONTH = "month"
|
|
14
|
+
|
|
15
|
+
@classmethod
|
|
16
|
+
def from_span(cls, span: datetime.timedelta) -> "Granularity":
|
|
17
|
+
if span <= datetime.timedelta(days=4):
|
|
18
|
+
return cls.HOUR
|
|
19
|
+
if span <= datetime.timedelta(days=90):
|
|
20
|
+
return cls.DAY
|
|
21
|
+
if span <= datetime.timedelta(days=730):
|
|
22
|
+
return cls.WEEK
|
|
23
|
+
return cls.MONTH
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def truncator(self):
|
|
27
|
+
return {
|
|
28
|
+
Granularity.HOUR: TruncHour,
|
|
29
|
+
Granularity.DAY: TruncDay,
|
|
30
|
+
Granularity.WEEK: TruncWeek,
|
|
31
|
+
Granularity.MONTH: TruncMonth,
|
|
32
|
+
}[self]
|
|
33
|
+
|
|
34
|
+
def floor(self, value: datetime.datetime) -> datetime.datetime:
|
|
35
|
+
if self is Granularity.HOUR:
|
|
36
|
+
return value.replace(minute=0, second=0, microsecond=0)
|
|
37
|
+
if self is Granularity.DAY:
|
|
38
|
+
return value.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
39
|
+
if self is Granularity.WEEK:
|
|
40
|
+
day = value.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
41
|
+
return day - datetime.timedelta(days=day.weekday())
|
|
42
|
+
return value.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
|
43
|
+
|
|
44
|
+
def next(self, value: datetime.datetime) -> datetime.datetime:
|
|
45
|
+
if self is Granularity.HOUR:
|
|
46
|
+
return value + datetime.timedelta(hours=1)
|
|
47
|
+
if self is Granularity.DAY:
|
|
48
|
+
return value + datetime.timedelta(days=1)
|
|
49
|
+
if self is Granularity.WEEK:
|
|
50
|
+
return value + datetime.timedelta(days=7)
|
|
51
|
+
if value.month == 12:
|
|
52
|
+
return value.replace(year=value.year + 1, month=1, day=1)
|
|
53
|
+
return value.replace(month=value.month + 1, day=1)
|
|
54
|
+
|
|
55
|
+
def label(self, value: datetime.datetime) -> str:
|
|
56
|
+
if self is Granularity.HOUR:
|
|
57
|
+
return value.strftime("%Y-%m-%d %H:00")
|
|
58
|
+
if self is Granularity.DAY:
|
|
59
|
+
return value.strftime("%Y-%m-%d")
|
|
60
|
+
if self is Granularity.WEEK:
|
|
61
|
+
return f"Week of {value.strftime('%Y-%m-%d')}"
|
|
62
|
+
return value.strftime("%Y-%m")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class Histogram:
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
queryset: QuerySet,
|
|
69
|
+
start_date: datetime.date | None = None,
|
|
70
|
+
end_date: datetime.date | None = None,
|
|
71
|
+
):
|
|
72
|
+
self.queryset = queryset
|
|
73
|
+
self.start_date = start_date
|
|
74
|
+
self.end_date = end_date
|
|
75
|
+
self.tz = timezone.get_current_timezone()
|
|
76
|
+
|
|
77
|
+
def _to_localtime(self, value: datetime.datetime) -> datetime.datetime:
|
|
78
|
+
if timezone.is_naive(value):
|
|
79
|
+
value = timezone.make_aware(value, self.tz)
|
|
80
|
+
return timezone.localtime(value, self.tz)
|
|
81
|
+
|
|
82
|
+
def _resolve_range(self) -> tuple[datetime.datetime, datetime.datetime] | None:
|
|
83
|
+
start_date = self.start_date
|
|
84
|
+
end_date = self.end_date
|
|
85
|
+
if start_date and not end_date:
|
|
86
|
+
end_date = timezone.localdate()
|
|
87
|
+
if end_date and not start_date:
|
|
88
|
+
start_date = end_date
|
|
89
|
+
|
|
90
|
+
if start_date and end_date:
|
|
91
|
+
start = timezone.make_aware(
|
|
92
|
+
datetime.datetime.combine(start_date, datetime.time.min),
|
|
93
|
+
self.tz,
|
|
94
|
+
)
|
|
95
|
+
end = timezone.make_aware(
|
|
96
|
+
datetime.datetime.combine(end_date, datetime.time.max),
|
|
97
|
+
self.tz,
|
|
98
|
+
)
|
|
99
|
+
return (start, end)
|
|
100
|
+
|
|
101
|
+
agg = self.queryset.aggregate(start=Min("timestamp"), end=Max("timestamp"))
|
|
102
|
+
if not agg["start"] or not agg["end"]:
|
|
103
|
+
return None
|
|
104
|
+
return (self._to_localtime(agg["start"]), self._to_localtime(agg["end"]))
|
|
105
|
+
|
|
106
|
+
def as_context(self) -> dict | None:
|
|
107
|
+
range_values = self._resolve_range()
|
|
108
|
+
if not range_values:
|
|
109
|
+
return None
|
|
110
|
+
start, end = range_values
|
|
111
|
+
|
|
112
|
+
granularity = Granularity.from_span(end - start)
|
|
113
|
+
raw_buckets = (
|
|
114
|
+
self.queryset.annotate(bucket=granularity.truncator("timestamp"))
|
|
115
|
+
.values("bucket")
|
|
116
|
+
.annotate(num=Count("id"))
|
|
117
|
+
.order_by("bucket")
|
|
118
|
+
)
|
|
119
|
+
counts = {
|
|
120
|
+
self._to_localtime(item["bucket"]): item["num"] for item in raw_buckets
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
bucket_start = granularity.floor(start)
|
|
124
|
+
bucket_end = granularity.floor(end)
|
|
125
|
+
if bucket_end < bucket_start:
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
buckets = []
|
|
129
|
+
cursor = bucket_start
|
|
130
|
+
max_count = max(counts.values()) if counts else 0
|
|
131
|
+
while cursor <= bucket_end:
|
|
132
|
+
count = counts.get(cursor, 0)
|
|
133
|
+
buckets.append(
|
|
134
|
+
{
|
|
135
|
+
"label": granularity.label(cursor),
|
|
136
|
+
"count": count,
|
|
137
|
+
"height_pct": (count / max_count * 100) if max_count else 0,
|
|
138
|
+
}
|
|
139
|
+
)
|
|
140
|
+
cursor = granularity.next(cursor)
|
|
141
|
+
if len(buckets) > 500:
|
|
142
|
+
break
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
"buckets": buckets,
|
|
146
|
+
"granularity": str(granularity),
|
|
147
|
+
"start_label": granularity.label(bucket_start),
|
|
148
|
+
"end_label": granularity.label(bucket_end),
|
|
149
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from django.core.management import BaseCommand
|
|
2
|
+
|
|
3
|
+
from varanus.server.tasks import maintenance
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Command(BaseCommand):
|
|
7
|
+
help = "Manually runs the maintenance task."
|
|
8
|
+
|
|
9
|
+
def add_arguments(self, parser):
|
|
10
|
+
parser.add_argument(
|
|
11
|
+
"-q",
|
|
12
|
+
"--queue",
|
|
13
|
+
action="store_true",
|
|
14
|
+
help="Queues the task instead of running it directly",
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
def handle(self, *args, **options):
|
|
18
|
+
if options["queue"]:
|
|
19
|
+
maintenance.enqueue()
|
|
20
|
+
else:
|
|
21
|
+
maintenance.call()
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Generated by Django 6.0 on 2025-12-20 21:41
|
|
2
|
+
|
|
3
|
+
from django.db import migrations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
dependencies = [
|
|
8
|
+
("varanus", "0001_initial"),
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
operations = [
|
|
12
|
+
migrations.RunSQL("DROP TABLE IF EXISTS tasks_scheduledtask"),
|
|
13
|
+
]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Generated by Django 6.0 on 2025-12-22 17:46
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
import varanus.server.models
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Migration(migrations.Migration):
|
|
9
|
+
dependencies = [
|
|
10
|
+
("varanus", "0002_drop_scheduledtask"),
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
operations = [
|
|
14
|
+
migrations.AddField(
|
|
15
|
+
model_name="site",
|
|
16
|
+
name="retention",
|
|
17
|
+
field=models.CharField(
|
|
18
|
+
default=varanus.server.models.default_retention, max_length=20
|
|
19
|
+
),
|
|
20
|
+
),
|
|
21
|
+
]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Generated by Django 6.0 on 2025-12-29 18:41
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
dependencies = [
|
|
8
|
+
("varanus", "0003_site_retention"),
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
operations = [
|
|
12
|
+
migrations.AddField(
|
|
13
|
+
model_name="node",
|
|
14
|
+
name="version",
|
|
15
|
+
field=models.CharField(blank=True, max_length=20),
|
|
16
|
+
),
|
|
17
|
+
]
|
|
@@ -4,7 +4,7 @@ from typing import Self
|
|
|
4
4
|
|
|
5
5
|
import msgspec
|
|
6
6
|
import sqlparse
|
|
7
|
-
from
|
|
7
|
+
from dbtasks import Duration
|
|
8
8
|
from django.conf import settings
|
|
9
9
|
from django.contrib.contenttypes.models import ContentType
|
|
10
10
|
from django.core.management import call_command
|
|
@@ -40,6 +40,10 @@ def default_module_filter():
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
|
|
43
|
+
def default_retention():
|
|
44
|
+
return settings.VARANUS_DEFAULT_RETENTION
|
|
45
|
+
|
|
46
|
+
|
|
43
47
|
class Site(models.Model):
|
|
44
48
|
name = models.CharField(max_length=200)
|
|
45
49
|
slug = models.SlugField(max_length=200, unique=True)
|
|
@@ -59,12 +63,14 @@ class Site(models.Model):
|
|
|
59
63
|
show_metrics = models.BooleanField(default=True)
|
|
60
64
|
|
|
61
65
|
module_filter = models.JSONField(default=default_module_filter)
|
|
66
|
+
retention = models.CharField(max_length=20, default=default_retention)
|
|
62
67
|
|
|
63
68
|
# Not exactly QuerySets, but close enough.
|
|
64
69
|
members: QuerySet["SiteMember"]
|
|
65
70
|
integrations: QuerySet["SiteIntegration"]
|
|
66
71
|
nodes: QuerySet["Node"]
|
|
67
72
|
keys: QuerySet["SiteKey"]
|
|
73
|
+
contexts: QuerySet["Context"]
|
|
68
74
|
logs: QuerySet["Log"]
|
|
69
75
|
errors: QuerySet["Error"]
|
|
70
76
|
metrics: QuerySet["Metric"]
|
|
@@ -116,6 +122,14 @@ class Site(models.Model):
|
|
|
116
122
|
return True
|
|
117
123
|
return False
|
|
118
124
|
|
|
125
|
+
def cleanup(self):
|
|
126
|
+
cutoff = timezone.now() - Duration(self.retention)
|
|
127
|
+
return {
|
|
128
|
+
"retention": self.retention,
|
|
129
|
+
"cutoff": cutoff.isoformat(),
|
|
130
|
+
"deleted": self.contexts.filter(timestamp__lt=cutoff).delete(),
|
|
131
|
+
}
|
|
132
|
+
|
|
119
133
|
|
|
120
134
|
class SiteMember(models.Model):
|
|
121
135
|
site = models.ForeignKey(Site, on_delete=models.CASCADE, related_name="members")
|
|
@@ -221,6 +235,7 @@ class EnvironmentModel(models.Model):
|
|
|
221
235
|
|
|
222
236
|
class Node(EnvironmentModel):
|
|
223
237
|
name = models.CharField(max_length=200)
|
|
238
|
+
version = models.CharField(max_length=20, blank=True)
|
|
224
239
|
platform = models.CharField(max_length=200)
|
|
225
240
|
language = models.CharField(max_length=100)
|
|
226
241
|
language_version = models.CharField(max_length=100)
|
|
@@ -272,6 +287,7 @@ class Node(EnvironmentModel):
|
|
|
272
287
|
name=info.name,
|
|
273
288
|
environment=environment,
|
|
274
289
|
defaults={
|
|
290
|
+
"version": info.version,
|
|
275
291
|
"platform": info.platform,
|
|
276
292
|
"language": info.language,
|
|
277
293
|
"language_version": info.language_version,
|