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.
Files changed (113) hide show
  1. varanus-0.1.1/PKG-INFO +46 -0
  2. varanus-0.1.1/README.md +25 -0
  3. {varanus-0.1.0.dev7 → varanus-0.1.1}/pyproject.toml +11 -10
  4. varanus-0.1.1/src/varanus/.DS_Store +0 -0
  5. varanus-0.1.1/src/varanus/__init__.py +6 -0
  6. varanus-0.1.1/src/varanus/client/.DS_Store +0 -0
  7. {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/client/client.py +11 -1
  8. {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/client/context.py +11 -0
  9. varanus-0.1.1/src/varanus/client/transport/.DS_Store +0 -0
  10. {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/client/transport/database.py +2 -0
  11. {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/events.py +3 -2
  12. varanus-0.1.1/src/varanus/search/.DS_Store +0 -0
  13. {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/search/fields.py +3 -2
  14. varanus-0.1.1/src/varanus/search/templates/search/.DS_Store +0 -0
  15. varanus-0.1.1/src/varanus/server/.DS_Store +0 -0
  16. {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/admin.py +34 -4
  17. varanus-0.1.1/src/varanus/server/histogram.py +149 -0
  18. varanus-0.1.1/src/varanus/server/management/commands/.DS_Store +0 -0
  19. varanus-0.1.1/src/varanus/server/management/commands/maintenance.py +21 -0
  20. varanus-0.1.1/src/varanus/server/migrations/0002_drop_scheduledtask.py +13 -0
  21. varanus-0.1.1/src/varanus/server/migrations/0003_site_retention.py +21 -0
  22. varanus-0.1.1/src/varanus/server/migrations/0004_node_version.py +17 -0
  23. {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/models.py +17 -1
  24. {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/settings.py +26 -7
  25. varanus-0.1.1/src/varanus/server/static/.DS_Store +0 -0
  26. varanus-0.1.1/src/varanus/server/static/css/.DS_Store +0 -0
  27. varanus-0.1.1/src/varanus/server/static/css/varanus.css +30 -0
  28. varanus-0.1.1/src/varanus/server/static/js/.DS_Store +0 -0
  29. varanus-0.1.1/src/varanus/server/static/js/varanus.js +85 -0
  30. {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/tasks.py +9 -0
  31. varanus-0.1.1/src/varanus/server/templates/.DS_Store +0 -0
  32. {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/templates/dashboard.html +2 -0
  33. varanus-0.1.1/src/varanus/server/templates/site/.DS_Store +0 -0
  34. {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/templates/site/base.html +12 -6
  35. {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/templates/site/details/metric.html +3 -3
  36. {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/templates/site/errors.html +1 -0
  37. varanus-0.1.1/src/varanus/server/templates/site/histogram.html +23 -0
  38. {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/templates/site/logs.html +1 -0
  39. {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/templates/site/metrics.html +4 -3
  40. {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/templates/site/overview.html +13 -3
  41. {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/templates/site/queries.html +1 -0
  42. {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/templates/site/requests.html +1 -0
  43. varanus-0.1.1/src/varanus/server/templatetags/varanus.py +50 -0
  44. varanus-0.1.1/src/varanus/server/views/.DS_Store +0 -0
  45. {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/views/site.py +8 -0
  46. varanus-0.1.0.dev7/PKG-INFO +0 -19
  47. varanus-0.1.0.dev7/README.md +0 -3
  48. varanus-0.1.0.dev7/src/varanus/server/management/commands/serve.py +0 -64
  49. varanus-0.1.0.dev7/src/varanus/server/static/css/varanus.css +0 -4
  50. varanus-0.1.0.dev7/src/varanus/server/static/js/varanus.js +0 -0
  51. varanus-0.1.0.dev7/src/varanus/server/templatetags/varanus.py +0 -20
  52. varanus-0.1.0.dev7/src/varanus/tasks/__init__.py +0 -0
  53. varanus-0.1.0.dev7/src/varanus/tasks/admin.py +0 -15
  54. varanus-0.1.0.dev7/src/varanus/tasks/apps.py +0 -0
  55. varanus-0.1.0.dev7/src/varanus/tasks/backend.py +0 -62
  56. varanus-0.1.0.dev7/src/varanus/tasks/management/__init__.py +0 -0
  57. varanus-0.1.0.dev7/src/varanus/tasks/management/commands/__init__.py +0 -0
  58. varanus-0.1.0.dev7/src/varanus/tasks/management/commands/tasker.py +0 -20
  59. varanus-0.1.0.dev7/src/varanus/tasks/migrations/0001_initial.py +0 -66
  60. varanus-0.1.0.dev7/src/varanus/tasks/migrations/__init__.py +0 -0
  61. varanus-0.1.0.dev7/src/varanus/tasks/models.py +0 -131
  62. varanus-0.1.0.dev7/src/varanus/tasks/runner.py +0 -107
  63. {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/client/__init__.py +0 -0
  64. {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/client/apps.py +0 -0
  65. {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/client/loggers.py +0 -0
  66. {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/client/middleware.py +0 -0
  67. {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/client/transport/__init__.py +0 -0
  68. {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/client/transport/base.py +0 -0
  69. {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/client/transport/http.py +0 -0
  70. {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/search/__init__.py +0 -0
  71. {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/search/base.py +0 -0
  72. {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/search/templates/search/daterange.html +0 -0
  73. {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/search/templates/search/filter.html +0 -0
  74. {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/search/templates/search/hidden.html +0 -0
  75. {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/search/templates/search/multifacet.html +0 -0
  76. {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/search/templates/search/search.html +0 -0
  77. {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/search/utils.py +0 -0
  78. {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/__init__.py +0 -0
  79. {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/__main__.py +0 -0
  80. {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/apps.py +0 -0
  81. {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/asgi.py +0 -0
  82. {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/context_processors.py +0 -0
  83. {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/integrations/__init__.py +0 -0
  84. {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/integrations/base.py +0 -0
  85. {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/integrations/squish.py +0 -0
  86. {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/management/__init__.py +0 -0
  87. {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/management/commands/__init__.py +0 -0
  88. {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/management/commands/migrateall.py +0 -0
  89. {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/middleware.py +0 -0
  90. {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/migrations/0001_initial.py +0 -0
  91. {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/migrations/__init__.py +0 -0
  92. {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/router.py +0 -0
  93. {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/templates/base.html +0 -0
  94. {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/templates/registration/login.html +0 -0
  95. {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/templates/site/details/environment_nodes.html +0 -0
  96. {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/templates/site/details/error.html +0 -0
  97. {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/templates/site/details/log.html +0 -0
  98. {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/templates/site/details/node_env.html +0 -0
  99. {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/templates/site/details/node_environments.html +0 -0
  100. {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/templates/site/details/node_packages.html +0 -0
  101. {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/templates/site/details/node_settings.html +0 -0
  102. {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/templates/site/details/query.html +0 -0
  103. {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/templates/site/details/request.html +0 -0
  104. {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/templatetags/__init__.py +0 -0
  105. {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/urls.py +0 -0
  106. {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/utils.py +0 -0
  107. {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/views/__init__.py +0 -0
  108. {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/views/api.py +0 -0
  109. {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/views/base.py +0 -0
  110. {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/views/dashboard.py +0 -0
  111. {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/server/wsgi.py +0 -0
  112. {varanus-0.1.0.dev7 → varanus-0.1.1}/src/varanus/utils.py +0 -0
  113. /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.
@@ -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.0.dev7"
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.9.2,<0.10.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
@@ -0,0 +1,6 @@
1
+ from .version import __version__, __version_info__
2
+
3
+ __all__ = [
4
+ "__version__",
5
+ "__version_info__",
6
+ ]
@@ -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)
@@ -47,6 +47,8 @@ class ModelTransport(BaseTransport):
47
47
  def send(self, event: events.Context):
48
48
  from varanus.server.tasks import ingest
49
49
 
50
+ self.ensure_site()
51
+
50
52
  ingest.enqueue(
51
53
  self.site.pk,
52
54
  self.node_name,
@@ -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 = 0.0
157
- agg_max: float = 0.0
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
@@ -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
- (_("Last 7 Days"), today - datetime.timedelta(days=7)),
108
- (_("Last 30 Days"), today - datetime.timedelta(days=30)),
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),
@@ -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
+ }
@@ -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 cconf import Duration
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,