varanus 0.1.0.dev4__tar.gz → 0.1.0.dev6__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.dev4 → varanus-0.1.0.dev6}/PKG-INFO +3 -4
- {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/pyproject.toml +3 -4
- {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/client/client.py +19 -4
- {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/client/loggers.py +15 -16
- {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/client/middleware.py +9 -1
- {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/client/transport/database.py +10 -10
- {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/events.py +4 -2
- {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/admin.py +18 -4
- {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/apps.py +3 -0
- varanus-0.1.0.dev6/src/varanus/server/integrations/__init__.py +3 -0
- varanus-0.1.0.dev6/src/varanus/server/integrations/base.py +38 -0
- varanus-0.1.0.dev6/src/varanus/server/integrations/squish.py +74 -0
- varanus-0.1.0.dev6/src/varanus/server/management/commands/serve.py +64 -0
- {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/migrations/0001_initial.py +90 -1
- {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/models.py +89 -7
- {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/router.py +1 -0
- {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/settings.py +33 -7
- varanus-0.1.0.dev6/src/varanus/server/tasks.py +64 -0
- varanus-0.1.0.dev6/src/varanus/server/templates/dashboard.html +41 -0
- {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/templates/site/details/error.html +9 -11
- {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/templates/site/details/log.html +3 -5
- {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/templates/site/details/metric.html +3 -5
- {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/templates/site/details/query.html +5 -7
- {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/urls.py +10 -0
- {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/views/api.py +14 -24
- {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/views/dashboard.py +3 -8
- varanus-0.1.0.dev6/src/varanus/tasks/__init__.py +0 -0
- varanus-0.1.0.dev6/src/varanus/tasks/admin.py +15 -0
- varanus-0.1.0.dev6/src/varanus/tasks/apps.py +0 -0
- varanus-0.1.0.dev6/src/varanus/tasks/backend.py +62 -0
- 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 +20 -0
- varanus-0.1.0.dev6/src/varanus/tasks/migrations/0001_initial.py +66 -0
- varanus-0.1.0.dev6/src/varanus/tasks/migrations/__init__.py +0 -0
- varanus-0.1.0.dev6/src/varanus/tasks/models.py +131 -0
- varanus-0.1.0.dev6/src/varanus/tasks/runner.py +107 -0
- varanus-0.1.0.dev6/src/varanus/utils.py +28 -0
- varanus-0.1.0.dev4/src/varanus/server/templates/dashboard.html +0 -11
- varanus-0.1.0.dev4/src/varanus/utils.py +0 -10
- {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/README.md +0 -0
- {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/__init__.py +0 -0
- {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/client/__init__.py +0 -0
- {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/client/apps.py +0 -0
- {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/client/context.py +0 -0
- {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/client/transport/__init__.py +0 -0
- {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/client/transport/base.py +0 -0
- {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/client/transport/http.py +0 -0
- {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/search/__init__.py +0 -0
- {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/search/base.py +0 -0
- {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/search/fields.py +0 -0
- {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/search/templates/search/daterange.html +0 -0
- {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/search/templates/search/filter.html +0 -0
- {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/search/templates/search/hidden.html +0 -0
- {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/search/templates/search/multifacet.html +0 -0
- {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/search/templates/search/search.html +0 -0
- {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/search/utils.py +0 -0
- {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/__init__.py +0 -0
- {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/__main__.py +0 -0
- {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/asgi.py +0 -0
- {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/context_processors.py +0 -0
- {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/management/__init__.py +0 -0
- {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/management/commands/__init__.py +0 -0
- {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/management/commands/migrateall.py +0 -0
- {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/middleware.py +0 -0
- {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/migrations/__init__.py +0 -0
- {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/static/css/varanus.css +0 -0
- {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/static/js/varanus.js +0 -0
- {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/templates/base.html +0 -0
- {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/templates/registration/login.html +0 -0
- {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/templates/site/base.html +0 -0
- {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/templates/site/details/environment_nodes.html +0 -0
- {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/templates/site/details/node_env.html +0 -0
- {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/templates/site/details/node_environments.html +0 -0
- {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/templates/site/details/node_packages.html +0 -0
- {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/templates/site/details/node_settings.html +0 -0
- {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/templates/site/details/request.html +0 -0
- {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/templates/site/errors.html +0 -0
- {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/templates/site/logs.html +0 -0
- {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/templates/site/metrics.html +0 -0
- {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/templates/site/overview.html +0 -0
- {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/templates/site/queries.html +0 -0
- {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/templates/site/requests.html +0 -0
- {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/templatetags/__init__.py +0 -0
- {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/templatetags/varanus.py +0 -0
- {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/utils.py +0 -0
- {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/views/__init__.py +0 -0
- {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/views/base.py +0 -0
- {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/views/site.py +0 -0
- {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/wsgi.py +0 -0
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: varanus
|
|
3
|
-
Version: 0.1.0.
|
|
3
|
+
Version: 0.1.0.dev6
|
|
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
|
-
Requires-Dist: django
|
|
8
|
+
Requires-Dist: django>=6.0 ; python_full_version >= '3.12' and extra == 'server'
|
|
9
9
|
Requires-Dist: django-passkey-auth>=0.2.0 ; extra == 'server'
|
|
10
|
-
Requires-Dist: granian>=2.4.2 ; extra == 'server'
|
|
10
|
+
Requires-Dist: granian[reload]>=2.4.2 ; extra == 'server'
|
|
11
11
|
Requires-Dist: psycopg[binary]>=3.2.1 ; extra == 'server'
|
|
12
|
-
Requires-Dist: websockets>=13.0 ; extra == 'server'
|
|
13
12
|
Requires-Dist: whitenoise>=6.7.0 ; extra == 'server'
|
|
14
13
|
Requires-Python: >=3.11
|
|
15
14
|
Provides-Extra: server
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "varanus"
|
|
3
|
-
version = "0.1.0.
|
|
3
|
+
version = "0.1.0.dev6"
|
|
4
4
|
description = "Django application monitoring."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -12,11 +12,10 @@ dependencies = [
|
|
|
12
12
|
[project.optional-dependencies]
|
|
13
13
|
server = [
|
|
14
14
|
"cconf>=1.0.0",
|
|
15
|
-
"django
|
|
15
|
+
"django>=6.0; python_version>='3.12'",
|
|
16
16
|
"django-passkey-auth>=0.2.0",
|
|
17
|
-
"granian>=2.4.2",
|
|
17
|
+
"granian[reload]>=2.4.2",
|
|
18
18
|
"psycopg[binary]>=3.2.1",
|
|
19
|
-
"websockets>=13.0",
|
|
20
19
|
"whitenoise>=6.7.0",
|
|
21
20
|
]
|
|
22
21
|
|
|
@@ -40,8 +40,9 @@ def resolve_include_exclude(
|
|
|
40
40
|
|
|
41
41
|
|
|
42
42
|
class VaranusClient:
|
|
43
|
+
# Things we don't want to send, at least by default.
|
|
43
44
|
sensitive_headers = set(["authorization", "cookie", "proxy-authorization"])
|
|
44
|
-
sensitive_settings = set(["
|
|
45
|
+
sensitive_settings = set(["SECRET_KEY"])
|
|
45
46
|
sensitive_env = set(["PGPASSWORD"])
|
|
46
47
|
|
|
47
48
|
scheme_transports = {
|
|
@@ -63,9 +64,10 @@ class VaranusClient:
|
|
|
63
64
|
tags: dict | None = None,
|
|
64
65
|
include_headers: Iterable[str] | bool = False,
|
|
65
66
|
exclude_headers: Iterable[str] | None = None,
|
|
66
|
-
include_settings: Iterable[str] | bool =
|
|
67
|
+
include_settings: Iterable[str] | bool = True,
|
|
67
68
|
exclude_settings: Iterable[str] | None = None,
|
|
68
69
|
include_default_settings: bool = False,
|
|
70
|
+
filter_settings: bool = True,
|
|
69
71
|
include_env: Iterable[str] | bool = False,
|
|
70
72
|
exclude_env: Iterable[str] | None = None,
|
|
71
73
|
log_queries: bool | int = False,
|
|
@@ -106,6 +108,7 @@ class VaranusClient:
|
|
|
106
108
|
else set(exclude_settings)
|
|
107
109
|
)
|
|
108
110
|
self.include_default_settings = include_default_settings
|
|
111
|
+
self.filter_settings = filter_settings
|
|
109
112
|
self.include_env = include_env
|
|
110
113
|
self.exclude_env = (
|
|
111
114
|
self.sensitive_env if exclude_env is None else set(exclude_env)
|
|
@@ -152,8 +155,20 @@ class VaranusClient:
|
|
|
152
155
|
from django import get_version
|
|
153
156
|
from django.conf import settings
|
|
154
157
|
|
|
158
|
+
if self.filter_settings:
|
|
159
|
+
from django.views.debug import SafeExceptionReporterFilter
|
|
160
|
+
|
|
161
|
+
settings_dict = {
|
|
162
|
+
k: repr(v)
|
|
163
|
+
for k, v in SafeExceptionReporterFilter().get_safe_settings().items()
|
|
164
|
+
}
|
|
165
|
+
else:
|
|
166
|
+
settings_dict = {
|
|
167
|
+
s: repr(getattr(settings, s)) for s in dir(settings) if s.isupper()
|
|
168
|
+
}
|
|
169
|
+
|
|
155
170
|
include_settings = resolve_include_exclude(
|
|
156
|
-
|
|
171
|
+
settings_dict.keys(),
|
|
157
172
|
self.include_settings,
|
|
158
173
|
self.exclude_settings,
|
|
159
174
|
)
|
|
@@ -173,7 +188,7 @@ class VaranusClient:
|
|
|
173
188
|
framework_version=get_version(),
|
|
174
189
|
packages={d.name: d.version for d in distributions()},
|
|
175
190
|
settings={
|
|
176
|
-
s:
|
|
191
|
+
s: settings_dict[s]
|
|
177
192
|
for s in include_settings
|
|
178
193
|
if self.include_default_settings or settings.is_overridden(s)
|
|
179
194
|
},
|
|
@@ -42,20 +42,19 @@ class QueryLogger:
|
|
|
42
42
|
elapsed_ms = (now() - start) // ONE_MS
|
|
43
43
|
if self.metrics_name:
|
|
44
44
|
ctx.metric(self.metrics_name, elapsed_ms)
|
|
45
|
-
if elapsed_ms
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
45
|
+
if elapsed_ms >= self.threshold:
|
|
46
|
+
ctx.queries.append(
|
|
47
|
+
Query(
|
|
48
|
+
timestamp=start,
|
|
49
|
+
sql=sql,
|
|
50
|
+
params=(
|
|
51
|
+
[repr(p) for p in params]
|
|
52
|
+
if params and self.log_params
|
|
53
|
+
else []
|
|
54
|
+
),
|
|
55
|
+
db=context["connection"].alias,
|
|
56
|
+
elapsed_ms=elapsed_ms,
|
|
57
|
+
success=success,
|
|
58
|
+
stack=capture_stack(1) if self.log_stack else [],
|
|
59
|
+
)
|
|
60
60
|
)
|
|
61
|
-
)
|
|
@@ -2,6 +2,7 @@ import ipaddress
|
|
|
2
2
|
|
|
3
3
|
from django.core.exceptions import MiddlewareNotUsed
|
|
4
4
|
from django.http import HttpRequest, HttpResponse
|
|
5
|
+
from django.utils import timezone
|
|
5
6
|
|
|
6
7
|
from ..events import Request
|
|
7
8
|
from .client import client, resolve_include_exclude
|
|
@@ -40,7 +41,7 @@ class VaranusMiddleware:
|
|
|
40
41
|
# TODO: warning
|
|
41
42
|
print("VaranusClient is not configured -- disabling middleware.")
|
|
42
43
|
raise MiddlewareNotUsed()
|
|
43
|
-
|
|
44
|
+
self.last_ping = None
|
|
44
45
|
self.get_response = get_response
|
|
45
46
|
|
|
46
47
|
def process_exception(self, request, exception):
|
|
@@ -48,6 +49,13 @@ class VaranusMiddleware:
|
|
|
48
49
|
client.raw_exception(exception)
|
|
49
50
|
|
|
50
51
|
def __call__(self, request: HttpRequest):
|
|
52
|
+
if self.last_ping is None:
|
|
53
|
+
self.last_ping = timezone.now()
|
|
54
|
+
try:
|
|
55
|
+
client.ping()
|
|
56
|
+
except Exception:
|
|
57
|
+
pass
|
|
58
|
+
|
|
51
59
|
with client.context(request.path) as varanus:
|
|
52
60
|
setattr(request, client.request_attr, varanus)
|
|
53
61
|
response = self.get_response(request)
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import typing
|
|
2
|
-
import uuid
|
|
3
2
|
from urllib.parse import SplitResult
|
|
4
3
|
|
|
4
|
+
import msgspec
|
|
5
|
+
|
|
5
6
|
from varanus import events
|
|
6
7
|
|
|
7
8
|
from .base import BaseTransport
|
|
@@ -17,6 +18,7 @@ class ModelTransport(BaseTransport):
|
|
|
17
18
|
def __init__(self, url: SplitResult, environment: str, node: str):
|
|
18
19
|
self.slug = url.netloc
|
|
19
20
|
self.environment = environment
|
|
21
|
+
self.node_name = node
|
|
20
22
|
|
|
21
23
|
def ensure_site(self):
|
|
22
24
|
if hasattr(self, "site"):
|
|
@@ -43,13 +45,11 @@ class ModelTransport(BaseTransport):
|
|
|
43
45
|
)
|
|
44
46
|
|
|
45
47
|
def send(self, event: events.Context):
|
|
46
|
-
from varanus.server.
|
|
48
|
+
from varanus.server.tasks import ingest
|
|
47
49
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
node=self.node,
|
|
55
|
-
)
|
|
50
|
+
ingest.enqueue(
|
|
51
|
+
self.site.pk,
|
|
52
|
+
self.node_name,
|
|
53
|
+
self.environment,
|
|
54
|
+
msgspec.json.encode(event).decode(),
|
|
55
|
+
)
|
|
@@ -4,6 +4,8 @@ from datetime import datetime, timezone
|
|
|
4
4
|
|
|
5
5
|
from msgspec import Struct, field
|
|
6
6
|
|
|
7
|
+
from .utils import safe_repr
|
|
8
|
+
|
|
7
9
|
|
|
8
10
|
def now():
|
|
9
11
|
return datetime.now(tz=timezone.utc)
|
|
@@ -46,7 +48,7 @@ def capture_stack(skip: int = 0, include_locals: bool = False) -> list[StackLine
|
|
|
46
48
|
function=frame.function,
|
|
47
49
|
module=frame.frame.f_globals.get("__name__", ""),
|
|
48
50
|
locals=(
|
|
49
|
-
{name:
|
|
51
|
+
{name: safe_repr(val) for name, val in frame.frame.f_locals.items()}
|
|
50
52
|
if include_locals
|
|
51
53
|
else {}
|
|
52
54
|
),
|
|
@@ -94,7 +96,7 @@ class Error(Event):
|
|
|
94
96
|
function=function,
|
|
95
97
|
module=module,
|
|
96
98
|
linesrc=linesrc,
|
|
97
|
-
locals={name:
|
|
99
|
+
locals={name: safe_repr(val) for name, val in f_locals.items()},
|
|
98
100
|
)
|
|
99
101
|
)
|
|
100
102
|
tb = tb.tb_next
|
|
@@ -4,6 +4,7 @@ from django.contrib import admin
|
|
|
4
4
|
|
|
5
5
|
from .models import (
|
|
6
6
|
Context,
|
|
7
|
+
ContextIntegration,
|
|
7
8
|
Error,
|
|
8
9
|
Log,
|
|
9
10
|
Metric,
|
|
@@ -13,6 +14,7 @@ from .models import (
|
|
|
13
14
|
Query,
|
|
14
15
|
Request,
|
|
15
16
|
Site,
|
|
17
|
+
SiteIntegration,
|
|
16
18
|
SiteKey,
|
|
17
19
|
SiteMember,
|
|
18
20
|
)
|
|
@@ -34,9 +36,14 @@ class SiteKeyInline(admin.TabularInline):
|
|
|
34
36
|
extra = 0
|
|
35
37
|
|
|
36
38
|
|
|
39
|
+
class SiteIntegrationInline(admin.StackedInline):
|
|
40
|
+
model = SiteIntegration
|
|
41
|
+
extra = 0
|
|
42
|
+
|
|
43
|
+
|
|
37
44
|
class SiteAdmin(admin.ModelAdmin):
|
|
38
45
|
list_display = ["name", "slug", "schema_name"]
|
|
39
|
-
inlines = [SiteMemberInline, SiteKeyInline]
|
|
46
|
+
inlines = [SiteMemberInline, SiteKeyInline, SiteIntegrationInline]
|
|
40
47
|
prepopulated_fields = {"slug": ["name"], "schema_name": ["name"]}
|
|
41
48
|
fieldsets = [
|
|
42
49
|
(
|
|
@@ -160,15 +167,22 @@ class NodeUpdateAdmin(admin.ModelAdmin):
|
|
|
160
167
|
list_filter = ["node", "node__site", "node__environment"]
|
|
161
168
|
|
|
162
169
|
|
|
170
|
+
class ContextIntegrationAdmin(admin.ModelAdmin):
|
|
171
|
+
list_display = ["context", "context__site", "integration", "run_date"]
|
|
172
|
+
list_filter = ["context__site", "integration"]
|
|
173
|
+
raw_id_fields = ["context"]
|
|
174
|
+
|
|
175
|
+
|
|
163
176
|
admin.site.register(Site, SiteAdmin)
|
|
177
|
+
admin.site.register(Node, NodeAdmin)
|
|
178
|
+
admin.site.register(NodePackage, NodePackageAdmin)
|
|
179
|
+
admin.site.register(NodeUpdate, NodeUpdateAdmin)
|
|
164
180
|
|
|
165
181
|
if not settings.VARANUS_USE_SCHEMAS:
|
|
166
|
-
admin.site.register(Node, NodeAdmin)
|
|
167
|
-
admin.site.register(NodePackage, NodePackageAdmin)
|
|
168
|
-
admin.site.register(NodeUpdate, NodeUpdateAdmin)
|
|
169
182
|
admin.site.register(Request, RequestAdmin)
|
|
170
183
|
admin.site.register(Log, LogAdmin)
|
|
171
184
|
admin.site.register(Error, ErrorAdmin)
|
|
172
185
|
admin.site.register(Context, ContextAdmin)
|
|
173
186
|
admin.site.register(Metric, MetricAdmin)
|
|
174
187
|
admin.site.register(Query, QueryAdmin)
|
|
188
|
+
admin.site.register(ContextIntegration, ContextIntegrationAdmin)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING, Any
|
|
2
|
+
|
|
3
|
+
from varanus import events
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from ..models import Context
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class IntegrationError(Exception):
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DuplicateIntegration(IntegrationError):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Integration:
|
|
18
|
+
def __init__(self, settings: dict[str, Any]):
|
|
19
|
+
self.settings = settings
|
|
20
|
+
|
|
21
|
+
def is_valid(self, context: events.Context) -> bool:
|
|
22
|
+
"""
|
|
23
|
+
Returns whether the integration should be scheduled to run for the given
|
|
24
|
+
Context event (not the Context model).
|
|
25
|
+
"""
|
|
26
|
+
return True
|
|
27
|
+
|
|
28
|
+
def fingerprint(self, context: "Context") -> str | None:
|
|
29
|
+
"""
|
|
30
|
+
Given a Context model, returns a fingerprint for debouncing integration calls.
|
|
31
|
+
"""
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
def execute(self, context: "Context") -> Any:
|
|
35
|
+
"""
|
|
36
|
+
Runs the integration for the given Context model.
|
|
37
|
+
"""
|
|
38
|
+
raise NotImplementedError()
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import itertools
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
import httpx
|
|
5
|
+
|
|
6
|
+
from varanus import events
|
|
7
|
+
from varanus.utils import make_fingerprint
|
|
8
|
+
|
|
9
|
+
from ..models import Context
|
|
10
|
+
from .base import Integration
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SquishIntegration(Integration):
|
|
16
|
+
def is_valid(self, context: events.Context) -> bool:
|
|
17
|
+
return (
|
|
18
|
+
(len(context.errors) > 0)
|
|
19
|
+
and bool(self.settings.get("endpoint"))
|
|
20
|
+
and bool(self.settings.get("api_key"))
|
|
21
|
+
and bool(self.settings.get("user_key"))
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
def fingerprint(self, context: "Context") -> str | None:
|
|
25
|
+
return make_fingerprint(
|
|
26
|
+
itertools.chain.from_iterable(
|
|
27
|
+
err.fingerprint_parts() for err in context.errors.all()
|
|
28
|
+
)
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
def execute(self, context: Context):
|
|
32
|
+
lines = []
|
|
33
|
+
|
|
34
|
+
for err in context.errors.all():
|
|
35
|
+
lines.append(f"### {err.kind} in {err.module}")
|
|
36
|
+
lines.append(f"> {err.message}")
|
|
37
|
+
lines.append("```")
|
|
38
|
+
for line in err.stack:
|
|
39
|
+
lines.append(
|
|
40
|
+
"{module}.{func} - {file}:{lineno}".format(
|
|
41
|
+
module=line["module"],
|
|
42
|
+
func=line["function"],
|
|
43
|
+
file=line["file"],
|
|
44
|
+
lineno=line["lineno"],
|
|
45
|
+
)
|
|
46
|
+
)
|
|
47
|
+
lines.append(" " + line["linesrc"])
|
|
48
|
+
lines.append("```")
|
|
49
|
+
|
|
50
|
+
for idx, log in enumerate(context.logs.all()):
|
|
51
|
+
if idx == 0:
|
|
52
|
+
lines.append("### Request Logs")
|
|
53
|
+
lines.append(
|
|
54
|
+
"* `{level} - {name}:{lineno}`: *{msg}*".format(
|
|
55
|
+
level=log.get_level_display(), # type: ignore
|
|
56
|
+
name=log.name,
|
|
57
|
+
lineno=log.lineno,
|
|
58
|
+
msg=log.message,
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
subject = f"{context.site.name} ({context.environment}) - {context.name}"
|
|
63
|
+
return httpx.post(
|
|
64
|
+
self.settings["endpoint"],
|
|
65
|
+
json={
|
|
66
|
+
"issue_type": self.settings.get("issue_type", "bug"),
|
|
67
|
+
"subject": subject,
|
|
68
|
+
"description": {"comment": "\n".join(lines), "format": "markdown"},
|
|
69
|
+
},
|
|
70
|
+
headers={
|
|
71
|
+
"X-Squish-API-Key": self.settings["api_key"],
|
|
72
|
+
"X-Squish-User-Key": self.settings["user_key"],
|
|
73
|
+
},
|
|
74
|
+
).json()
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import threading
|
|
3
|
+
|
|
4
|
+
from django.conf import settings
|
|
5
|
+
from django.core.management import BaseCommand
|
|
6
|
+
from granian import Granian
|
|
7
|
+
from granian.constants import Interfaces
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def cpus() -> int:
|
|
11
|
+
return os.cpu_count() or 4
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Command(BaseCommand):
|
|
15
|
+
help = "Web server and task runner."
|
|
16
|
+
|
|
17
|
+
def add_arguments(self, parser):
|
|
18
|
+
parser.add_argument("-r", "--reload", action="store_true", default=False)
|
|
19
|
+
parser.add_argument("-w", "--workers", type=int, default=1)
|
|
20
|
+
parser.add_argument("-t", "--threads", type=int, default=cpus())
|
|
21
|
+
parser.add_argument(
|
|
22
|
+
"-k",
|
|
23
|
+
"--tasks",
|
|
24
|
+
nargs="?",
|
|
25
|
+
type=int,
|
|
26
|
+
const=cpus() // 2,
|
|
27
|
+
default=0,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
def on_startup(self):
|
|
31
|
+
if self.runner:
|
|
32
|
+
threading.Thread(target=self.runner.run).start()
|
|
33
|
+
|
|
34
|
+
def on_reload(self):
|
|
35
|
+
if self.runner:
|
|
36
|
+
self.runner.reload()
|
|
37
|
+
|
|
38
|
+
def on_shutdown(self):
|
|
39
|
+
if self.runner:
|
|
40
|
+
self.runner.stop()
|
|
41
|
+
|
|
42
|
+
def handle(self, *args, **options):
|
|
43
|
+
self.runner = None
|
|
44
|
+
if workers := options["tasks"]:
|
|
45
|
+
from varanus.tasks.runner import Runner
|
|
46
|
+
|
|
47
|
+
self.runner = Runner(workers=workers)
|
|
48
|
+
|
|
49
|
+
server = Granian(
|
|
50
|
+
":".join(settings.WSGI_APPLICATION.rsplit(".", 1)),
|
|
51
|
+
address="0.0.0.0",
|
|
52
|
+
port=9000,
|
|
53
|
+
interface=Interfaces.WSGI,
|
|
54
|
+
workers=options["workers"],
|
|
55
|
+
blocking_threads=options["threads"],
|
|
56
|
+
log_access=True,
|
|
57
|
+
reload=options["reload"],
|
|
58
|
+
reload_paths=[settings.BASE_DIR / "src"],
|
|
59
|
+
websockets=False,
|
|
60
|
+
)
|
|
61
|
+
server.on_startup(self.on_startup)
|
|
62
|
+
server.on_reload(self.on_reload)
|
|
63
|
+
server.on_shutdown(self.on_shutdown)
|
|
64
|
+
server.serve()
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Generated by Django
|
|
1
|
+
# Generated by Django 6.0 on 2025-12-11 00:00
|
|
2
2
|
|
|
3
3
|
import secrets
|
|
4
4
|
|
|
@@ -491,6 +491,81 @@ class Migration(migrations.Migration):
|
|
|
491
491
|
to="varanus.site",
|
|
492
492
|
),
|
|
493
493
|
),
|
|
494
|
+
migrations.CreateModel(
|
|
495
|
+
name="SiteIntegration",
|
|
496
|
+
fields=[
|
|
497
|
+
(
|
|
498
|
+
"id",
|
|
499
|
+
models.BigAutoField(
|
|
500
|
+
auto_created=True,
|
|
501
|
+
primary_key=True,
|
|
502
|
+
serialize=False,
|
|
503
|
+
verbose_name="ID",
|
|
504
|
+
),
|
|
505
|
+
),
|
|
506
|
+
("name", models.CharField(max_length=100)),
|
|
507
|
+
("integration_path", models.CharField(max_length=250)),
|
|
508
|
+
("settings", models.JSONField(blank=True, default=dict)),
|
|
509
|
+
("event_filter", models.JSONField(blank=True, default=dict)),
|
|
510
|
+
("debounce", models.CharField(default="30d", max_length=20)),
|
|
511
|
+
(
|
|
512
|
+
"site",
|
|
513
|
+
models.ForeignKey(
|
|
514
|
+
on_delete=django.db.models.deletion.CASCADE,
|
|
515
|
+
related_name="integrations",
|
|
516
|
+
to="varanus.site",
|
|
517
|
+
),
|
|
518
|
+
),
|
|
519
|
+
],
|
|
520
|
+
options={
|
|
521
|
+
"db_table": "site_integration",
|
|
522
|
+
},
|
|
523
|
+
),
|
|
524
|
+
migrations.CreateModel(
|
|
525
|
+
name="ContextIntegration",
|
|
526
|
+
fields=[
|
|
527
|
+
(
|
|
528
|
+
"id",
|
|
529
|
+
models.BigAutoField(
|
|
530
|
+
auto_created=True,
|
|
531
|
+
primary_key=True,
|
|
532
|
+
serialize=False,
|
|
533
|
+
verbose_name="ID",
|
|
534
|
+
),
|
|
535
|
+
),
|
|
536
|
+
(
|
|
537
|
+
"identifier",
|
|
538
|
+
models.CharField(
|
|
539
|
+
blank=True,
|
|
540
|
+
db_index=True,
|
|
541
|
+
default=None,
|
|
542
|
+
max_length=200,
|
|
543
|
+
null=True,
|
|
544
|
+
),
|
|
545
|
+
),
|
|
546
|
+
("result", models.JSONField(blank=True, default=dict)),
|
|
547
|
+
("run_date", models.DateTimeField(auto_now_add=True)),
|
|
548
|
+
(
|
|
549
|
+
"context",
|
|
550
|
+
models.ForeignKey(
|
|
551
|
+
on_delete=django.db.models.deletion.CASCADE,
|
|
552
|
+
related_name="integrations",
|
|
553
|
+
to="varanus.context",
|
|
554
|
+
),
|
|
555
|
+
),
|
|
556
|
+
(
|
|
557
|
+
"integration",
|
|
558
|
+
models.ForeignKey(
|
|
559
|
+
on_delete=django.db.models.deletion.CASCADE,
|
|
560
|
+
related_name="contexts",
|
|
561
|
+
to="varanus.siteintegration",
|
|
562
|
+
),
|
|
563
|
+
),
|
|
564
|
+
],
|
|
565
|
+
options={
|
|
566
|
+
"db_table": "context_integration",
|
|
567
|
+
},
|
|
568
|
+
),
|
|
494
569
|
migrations.CreateModel(
|
|
495
570
|
name="SiteKey",
|
|
496
571
|
fields=[
|
|
@@ -604,6 +679,20 @@ class Migration(migrations.Migration):
|
|
|
604
679
|
name="unique_site_node",
|
|
605
680
|
),
|
|
606
681
|
),
|
|
682
|
+
migrations.AddConstraint(
|
|
683
|
+
model_name="context",
|
|
684
|
+
constraint=models.UniqueConstraint(
|
|
685
|
+
condition=models.Q(("parent__isnull", True)),
|
|
686
|
+
fields=("event_id",),
|
|
687
|
+
name="unique_context_event_id",
|
|
688
|
+
),
|
|
689
|
+
),
|
|
690
|
+
migrations.AddConstraint(
|
|
691
|
+
model_name="contextintegration",
|
|
692
|
+
constraint=models.UniqueConstraint(
|
|
693
|
+
fields=("context", "integration"), name="uniq_context_integration"
|
|
694
|
+
),
|
|
695
|
+
),
|
|
607
696
|
migrations.AddConstraint(
|
|
608
697
|
model_name="sitemember",
|
|
609
698
|
constraint=models.UniqueConstraint(
|