varanus 0.1.0.dev6__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.dev6 → 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.dev6 → varanus-0.1.1}/src/varanus/client/__init__.py +1 -0
- {varanus-0.1.0.dev6 → varanus-0.1.1}/src/varanus/client/client.py +15 -1
- {varanus-0.1.0.dev6 → varanus-0.1.1}/src/varanus/client/context.py +22 -0
- varanus-0.1.1/src/varanus/client/transport/.DS_Store +0 -0
- {varanus-0.1.0.dev6 → varanus-0.1.1}/src/varanus/client/transport/database.py +2 -0
- {varanus-0.1.0.dev6 → varanus-0.1.1}/src/varanus/client/transport/http.py +2 -2
- {varanus-0.1.0.dev6 → 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.dev6 → 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.dev6 → 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.dev6 → varanus-0.1.1}/src/varanus/server/models.py +17 -1
- {varanus-0.1.0.dev6 → 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.dev6 → 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.dev6 → 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.dev6 → varanus-0.1.1}/src/varanus/server/templates/site/base.html +12 -6
- {varanus-0.1.0.dev6 → varanus-0.1.1}/src/varanus/server/templates/site/details/metric.html +3 -3
- {varanus-0.1.0.dev6 → varanus-0.1.1}/src/varanus/server/templates/site/details/node_env.html +1 -1
- {varanus-0.1.0.dev6 → varanus-0.1.1}/src/varanus/server/templates/site/details/node_settings.html +1 -1
- {varanus-0.1.0.dev6 → varanus-0.1.1}/src/varanus/server/templates/site/details/request.html +4 -0
- {varanus-0.1.0.dev6 → 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.dev6 → varanus-0.1.1}/src/varanus/server/templates/site/logs.html +1 -0
- {varanus-0.1.0.dev6 → varanus-0.1.1}/src/varanus/server/templates/site/metrics.html +4 -3
- {varanus-0.1.0.dev6 → varanus-0.1.1}/src/varanus/server/templates/site/overview.html +13 -3
- {varanus-0.1.0.dev6 → varanus-0.1.1}/src/varanus/server/templates/site/queries.html +1 -0
- {varanus-0.1.0.dev6 → 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.dev6 → varanus-0.1.1}/src/varanus/server/views/site.py +8 -0
- varanus-0.1.0.dev6/PKG-INFO +0 -19
- varanus-0.1.0.dev6/README.md +0 -3
- varanus-0.1.0.dev6/src/varanus/server/management/commands/serve.py +0 -64
- varanus-0.1.0.dev6/src/varanus/server/static/css/varanus.css +0 -4
- varanus-0.1.0.dev6/src/varanus/server/static/js/varanus.js +0 -0
- varanus-0.1.0.dev6/src/varanus/server/templatetags/varanus.py +0 -20
- varanus-0.1.0.dev6/src/varanus/tasks/__init__.py +0 -0
- varanus-0.1.0.dev6/src/varanus/tasks/admin.py +0 -15
- varanus-0.1.0.dev6/src/varanus/tasks/apps.py +0 -0
- varanus-0.1.0.dev6/src/varanus/tasks/backend.py +0 -62
- varanus-0.1.0.dev6/src/varanus/tasks/management/__init__.py +0 -0
- varanus-0.1.0.dev6/src/varanus/tasks/management/commands/__init__.py +0 -0
- varanus-0.1.0.dev6/src/varanus/tasks/management/commands/tasker.py +0 -20
- varanus-0.1.0.dev6/src/varanus/tasks/migrations/0001_initial.py +0 -66
- varanus-0.1.0.dev6/src/varanus/tasks/migrations/__init__.py +0 -0
- varanus-0.1.0.dev6/src/varanus/tasks/models.py +0 -131
- varanus-0.1.0.dev6/src/varanus/tasks/runner.py +0 -107
- {varanus-0.1.0.dev6 → varanus-0.1.1}/src/varanus/client/apps.py +0 -0
- {varanus-0.1.0.dev6 → varanus-0.1.1}/src/varanus/client/loggers.py +0 -0
- {varanus-0.1.0.dev6 → varanus-0.1.1}/src/varanus/client/middleware.py +0 -0
- {varanus-0.1.0.dev6 → varanus-0.1.1}/src/varanus/client/transport/__init__.py +0 -0
- {varanus-0.1.0.dev6 → varanus-0.1.1}/src/varanus/client/transport/base.py +0 -0
- {varanus-0.1.0.dev6 → varanus-0.1.1}/src/varanus/search/__init__.py +0 -0
- {varanus-0.1.0.dev6 → varanus-0.1.1}/src/varanus/search/base.py +0 -0
- {varanus-0.1.0.dev6 → varanus-0.1.1}/src/varanus/search/templates/search/daterange.html +0 -0
- {varanus-0.1.0.dev6 → varanus-0.1.1}/src/varanus/search/templates/search/filter.html +0 -0
- {varanus-0.1.0.dev6 → varanus-0.1.1}/src/varanus/search/templates/search/hidden.html +0 -0
- {varanus-0.1.0.dev6 → varanus-0.1.1}/src/varanus/search/templates/search/multifacet.html +0 -0
- {varanus-0.1.0.dev6 → varanus-0.1.1}/src/varanus/search/templates/search/search.html +0 -0
- {varanus-0.1.0.dev6 → varanus-0.1.1}/src/varanus/search/utils.py +0 -0
- {varanus-0.1.0.dev6 → varanus-0.1.1}/src/varanus/server/__init__.py +0 -0
- {varanus-0.1.0.dev6 → varanus-0.1.1}/src/varanus/server/__main__.py +0 -0
- {varanus-0.1.0.dev6 → varanus-0.1.1}/src/varanus/server/apps.py +0 -0
- {varanus-0.1.0.dev6 → varanus-0.1.1}/src/varanus/server/asgi.py +0 -0
- {varanus-0.1.0.dev6 → varanus-0.1.1}/src/varanus/server/context_processors.py +0 -0
- {varanus-0.1.0.dev6 → varanus-0.1.1}/src/varanus/server/integrations/__init__.py +0 -0
- {varanus-0.1.0.dev6 → varanus-0.1.1}/src/varanus/server/integrations/base.py +0 -0
- {varanus-0.1.0.dev6 → varanus-0.1.1}/src/varanus/server/integrations/squish.py +0 -0
- {varanus-0.1.0.dev6 → varanus-0.1.1}/src/varanus/server/management/__init__.py +0 -0
- {varanus-0.1.0.dev6 → varanus-0.1.1}/src/varanus/server/management/commands/__init__.py +0 -0
- {varanus-0.1.0.dev6 → varanus-0.1.1}/src/varanus/server/management/commands/migrateall.py +0 -0
- {varanus-0.1.0.dev6 → varanus-0.1.1}/src/varanus/server/middleware.py +0 -0
- {varanus-0.1.0.dev6 → varanus-0.1.1}/src/varanus/server/migrations/0001_initial.py +0 -0
- {varanus-0.1.0.dev6 → varanus-0.1.1}/src/varanus/server/migrations/__init__.py +0 -0
- {varanus-0.1.0.dev6 → varanus-0.1.1}/src/varanus/server/router.py +0 -0
- {varanus-0.1.0.dev6 → varanus-0.1.1}/src/varanus/server/templates/base.html +0 -0
- {varanus-0.1.0.dev6 → varanus-0.1.1}/src/varanus/server/templates/registration/login.html +0 -0
- {varanus-0.1.0.dev6 → varanus-0.1.1}/src/varanus/server/templates/site/details/environment_nodes.html +0 -0
- {varanus-0.1.0.dev6 → varanus-0.1.1}/src/varanus/server/templates/site/details/error.html +0 -0
- {varanus-0.1.0.dev6 → varanus-0.1.1}/src/varanus/server/templates/site/details/log.html +0 -0
- {varanus-0.1.0.dev6 → varanus-0.1.1}/src/varanus/server/templates/site/details/node_environments.html +0 -0
- {varanus-0.1.0.dev6 → varanus-0.1.1}/src/varanus/server/templates/site/details/node_packages.html +0 -0
- {varanus-0.1.0.dev6 → varanus-0.1.1}/src/varanus/server/templates/site/details/query.html +0 -0
- {varanus-0.1.0.dev6 → varanus-0.1.1}/src/varanus/server/templatetags/__init__.py +0 -0
- {varanus-0.1.0.dev6 → varanus-0.1.1}/src/varanus/server/urls.py +0 -0
- {varanus-0.1.0.dev6 → varanus-0.1.1}/src/varanus/server/utils.py +0 -0
- {varanus-0.1.0.dev6 → varanus-0.1.1}/src/varanus/server/views/__init__.py +0 -0
- {varanus-0.1.0.dev6 → varanus-0.1.1}/src/varanus/server/views/api.py +0 -0
- {varanus-0.1.0.dev6 → varanus-0.1.1}/src/varanus/server/views/base.py +0 -0
- {varanus-0.1.0.dev6 → varanus-0.1.1}/src/varanus/server/views/dashboard.py +0 -0
- {varanus-0.1.0.dev6 → varanus-0.1.1}/src/varanus/server/wsgi.py +0 -0
- {varanus-0.1.0.dev6 → varanus-0.1.1}/src/varanus/utils.py +0 -0
- /varanus-0.1.0.dev6/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(),
|
|
@@ -209,6 +217,12 @@ class VaranusClient:
|
|
|
209
217
|
if ctx := current_context.get():
|
|
210
218
|
ctx.metric(name, value, tags=tags)
|
|
211
219
|
|
|
220
|
+
def timer(self, name: str, tags: dict | None = None):
|
|
221
|
+
if ctx := current_context.get():
|
|
222
|
+
return ctx.timer(name, tags=tags)
|
|
223
|
+
else:
|
|
224
|
+
return LoggingTimer(name, tags=tags)
|
|
225
|
+
|
|
212
226
|
def context(self, name: str, tags: dict | None = None):
|
|
213
227
|
if ctx := current_context.get():
|
|
214
228
|
return ctx.context(name, tags)
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import contextlib
|
|
1
2
|
import inspect
|
|
2
3
|
import logging
|
|
3
4
|
import sys
|
|
5
|
+
import time
|
|
4
6
|
from contextvars import ContextVar, Token
|
|
5
7
|
from datetime import timedelta
|
|
6
8
|
from typing import TYPE_CHECKING
|
|
@@ -11,6 +13,7 @@ if TYPE_CHECKING:
|
|
|
11
13
|
from .client import VaranusClient
|
|
12
14
|
|
|
13
15
|
ONE_MS = timedelta(milliseconds=1)
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
14
17
|
|
|
15
18
|
|
|
16
19
|
class VaranusContext:
|
|
@@ -138,8 +141,27 @@ class VaranusContext:
|
|
|
138
141
|
self.metrics[name] = Metric(name=name, tags=tags or {})
|
|
139
142
|
self.metrics[name].update(value)
|
|
140
143
|
|
|
144
|
+
@contextlib.contextmanager
|
|
145
|
+
def timer(self, name: str, tags: dict | None = None):
|
|
146
|
+
start = time.monotonic()
|
|
147
|
+
try:
|
|
148
|
+
yield self
|
|
149
|
+
finally:
|
|
150
|
+
elapsed = time.monotonic() - start
|
|
151
|
+
self.metric(name, elapsed, tags=tags)
|
|
152
|
+
|
|
141
153
|
|
|
142
154
|
current_context: ContextVar[VaranusContext | None] = ContextVar(
|
|
143
155
|
"current_context",
|
|
144
156
|
default=None,
|
|
145
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
|
|
@@ -48,7 +48,7 @@ class HttpTransport(BaseTransport):
|
|
|
48
48
|
|
|
49
49
|
def sender(pending: queue.SimpleQueue, client: httpx.Client, url: str, rate: float):
|
|
50
50
|
while True:
|
|
51
|
-
start = time.
|
|
51
|
+
start = time.monotonic()
|
|
52
52
|
events = []
|
|
53
53
|
while True:
|
|
54
54
|
try:
|
|
@@ -62,7 +62,7 @@ def sender(pending: queue.SimpleQueue, client: httpx.Client, url: str, rate: flo
|
|
|
62
62
|
client.post(url, content=msgspec.json.encode(events))
|
|
63
63
|
except Exception:
|
|
64
64
|
logger.exception("error sending to %s", url)
|
|
65
|
-
elapsed = time.
|
|
65
|
+
elapsed = time.monotonic() - start
|
|
66
66
|
time.sleep(max(rate - elapsed, 1.0))
|
|
67
67
|
|
|
68
68
|
|
|
@@ -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
|
+
]
|