varanus 0.1.0.dev0__tar.gz → 0.1.0.dev1__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.0.dev0 → varanus-0.1.0.dev1}/PKG-INFO +3 -3
- {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/pyproject.toml +3 -3
- {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/client/transport/http.py +13 -3
- varanus-0.1.0.dev1/src/varanus/server/asgi.py +7 -0
- varanus-0.1.0.dev1/src/varanus/server/migrations/0002_context_node_error_node_log_node_metric_node_and_more.py +79 -0
- {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/server/models.py +24 -0
- varanus-0.1.0.dev1/src/varanus/server/search/__init__.py +9 -0
- varanus-0.1.0.dev1/src/varanus/server/search/base.py +106 -0
- varanus-0.1.0.dev1/src/varanus/server/search/date.py +33 -0
- varanus-0.1.0.dev1/src/varanus/server/search/facet.py +59 -0
- {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/server/settings.py +2 -2
- varanus-0.1.0.dev1/src/varanus/server/static/css/varanus.css +4 -0
- {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/server/templates/base.html +3 -1
- varanus-0.1.0.dev1/src/varanus/server/templates/search/daterange.html +10 -0
- varanus-0.1.0.dev1/src/varanus/server/templates/search/multifacet.html +11 -0
- {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/server/templates/site/base.html +19 -12
- varanus-0.1.0.dev1/src/varanus/server/templates/site/details/error.html +1 -0
- varanus-0.1.0.dev1/src/varanus/server/templates/site/details/log.html +1 -0
- varanus-0.1.0.dev1/src/varanus/server/templates/site/details/metric.html +1 -0
- varanus-0.1.0.dev1/src/varanus/server/templates/site/details/query.html +1 -0
- varanus-0.1.0.dev1/src/varanus/server/templates/site/details/request.html +44 -0
- {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/server/templates/site/errors.html +6 -4
- {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/server/templates/site/logs.html +6 -4
- {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/server/templates/site/metrics.html +6 -4
- varanus-0.1.0.dev1/src/varanus/server/templates/site/overview.html +48 -0
- {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/server/templates/site/queries.html +8 -4
- {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/server/templates/site/requests.html +6 -4
- {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/server/urls.py +5 -0
- {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/server/views/api.py +7 -0
- {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/server/views/base.py +1 -1
- varanus-0.1.0.dev1/src/varanus/server/views/site.py +112 -0
- varanus-0.1.0.dev0/src/varanus/server/asgi.py +0 -18
- varanus-0.1.0.dev0/src/varanus/server/management/commands/runserver.py +0 -15
- varanus-0.1.0.dev0/src/varanus/server/search.py +0 -120
- varanus-0.1.0.dev0/src/varanus/server/static/css/varanus.css +0 -0
- varanus-0.1.0.dev0/src/varanus/server/templates/site/overview.html +0 -14
- varanus-0.1.0.dev0/src/varanus/server/views/site.py +0 -69
- varanus-0.1.0.dev0/src/varanus/server/websockets.py +0 -13
- {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/README.md +0 -0
- {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/__init__.py +0 -0
- {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/client/__init__.py +0 -0
- {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/client/apps.py +0 -0
- {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/client/client.py +0 -0
- {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/client/context.py +0 -0
- {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/client/loggers.py +0 -0
- {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/client/middleware.py +0 -0
- {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/client/transport/__init__.py +0 -0
- {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/client/transport/base.py +0 -0
- {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/client/transport/database.py +0 -0
- {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/events.py +0 -0
- {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/server/__init__.py +0 -0
- {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/server/__main__.py +0 -0
- {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/server/admin.py +0 -0
- {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/server/apps.py +0 -0
- {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/server/context_processors.py +0 -0
- {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/server/management/__init__.py +0 -0
- {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/server/management/commands/__init__.py +0 -0
- {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/server/management/commands/migrateall.py +0 -0
- {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/server/middleware.py +0 -0
- {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/server/migrations/0001_initial.py +0 -0
- {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/server/migrations/__init__.py +0 -0
- {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/server/router.py +0 -0
- {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/server/static/js/varanus.js +0 -0
- {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/server/templates/dashboard.html +0 -0
- {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/server/templates/registration/login.html +0 -0
- {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/server/views/__init__.py +0 -0
- {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/server/views/dashboard.py +0 -0
- {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/server/wsgi.py +0 -0
- {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/utils.py +0 -0
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: varanus
|
|
3
|
-
Version: 0.1.0.
|
|
3
|
+
Version: 0.1.0.dev1
|
|
4
4
|
Summary: Django application monitoring.
|
|
5
5
|
Requires-Dist: httpx>=0.27.0
|
|
6
6
|
Requires-Dist: msgspec>=0.19.0
|
|
7
7
|
Requires-Dist: cconf>=1.0.0 ; extra == 'server'
|
|
8
8
|
Requires-Dist: django~=5.2.0 ; extra == 'server'
|
|
9
9
|
Requires-Dist: django-passkey-auth>=0.2.0 ; extra == 'server'
|
|
10
|
+
Requires-Dist: granian>=2.4.2 ; extra == 'server'
|
|
10
11
|
Requires-Dist: psycopg[binary]>=3.2.1 ; extra == 'server'
|
|
11
|
-
Requires-Dist: uvicorn>=0.30.6 ; extra == 'server'
|
|
12
12
|
Requires-Dist: websockets>=13.0 ; extra == 'server'
|
|
13
13
|
Requires-Dist: whitenoise>=6.7.0 ; extra == 'server'
|
|
14
|
-
Requires-Python: >=3.
|
|
14
|
+
Requires-Python: >=3.13
|
|
15
15
|
Provides-Extra: server
|
|
16
16
|
Description-Content-Type: text/markdown
|
|
17
17
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "varanus"
|
|
3
|
-
version = "0.1.0.
|
|
3
|
+
version = "0.1.0.dev1"
|
|
4
4
|
description = "Django application monitoring."
|
|
5
5
|
readme = "README.md"
|
|
6
|
-
requires-python = ">=3.
|
|
6
|
+
requires-python = ">=3.13"
|
|
7
7
|
dependencies = [
|
|
8
8
|
"httpx>=0.27.0",
|
|
9
9
|
"msgspec>=0.19.0",
|
|
@@ -14,8 +14,8 @@ server = [
|
|
|
14
14
|
"cconf>=1.0.0",
|
|
15
15
|
"django~=5.2.0",
|
|
16
16
|
"django-passkey-auth>=0.2.0",
|
|
17
|
+
"granian>=2.4.2",
|
|
17
18
|
"psycopg[binary]>=3.2.1",
|
|
18
|
-
"uvicorn>=0.30.6",
|
|
19
19
|
"websockets>=13.0",
|
|
20
20
|
"whitenoise>=6.7.0",
|
|
21
21
|
]
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import platform
|
|
2
|
+
from typing import Any
|
|
1
3
|
from urllib.parse import SplitResult
|
|
2
4
|
|
|
3
5
|
import httpx
|
|
@@ -17,11 +19,19 @@ class HttpTransport(BaseTransport):
|
|
|
17
19
|
headers={
|
|
18
20
|
"X-Varanus-Key": url.username or "",
|
|
19
21
|
"X-Varanus-Environment": environment or "",
|
|
20
|
-
|
|
22
|
+
"X-Varanus-Node": platform.node(),
|
|
23
|
+
},
|
|
24
|
+
timeout=1.0,
|
|
21
25
|
)
|
|
22
26
|
|
|
27
|
+
def request(self, url: str, obj: Any):
|
|
28
|
+
try:
|
|
29
|
+
self.client.post(url, content=msgspec.json.encode(obj))
|
|
30
|
+
except Exception as ex:
|
|
31
|
+
print(f"error sending to {url}: {ex}")
|
|
32
|
+
|
|
23
33
|
def ping(self, info: events.NodeInfo):
|
|
24
|
-
self.
|
|
34
|
+
self.request(self.ping_url, info)
|
|
25
35
|
|
|
26
36
|
def send(self, event: events.Context):
|
|
27
|
-
self.
|
|
37
|
+
self.request(self.event_url, event)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Generated by Django 5.2.8 on 2025-11-16 01:38
|
|
2
|
+
|
|
3
|
+
import django.db.models.deletion
|
|
4
|
+
from django.db import migrations, models
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Migration(migrations.Migration):
|
|
8
|
+
dependencies = [
|
|
9
|
+
("varanus", "0001_initial"),
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
operations = [
|
|
13
|
+
migrations.AddField(
|
|
14
|
+
model_name="context",
|
|
15
|
+
name="node",
|
|
16
|
+
field=models.ForeignKey(
|
|
17
|
+
blank=True,
|
|
18
|
+
null=True,
|
|
19
|
+
on_delete=django.db.models.deletion.SET_NULL,
|
|
20
|
+
related_name="%(class)ss",
|
|
21
|
+
to="varanus.node",
|
|
22
|
+
),
|
|
23
|
+
),
|
|
24
|
+
migrations.AddField(
|
|
25
|
+
model_name="error",
|
|
26
|
+
name="node",
|
|
27
|
+
field=models.ForeignKey(
|
|
28
|
+
blank=True,
|
|
29
|
+
null=True,
|
|
30
|
+
on_delete=django.db.models.deletion.SET_NULL,
|
|
31
|
+
related_name="%(class)ss",
|
|
32
|
+
to="varanus.node",
|
|
33
|
+
),
|
|
34
|
+
),
|
|
35
|
+
migrations.AddField(
|
|
36
|
+
model_name="log",
|
|
37
|
+
name="node",
|
|
38
|
+
field=models.ForeignKey(
|
|
39
|
+
blank=True,
|
|
40
|
+
null=True,
|
|
41
|
+
on_delete=django.db.models.deletion.SET_NULL,
|
|
42
|
+
related_name="%(class)ss",
|
|
43
|
+
to="varanus.node",
|
|
44
|
+
),
|
|
45
|
+
),
|
|
46
|
+
migrations.AddField(
|
|
47
|
+
model_name="metric",
|
|
48
|
+
name="node",
|
|
49
|
+
field=models.ForeignKey(
|
|
50
|
+
blank=True,
|
|
51
|
+
null=True,
|
|
52
|
+
on_delete=django.db.models.deletion.SET_NULL,
|
|
53
|
+
related_name="%(class)ss",
|
|
54
|
+
to="varanus.node",
|
|
55
|
+
),
|
|
56
|
+
),
|
|
57
|
+
migrations.AddField(
|
|
58
|
+
model_name="query",
|
|
59
|
+
name="node",
|
|
60
|
+
field=models.ForeignKey(
|
|
61
|
+
blank=True,
|
|
62
|
+
null=True,
|
|
63
|
+
on_delete=django.db.models.deletion.SET_NULL,
|
|
64
|
+
related_name="%(class)ss",
|
|
65
|
+
to="varanus.node",
|
|
66
|
+
),
|
|
67
|
+
),
|
|
68
|
+
migrations.AddField(
|
|
69
|
+
model_name="request",
|
|
70
|
+
name="node",
|
|
71
|
+
field=models.ForeignKey(
|
|
72
|
+
blank=True,
|
|
73
|
+
null=True,
|
|
74
|
+
on_delete=django.db.models.deletion.SET_NULL,
|
|
75
|
+
related_name="%(class)ss",
|
|
76
|
+
to="varanus.node",
|
|
77
|
+
),
|
|
78
|
+
),
|
|
79
|
+
]
|
|
@@ -7,6 +7,7 @@ from typing import Self
|
|
|
7
7
|
import msgspec
|
|
8
8
|
import sqlparse
|
|
9
9
|
from django.conf import settings
|
|
10
|
+
from django.contrib.contenttypes.models import ContentType
|
|
10
11
|
from django.core.management import call_command
|
|
11
12
|
from django.core.validators import RegexValidator
|
|
12
13
|
from django.db import connections, models
|
|
@@ -256,6 +257,13 @@ class EventModel(EnvironmentModel):
|
|
|
256
257
|
event_id = models.UUIDField(db_index=True)
|
|
257
258
|
timestamp = models.DateTimeField(default=timezone.now)
|
|
258
259
|
tags = models.JSONField(default=dict, blank=True)
|
|
260
|
+
node = models.ForeignKey(
|
|
261
|
+
Node,
|
|
262
|
+
on_delete=models.SET_NULL,
|
|
263
|
+
related_name="%(class)ss",
|
|
264
|
+
null=True,
|
|
265
|
+
blank=True,
|
|
266
|
+
)
|
|
259
267
|
|
|
260
268
|
class Meta:
|
|
261
269
|
abstract = True
|
|
@@ -265,6 +273,18 @@ class EventModel(EnvironmentModel):
|
|
|
265
273
|
def from_event(cls, event: events.Event, **extra):
|
|
266
274
|
raise NotImplementedError()
|
|
267
275
|
|
|
276
|
+
def get_absolute_url(self):
|
|
277
|
+
ct = ContentType.objects.get_for_model(self)
|
|
278
|
+
return reverse(
|
|
279
|
+
"event-details",
|
|
280
|
+
kwargs={
|
|
281
|
+
"slug": self.site.slug,
|
|
282
|
+
"event_id": self.event_id,
|
|
283
|
+
"model": ct.model,
|
|
284
|
+
"pk": self.pk,
|
|
285
|
+
},
|
|
286
|
+
)
|
|
287
|
+
|
|
268
288
|
|
|
269
289
|
class FingerprintModel(models.Model):
|
|
270
290
|
fingerprint = models.CharField(max_length=64, db_index=True)
|
|
@@ -529,6 +549,10 @@ class Query(ContextualModel, FingerprintModel):
|
|
|
529
549
|
return stripped
|
|
530
550
|
return stripped[:79] + "…"
|
|
531
551
|
|
|
552
|
+
@property
|
|
553
|
+
def sql_formatted(self):
|
|
554
|
+
return sqlparse.format(self.sql, reindent=True, keyword_case="upper")
|
|
555
|
+
|
|
532
556
|
def fingerprint_parts(self):
|
|
533
557
|
return self.sql.split()
|
|
534
558
|
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
from typing import ClassVar, Mapping, Self
|
|
2
|
+
|
|
3
|
+
from django.db.models import QuerySet
|
|
4
|
+
from django.http import HttpRequest
|
|
5
|
+
from django.template import loader
|
|
6
|
+
from django.utils.datastructures import MultiValueDict
|
|
7
|
+
|
|
8
|
+
type StringValues = dict[str, list[str]]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def string_data(data) -> StringValues:
|
|
12
|
+
if isinstance(data, MultiValueDict):
|
|
13
|
+
return {str(key): [str(v) for v in values] for key, values in data.lists()}
|
|
14
|
+
elif isinstance(data, Mapping):
|
|
15
|
+
return {
|
|
16
|
+
str(key): (
|
|
17
|
+
[str(v) for v in values]
|
|
18
|
+
if isinstance(values, (list, tuple))
|
|
19
|
+
else ([] if values is None else [str(values)])
|
|
20
|
+
)
|
|
21
|
+
for key, values in data.items()
|
|
22
|
+
}
|
|
23
|
+
return {}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class SearchField:
|
|
27
|
+
template_name: str
|
|
28
|
+
|
|
29
|
+
def __init__(self, label=None, field_name=None):
|
|
30
|
+
self.name = ""
|
|
31
|
+
self.label = label or ""
|
|
32
|
+
self.field_name = field_name or ""
|
|
33
|
+
self.prefix = ""
|
|
34
|
+
self.id = ""
|
|
35
|
+
|
|
36
|
+
def __set_name__(self, owner, name: str):
|
|
37
|
+
# print("__set_name__", owner, name)
|
|
38
|
+
assert issubclass(owner, Search)
|
|
39
|
+
self.name = name
|
|
40
|
+
self.prefix = f"{name}_"
|
|
41
|
+
self.id = f"id_{name}"
|
|
42
|
+
if not self.label:
|
|
43
|
+
self.label = name.capitalize()
|
|
44
|
+
if not self.field_name:
|
|
45
|
+
self.field_name = name
|
|
46
|
+
if not hasattr(owner, "_fields"):
|
|
47
|
+
owner._fields = []
|
|
48
|
+
owner._fields.append(self)
|
|
49
|
+
|
|
50
|
+
def __get__(self, instance: "Search", owner=None) -> StringValues | Self:
|
|
51
|
+
# print("__get__", instance, owner)
|
|
52
|
+
if owner is None:
|
|
53
|
+
return self
|
|
54
|
+
return {
|
|
55
|
+
key[len(self.prefix) :]: values
|
|
56
|
+
for key, values in instance._data.items()
|
|
57
|
+
if key.startswith(self.prefix)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
def __set__(self, instance, value):
|
|
61
|
+
print("__set__", instance, value)
|
|
62
|
+
|
|
63
|
+
def apply(self, queryset: QuerySet, field_data: StringValues) -> QuerySet:
|
|
64
|
+
raise NotImplementedError()
|
|
65
|
+
|
|
66
|
+
def get_context(self, queryset: QuerySet, field_data: StringValues) -> dict:
|
|
67
|
+
return {
|
|
68
|
+
"field": self,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
def render(
|
|
72
|
+
self,
|
|
73
|
+
queryset: QuerySet,
|
|
74
|
+
field_data: StringValues,
|
|
75
|
+
request: HttpRequest | None = None,
|
|
76
|
+
) -> str:
|
|
77
|
+
return loader.render_to_string(
|
|
78
|
+
self.template_name,
|
|
79
|
+
self.get_context(queryset, field_data),
|
|
80
|
+
request,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class Search:
|
|
85
|
+
_fields: ClassVar[list[SearchField]]
|
|
86
|
+
|
|
87
|
+
def __init__(self, queryset: QuerySet, data=None):
|
|
88
|
+
self._queryset = queryset
|
|
89
|
+
self._data = string_data(data)
|
|
90
|
+
|
|
91
|
+
def queryset(self, for_field=None):
|
|
92
|
+
qs = self._queryset
|
|
93
|
+
for field in self._fields:
|
|
94
|
+
if field == for_field:
|
|
95
|
+
continue
|
|
96
|
+
field_data = getattr(self, field.name)
|
|
97
|
+
qs = field.apply(qs, field_data)
|
|
98
|
+
return qs
|
|
99
|
+
|
|
100
|
+
def render(self, request: HttpRequest | None = None) -> str:
|
|
101
|
+
fragments = []
|
|
102
|
+
for field in self._fields:
|
|
103
|
+
qs = self.queryset(for_field=field)
|
|
104
|
+
field_data = getattr(self, field.name)
|
|
105
|
+
fragments.append(field.render(qs, field_data, request))
|
|
106
|
+
return "".join(fragments)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
|
|
3
|
+
from django.db.models import QuerySet
|
|
4
|
+
from django.utils.dateparse import parse_date
|
|
5
|
+
|
|
6
|
+
from .base import SearchField, StringValues
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def date_value(field_data: StringValues, name: str) -> datetime.date | None:
|
|
10
|
+
if name not in field_data:
|
|
11
|
+
return None
|
|
12
|
+
if not field_data[name]:
|
|
13
|
+
return None
|
|
14
|
+
return parse_date(field_data[name][0])
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DateRange(SearchField):
|
|
18
|
+
template_name = "search/daterange.html"
|
|
19
|
+
|
|
20
|
+
def apply(self, queryset: QuerySet, field_data: StringValues) -> QuerySet:
|
|
21
|
+
filters = {}
|
|
22
|
+
if start := date_value(field_data, "start"):
|
|
23
|
+
filters[f"{self.field_name}__date__gte"] = start
|
|
24
|
+
if end := date_value(field_data, "end"):
|
|
25
|
+
filters[f"{self.field_name}__date__lte"] = end
|
|
26
|
+
return queryset.filter(**filters)
|
|
27
|
+
|
|
28
|
+
def get_context(self, queryset: QuerySet, field_data: StringValues) -> dict:
|
|
29
|
+
return {
|
|
30
|
+
**super().get_context(queryset, field_data),
|
|
31
|
+
"start": date_value(field_data, "start"),
|
|
32
|
+
"end": date_value(field_data, "end"),
|
|
33
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from typing import Any, ClassVar
|
|
2
|
+
|
|
3
|
+
from django.db.models import Count, QuerySet
|
|
4
|
+
|
|
5
|
+
from .base import SearchField, StringValues
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Choice:
|
|
9
|
+
id: str
|
|
10
|
+
value: str
|
|
11
|
+
label: str
|
|
12
|
+
count: int
|
|
13
|
+
|
|
14
|
+
def __init__(self, field: SearchField, value: Any):
|
|
15
|
+
self.value = self.stringify(value)
|
|
16
|
+
self.label = self.value
|
|
17
|
+
self.id = field.id + "_" + self.value.replace(" ", "")
|
|
18
|
+
self.count = 0
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def stringify(cls, value):
|
|
22
|
+
return str(value)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Facet(SearchField):
|
|
26
|
+
choice_class: ClassVar[type[Choice]] = Choice
|
|
27
|
+
|
|
28
|
+
def get_choices(self, queryset, with_counts=True):
|
|
29
|
+
choices = [
|
|
30
|
+
self.choice_class(self, c)
|
|
31
|
+
for c in queryset.order_by(self.field_name)
|
|
32
|
+
.values_list(self.field_name, flat=True)
|
|
33
|
+
.distinct(self.field_name)
|
|
34
|
+
]
|
|
35
|
+
if with_counts:
|
|
36
|
+
counts = {
|
|
37
|
+
self.choice_class.stringify(r[self.field_name]): r["num"]
|
|
38
|
+
for r in queryset.values(self.field_name).annotate(num=Count("*"))
|
|
39
|
+
}
|
|
40
|
+
for c in choices:
|
|
41
|
+
c.count = counts.get(c.value, 0)
|
|
42
|
+
return choices
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class MultiFacet(Facet):
|
|
46
|
+
template_name = "search/multifacet.html"
|
|
47
|
+
|
|
48
|
+
def apply(self, queryset: QuerySet, field_data: StringValues) -> QuerySet:
|
|
49
|
+
filters = {}
|
|
50
|
+
if selected := field_data.get("s", []):
|
|
51
|
+
filters[f"{self.field_name}__in"] = selected
|
|
52
|
+
return queryset.filter(**filters)
|
|
53
|
+
|
|
54
|
+
def get_context(self, queryset: QuerySet, field_data: StringValues) -> dict:
|
|
55
|
+
return {
|
|
56
|
+
**super().get_context(queryset, field_data),
|
|
57
|
+
"choices": self.get_choices(queryset),
|
|
58
|
+
"selected": field_data.get("s", []),
|
|
59
|
+
}
|
|
@@ -48,9 +48,9 @@ INSTALLED_APPS = [
|
|
|
48
48
|
"django.contrib.contenttypes",
|
|
49
49
|
"django.contrib.sessions",
|
|
50
50
|
"django.contrib.messages",
|
|
51
|
-
|
|
52
|
-
"varanus.server.apps.VaranusServer",
|
|
51
|
+
"whitenoise.runserver_nostatic",
|
|
53
52
|
"django.contrib.staticfiles",
|
|
53
|
+
"varanus.server.apps.VaranusServer",
|
|
54
54
|
"cconf",
|
|
55
55
|
"passkeys",
|
|
56
56
|
]
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
{% load i18n %}
|
|
3
3
|
|
|
4
4
|
<!doctype html>
|
|
5
|
-
<html lang="en" data-bs-theme="
|
|
5
|
+
<html lang="en" data-bs-theme="dark">
|
|
6
6
|
<head>
|
|
7
7
|
<meta charset="utf-8" />
|
|
8
8
|
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
|
@@ -68,6 +68,8 @@
|
|
|
68
68
|
</footer>
|
|
69
69
|
|
|
70
70
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5/dist/js/bootstrap.bundle.min.js"></script>
|
|
71
|
+
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2/dist/htmx.min.js"></script>
|
|
72
|
+
|
|
71
73
|
<script src="{% static 'js/varanus.js' %}"></script>
|
|
72
74
|
|
|
73
75
|
{% block javascript %}{% endblock javascript %}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<div class="card mb-3">
|
|
2
|
+
<div class="card-header">{{ field.label }}</div>
|
|
3
|
+
<div class="card-body">
|
|
4
|
+
<div class="input-group">
|
|
5
|
+
<input type="date" class="form-control" placeholder="From" name="{{ field.prefix }}start" value="{{ start.isoformat|default:'' }}" />
|
|
6
|
+
<span class="input-group-text">-</span>
|
|
7
|
+
<input type="date" class="form-control" placeholder="To" name="{{ field.prefix }}end" value="{{ end.isoformat|default:'' }}" />
|
|
8
|
+
</div>
|
|
9
|
+
</div>
|
|
10
|
+
</div>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<div class="card mb-3">
|
|
2
|
+
<div class="card-header">{{ field.label }}</div>
|
|
3
|
+
<div class="card-body">
|
|
4
|
+
{% for c in choices %}
|
|
5
|
+
<div class="form-check">
|
|
6
|
+
<input type="checkbox" id="{{ c.id }}" class="form-check-input" name="{{ field.prefix }}s" value="{{ c.value }}"{% if c.value in selected %} checked{% endif %} />
|
|
7
|
+
<label for="{{ c.id }}">{{ c.label }} ({{ c.count }})</label>
|
|
8
|
+
</div>
|
|
9
|
+
{% endfor %}
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
@@ -25,21 +25,28 @@
|
|
|
25
25
|
</li>
|
|
26
26
|
</ul>
|
|
27
27
|
|
|
28
|
-
<!--
|
|
29
28
|
<div class="container-fluid">
|
|
30
|
-
<
|
|
31
|
-
|
|
32
|
-
{%
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
29
|
+
<div class="row">
|
|
30
|
+
<div class="col-md-3">
|
|
31
|
+
{% block search %}
|
|
32
|
+
<form action="" method="get">
|
|
33
|
+
{{ search.render|safe }}
|
|
34
|
+
<button class="btn btn-primary">Filter</button>
|
|
35
|
+
</form>
|
|
36
|
+
{% endblock search %}
|
|
37
|
+
</div>
|
|
38
|
+
<div class="col-md-9">
|
|
39
|
+
{% block page %}{% endblock page %}
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
38
42
|
</div>
|
|
39
|
-
-->
|
|
40
43
|
|
|
41
|
-
<div class="
|
|
42
|
-
|
|
44
|
+
<div class="offcanvas offcanvas-end" style="width:33%" tabindex="-1" id="inspector">
|
|
45
|
+
<div class="offcanvas-header">
|
|
46
|
+
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
|
|
47
|
+
</div>
|
|
48
|
+
<div class="offcanvas-body" id="inspector-content">
|
|
49
|
+
</div>
|
|
43
50
|
</div>
|
|
44
51
|
{% endblock content %}
|
|
45
52
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{{ object }}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{{ object }}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{{ object }}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<code><pre>{{ object.sql_formatted }}</pre></code>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{% load i18n %}
|
|
2
|
+
|
|
3
|
+
<table class="table table-sm">
|
|
4
|
+
<tbody>
|
|
5
|
+
<tr>
|
|
6
|
+
<th>{% trans "Host" %}</th>
|
|
7
|
+
<td>{{ object.host }}</td>
|
|
8
|
+
</tr>
|
|
9
|
+
<tr>
|
|
10
|
+
<th>{% trans "Node" %}</th>
|
|
11
|
+
<td>{{ object.node }}</td>
|
|
12
|
+
</tr>
|
|
13
|
+
<tr>
|
|
14
|
+
<th>{% trans "Request" %}</th>
|
|
15
|
+
<td>{{ object.method }} {{ object.path }}</td>
|
|
16
|
+
</tr>
|
|
17
|
+
<tr>
|
|
18
|
+
<th>{% trans "Status" %}</th>
|
|
19
|
+
<td>{{ object.http_status }}</td>
|
|
20
|
+
</tr>
|
|
21
|
+
<tr>
|
|
22
|
+
<th>{% trans "IP" %}</th>
|
|
23
|
+
<td>{{ object.ip }}</td>
|
|
24
|
+
</tr>
|
|
25
|
+
<tr>
|
|
26
|
+
<th>{% trans "User" %}</th>
|
|
27
|
+
<td>{{ object.user }}</td>
|
|
28
|
+
</tr>
|
|
29
|
+
</tbody>
|
|
30
|
+
</table>
|
|
31
|
+
|
|
32
|
+
{% if object.headers %}
|
|
33
|
+
<h5>{% trans "Headers" %}</h5>
|
|
34
|
+
<table class="table table-sm">
|
|
35
|
+
<tbody>
|
|
36
|
+
{% for name, value in object.headers.items %}
|
|
37
|
+
<tr>
|
|
38
|
+
<th>{{ name }}</th>
|
|
39
|
+
<td>{{ value }}</th>
|
|
40
|
+
</tr>
|
|
41
|
+
{% endfor %}
|
|
42
|
+
</tbody>
|
|
43
|
+
</table>
|
|
44
|
+
{% endif %}
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
{% load i18n %}
|
|
4
4
|
|
|
5
5
|
{% block page %}
|
|
6
|
-
<table class="table">
|
|
7
|
-
<thead>
|
|
6
|
+
<table class="table table-bordered">
|
|
7
|
+
<thead class="table-header">
|
|
8
8
|
<tr>
|
|
9
9
|
<th>{% trans "Context" %}</th>
|
|
10
10
|
<th>{% trans "Kind" %}</th>
|
|
@@ -14,9 +14,11 @@
|
|
|
14
14
|
</tr>
|
|
15
15
|
</thead>
|
|
16
16
|
<tbody>
|
|
17
|
-
{% for e in
|
|
17
|
+
{% for e in results %}
|
|
18
18
|
<tr>
|
|
19
|
-
<td>
|
|
19
|
+
<td>
|
|
20
|
+
<a href="{{ e.get_absolute_url }}" data-bs-toggle="offcanvas" data-bs-target="#inspector" hx-get="{{ e.get_absolute_url }}" hx-target="#inspector-content">{{ e.context }}</a>
|
|
21
|
+
</td>
|
|
20
22
|
<td>{{ e.kind }}</td>
|
|
21
23
|
<td>{{ e.message }}</td>
|
|
22
24
|
<td>{{ e.module }}</td>
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
{% load i18n %}
|
|
4
4
|
|
|
5
5
|
{% block page %}
|
|
6
|
-
<table class="table">
|
|
7
|
-
<thead>
|
|
6
|
+
<table class="table table-bordered">
|
|
7
|
+
<thead class="table-header">
|
|
8
8
|
<tr>
|
|
9
9
|
<th>{% trans "Context" %}</th>
|
|
10
10
|
<th>{% trans "Level" %}</th>
|
|
@@ -15,9 +15,11 @@
|
|
|
15
15
|
</tr>
|
|
16
16
|
</thead>
|
|
17
17
|
<tbody>
|
|
18
|
-
{% for log in
|
|
18
|
+
{% for log in results %}
|
|
19
19
|
<tr>
|
|
20
|
-
<td>
|
|
20
|
+
<td>
|
|
21
|
+
<a href="{{ log.get_absolute_url }}" data-bs-toggle="offcanvas" data-bs-target="#inspector" hx-get="{{ log.get_absolute_url }}" hx-target="#inspector-content">{{ log.context }}</a>
|
|
22
|
+
</td>
|
|
21
23
|
<td>{{ log.level_name }}</td>
|
|
22
24
|
<td>{{ log.message }}</td>
|
|
23
25
|
<td>{{ log.name }}</td>
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
{% load i18n %}
|
|
4
4
|
|
|
5
5
|
{% block page %}
|
|
6
|
-
<table class="table">
|
|
7
|
-
<thead>
|
|
6
|
+
<table class="table table-bordered">
|
|
7
|
+
<thead class="table-header">
|
|
8
8
|
<tr>
|
|
9
9
|
<th>{% trans "Context" %}</th>
|
|
10
10
|
<th>{% trans "Name" %}</th>
|
|
@@ -17,9 +17,11 @@
|
|
|
17
17
|
</tr>
|
|
18
18
|
</thead>
|
|
19
19
|
<tbody>
|
|
20
|
-
{% for m in
|
|
20
|
+
{% for m in results %}
|
|
21
21
|
<tr>
|
|
22
|
-
<td>
|
|
22
|
+
<td>
|
|
23
|
+
<a href="{{ m.get_absolute_url }}" data-bs-toggle="offcanvas" data-bs-target="#inspector" hx-get="{{ m.get_absolute_url }}" hx-target="#inspector-content">{{ m.context }}</a>
|
|
24
|
+
</td>
|
|
23
25
|
<td>{{ m.name }}</td>
|
|
24
26
|
<td>{{ m.agg_count }}</td>
|
|
25
27
|
<td>{{ m.agg_sum }}</td>
|