varanus 0.1.0.dev1__tar.gz → 0.1.0.dev3__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.dev1 → varanus-0.1.0.dev3}/PKG-INFO +2 -2
- {varanus-0.1.0.dev1 → varanus-0.1.0.dev3}/pyproject.toml +2 -2
- {varanus-0.1.0.dev1 → varanus-0.1.0.dev3}/src/varanus/client/apps.py +1 -1
- {varanus-0.1.0.dev1 → varanus-0.1.0.dev3}/src/varanus/client/client.py +33 -30
- {varanus-0.1.0.dev1 → varanus-0.1.0.dev3}/src/varanus/client/context.py +11 -11
- {varanus-0.1.0.dev1 → varanus-0.1.0.dev3}/src/varanus/client/middleware.py +6 -6
- {varanus-0.1.0.dev1 → varanus-0.1.0.dev3}/src/varanus/client/transport/base.py +1 -1
- {varanus-0.1.0.dev1 → varanus-0.1.0.dev3}/src/varanus/client/transport/database.py +4 -1
- varanus-0.1.0.dev3/src/varanus/client/transport/http.py +83 -0
- {varanus-0.1.0.dev1 → varanus-0.1.0.dev3}/src/varanus/events.py +10 -0
- varanus-0.1.0.dev3/src/varanus/server/migrations/0003_alter_log_level.py +28 -0
- {varanus-0.1.0.dev1 → varanus-0.1.0.dev3}/src/varanus/server/models.py +19 -7
- {varanus-0.1.0.dev1 → varanus-0.1.0.dev3}/src/varanus/server/search/base.py +17 -7
- varanus-0.1.0.dev3/src/varanus/server/search/facet.py +63 -0
- {varanus-0.1.0.dev1 → varanus-0.1.0.dev3}/src/varanus/server/templates/site/base.html +1 -5
- varanus-0.1.0.dev3/src/varanus/server/templates/site/details/environment_nodes.html +6 -0
- varanus-0.1.0.dev3/src/varanus/server/templates/site/details/error.html +49 -0
- varanus-0.1.0.dev3/src/varanus/server/templates/site/details/log.html +36 -0
- varanus-0.1.0.dev3/src/varanus/server/templates/site/details/metric.html +40 -0
- varanus-0.1.0.dev3/src/varanus/server/templates/site/details/node.html +44 -0
- varanus-0.1.0.dev3/src/varanus/server/templates/site/details/node_environments.html +6 -0
- varanus-0.1.0.dev3/src/varanus/server/templates/site/details/query.html +31 -0
- varanus-0.1.0.dev3/src/varanus/server/templates/site/details/request.html +136 -0
- {varanus-0.1.0.dev1 → varanus-0.1.0.dev3}/src/varanus/server/templates/site/errors.html +1 -1
- {varanus-0.1.0.dev1 → varanus-0.1.0.dev3}/src/varanus/server/templates/site/logs.html +2 -2
- {varanus-0.1.0.dev1 → varanus-0.1.0.dev3}/src/varanus/server/templates/site/metrics.html +1 -1
- {varanus-0.1.0.dev1 → varanus-0.1.0.dev3}/src/varanus/server/templates/site/overview.html +10 -2
- {varanus-0.1.0.dev1 → varanus-0.1.0.dev3}/src/varanus/server/templates/site/queries.html +1 -1
- {varanus-0.1.0.dev1 → varanus-0.1.0.dev3}/src/varanus/server/templates/site/requests.html +1 -1
- varanus-0.1.0.dev3/src/varanus/server/templatetags/varanus.py +13 -0
- {varanus-0.1.0.dev1 → varanus-0.1.0.dev3}/src/varanus/server/urls.py +22 -0
- varanus-0.1.0.dev3/src/varanus/server/views/__init__.py +0 -0
- {varanus-0.1.0.dev1 → varanus-0.1.0.dev3}/src/varanus/server/views/api.py +20 -12
- {varanus-0.1.0.dev1 → varanus-0.1.0.dev3}/src/varanus/server/views/site.py +57 -4
- varanus-0.1.0.dev1/src/varanus/client/transport/http.py +0 -37
- varanus-0.1.0.dev1/src/varanus/server/search/facet.py +0 -59
- varanus-0.1.0.dev1/src/varanus/server/templates/site/details/error.html +0 -1
- varanus-0.1.0.dev1/src/varanus/server/templates/site/details/log.html +0 -1
- varanus-0.1.0.dev1/src/varanus/server/templates/site/details/metric.html +0 -1
- varanus-0.1.0.dev1/src/varanus/server/templates/site/details/query.html +0 -1
- varanus-0.1.0.dev1/src/varanus/server/templates/site/details/request.html +0 -44
- {varanus-0.1.0.dev1 → varanus-0.1.0.dev3}/README.md +0 -0
- {varanus-0.1.0.dev1 → varanus-0.1.0.dev3}/src/varanus/__init__.py +0 -0
- {varanus-0.1.0.dev1 → varanus-0.1.0.dev3}/src/varanus/client/__init__.py +0 -0
- {varanus-0.1.0.dev1 → varanus-0.1.0.dev3}/src/varanus/client/loggers.py +0 -0
- {varanus-0.1.0.dev1 → varanus-0.1.0.dev3}/src/varanus/client/transport/__init__.py +0 -0
- {varanus-0.1.0.dev1 → varanus-0.1.0.dev3}/src/varanus/server/__init__.py +0 -0
- {varanus-0.1.0.dev1 → varanus-0.1.0.dev3}/src/varanus/server/__main__.py +0 -0
- {varanus-0.1.0.dev1 → varanus-0.1.0.dev3}/src/varanus/server/admin.py +0 -0
- {varanus-0.1.0.dev1 → varanus-0.1.0.dev3}/src/varanus/server/apps.py +0 -0
- {varanus-0.1.0.dev1 → varanus-0.1.0.dev3}/src/varanus/server/asgi.py +0 -0
- {varanus-0.1.0.dev1 → varanus-0.1.0.dev3}/src/varanus/server/context_processors.py +0 -0
- {varanus-0.1.0.dev1 → varanus-0.1.0.dev3}/src/varanus/server/management/__init__.py +0 -0
- {varanus-0.1.0.dev1 → varanus-0.1.0.dev3}/src/varanus/server/management/commands/__init__.py +0 -0
- {varanus-0.1.0.dev1 → varanus-0.1.0.dev3}/src/varanus/server/management/commands/migrateall.py +0 -0
- {varanus-0.1.0.dev1 → varanus-0.1.0.dev3}/src/varanus/server/middleware.py +0 -0
- {varanus-0.1.0.dev1 → varanus-0.1.0.dev3}/src/varanus/server/migrations/0001_initial.py +0 -0
- {varanus-0.1.0.dev1 → varanus-0.1.0.dev3}/src/varanus/server/migrations/0002_context_node_error_node_log_node_metric_node_and_more.py +0 -0
- {varanus-0.1.0.dev1 → varanus-0.1.0.dev3}/src/varanus/server/migrations/__init__.py +0 -0
- {varanus-0.1.0.dev1 → varanus-0.1.0.dev3}/src/varanus/server/router.py +0 -0
- {varanus-0.1.0.dev1 → varanus-0.1.0.dev3}/src/varanus/server/search/__init__.py +0 -0
- {varanus-0.1.0.dev1 → varanus-0.1.0.dev3}/src/varanus/server/search/date.py +0 -0
- {varanus-0.1.0.dev1 → varanus-0.1.0.dev3}/src/varanus/server/settings.py +0 -0
- {varanus-0.1.0.dev1 → varanus-0.1.0.dev3}/src/varanus/server/static/css/varanus.css +0 -0
- {varanus-0.1.0.dev1 → varanus-0.1.0.dev3}/src/varanus/server/static/js/varanus.js +0 -0
- {varanus-0.1.0.dev1 → varanus-0.1.0.dev3}/src/varanus/server/templates/base.html +0 -0
- {varanus-0.1.0.dev1 → varanus-0.1.0.dev3}/src/varanus/server/templates/dashboard.html +0 -0
- {varanus-0.1.0.dev1 → varanus-0.1.0.dev3}/src/varanus/server/templates/registration/login.html +0 -0
- {varanus-0.1.0.dev1 → varanus-0.1.0.dev3}/src/varanus/server/templates/search/daterange.html +0 -0
- {varanus-0.1.0.dev1 → varanus-0.1.0.dev3}/src/varanus/server/templates/search/multifacet.html +0 -0
- {varanus-0.1.0.dev1/src/varanus/server/views → varanus-0.1.0.dev3/src/varanus/server/templatetags}/__init__.py +0 -0
- {varanus-0.1.0.dev1 → varanus-0.1.0.dev3}/src/varanus/server/views/base.py +0 -0
- {varanus-0.1.0.dev1 → varanus-0.1.0.dev3}/src/varanus/server/views/dashboard.py +0 -0
- {varanus-0.1.0.dev1 → varanus-0.1.0.dev3}/src/varanus/server/wsgi.py +0 -0
- {varanus-0.1.0.dev1 → varanus-0.1.0.dev3}/src/varanus/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: varanus
|
|
3
|
-
Version: 0.1.0.
|
|
3
|
+
Version: 0.1.0.dev3
|
|
4
4
|
Summary: Django application monitoring.
|
|
5
5
|
Requires-Dist: httpx>=0.27.0
|
|
6
6
|
Requires-Dist: msgspec>=0.19.0
|
|
@@ -11,7 +11,7 @@ Requires-Dist: granian>=2.4.2 ; extra == 'server'
|
|
|
11
11
|
Requires-Dist: psycopg[binary]>=3.2.1 ; 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.11
|
|
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.dev3"
|
|
4
4
|
description = "Django application monitoring."
|
|
5
5
|
readme = "README.md"
|
|
6
|
-
requires-python = ">=3.
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
7
|
dependencies = [
|
|
8
8
|
"httpx>=0.27.0",
|
|
9
9
|
"msgspec>=0.19.0",
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import platform
|
|
2
2
|
from importlib.metadata import distributions
|
|
3
|
-
from typing import Union
|
|
4
3
|
from urllib.parse import urlsplit
|
|
5
4
|
|
|
6
5
|
from varanus.events import Context, NodeInfo
|
|
@@ -20,14 +19,14 @@ def install_query_logger(logger):
|
|
|
20
19
|
|
|
21
20
|
|
|
22
21
|
class VaranusClient:
|
|
23
|
-
environment: str
|
|
22
|
+
environment: str
|
|
24
23
|
transport: BaseTransport
|
|
25
24
|
|
|
26
25
|
request_attr: str
|
|
27
26
|
logger_name: str
|
|
28
27
|
tags: dict
|
|
29
28
|
|
|
30
|
-
include_headers:
|
|
29
|
+
include_headers: list | bool | None
|
|
31
30
|
exclude_headers: list | None
|
|
32
31
|
sensitive_headers = set(
|
|
33
32
|
[
|
|
@@ -38,9 +37,8 @@ class VaranusClient:
|
|
|
38
37
|
)
|
|
39
38
|
|
|
40
39
|
scheme_transports = {
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"https": "varanus.client.transport.http.HttpTransport",
|
|
40
|
+
"http": "varanus.client.transport.http.ThreadedHttpTransport",
|
|
41
|
+
"https": "varanus.client.transport.http.ThreadedHttpTransport",
|
|
44
42
|
"db": "varanus.client.transport.database.ModelTransport",
|
|
45
43
|
}
|
|
46
44
|
|
|
@@ -49,29 +47,38 @@ class VaranusClient:
|
|
|
49
47
|
|
|
50
48
|
def setup(
|
|
51
49
|
self,
|
|
52
|
-
dsn,
|
|
53
|
-
|
|
54
|
-
environment=
|
|
55
|
-
transport_class=None,
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
50
|
+
dsn: str,
|
|
51
|
+
node: str | None = None,
|
|
52
|
+
environment: str = "",
|
|
53
|
+
transport_class: str | type[BaseTransport] | None = None,
|
|
54
|
+
request_attr: str = "varanus",
|
|
55
|
+
logger_name: str = "varanus.request",
|
|
56
|
+
tags: dict | None = None,
|
|
57
|
+
include_headers: list | bool | None = None,
|
|
58
|
+
exclude_headers: list | None = None,
|
|
60
59
|
log_queries: bool | int = False,
|
|
61
|
-
log_query_params=False,
|
|
62
|
-
query_metrics=False,
|
|
63
|
-
send_all=False,
|
|
64
|
-
install=None,
|
|
60
|
+
log_query_params: bool = False,
|
|
61
|
+
query_metrics: bool | str = False,
|
|
62
|
+
send_all: bool = False,
|
|
63
|
+
install: list | None = None,
|
|
65
64
|
):
|
|
66
65
|
url = urlsplit(dsn)
|
|
67
66
|
self.environment = environment
|
|
67
|
+
self.node = node or platform.node()
|
|
68
68
|
if transport_class is None:
|
|
69
|
-
|
|
70
|
-
if transport_class is None:
|
|
69
|
+
if url.scheme not in self.scheme_transports:
|
|
71
70
|
raise ValueError(f"No transport class found for `{url.scheme}`")
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
71
|
+
resolved_class = import_string(self.scheme_transports[url.scheme])
|
|
72
|
+
elif isinstance(transport_class, str):
|
|
73
|
+
resolved_class = import_string(transport_class)
|
|
74
|
+
else:
|
|
75
|
+
resolved_class = transport_class
|
|
76
|
+
if not issubclass(resolved_class, BaseTransport):
|
|
77
|
+
raise ValueError(
|
|
78
|
+
f"Transport class `{transport_class}` must be a subclass of"
|
|
79
|
+
"BaseTransport."
|
|
80
|
+
)
|
|
81
|
+
self.transport = resolved_class(url, self.environment, self.node)
|
|
75
82
|
self.request_attr = request_attr
|
|
76
83
|
self.logger_name = logger_name
|
|
77
84
|
self.tags = tags or {}
|
|
@@ -98,10 +105,6 @@ class VaranusClient:
|
|
|
98
105
|
self.send_all = send_all
|
|
99
106
|
self.configured = True
|
|
100
107
|
if install:
|
|
101
|
-
if not isinstance(install, list):
|
|
102
|
-
raise TypeError(
|
|
103
|
-
"The varanus middleware can only be automatically installed into a list."
|
|
104
|
-
)
|
|
105
108
|
if "django.contrib.auth.middleware.AuthenticationMiddleware" in install:
|
|
106
109
|
idx = install.index(
|
|
107
110
|
"django.contrib.auth.middleware.AuthenticationMiddleware"
|
|
@@ -121,14 +124,14 @@ class VaranusClient:
|
|
|
121
124
|
def ping(self):
|
|
122
125
|
self.transport.ping(
|
|
123
126
|
NodeInfo(
|
|
124
|
-
name=
|
|
127
|
+
name=self.node,
|
|
125
128
|
platform=platform.platform(),
|
|
126
129
|
python_version=platform.python_version(),
|
|
127
130
|
packages={d.name: d.version for d in distributions()},
|
|
128
131
|
)
|
|
129
132
|
)
|
|
130
133
|
|
|
131
|
-
def log(self, level, message, *args, **kwargs):
|
|
134
|
+
def log(self, level: int, message: str, *args, **kwargs):
|
|
132
135
|
if ctx := current_context.get():
|
|
133
136
|
kwargs.setdefault("stacklevel", 2)
|
|
134
137
|
ctx.log(level, message, *args, **kwargs)
|
|
@@ -137,7 +140,7 @@ class VaranusClient:
|
|
|
137
140
|
if ctx := current_context.get():
|
|
138
141
|
ctx.raw_exception(exception, tags=tags)
|
|
139
142
|
|
|
140
|
-
def metric(self, name, value: float = 0.0, tags: dict | None = None):
|
|
143
|
+
def metric(self, name: str, value: float = 0.0, tags: dict | None = None):
|
|
141
144
|
if ctx := current_context.get():
|
|
142
145
|
ctx.metric(name, value, tags=tags)
|
|
143
146
|
|
|
@@ -78,11 +78,11 @@ class VaranusContext:
|
|
|
78
78
|
|
|
79
79
|
def log(
|
|
80
80
|
self,
|
|
81
|
-
level,
|
|
82
|
-
message,
|
|
81
|
+
level: int,
|
|
82
|
+
message: str,
|
|
83
83
|
*args,
|
|
84
84
|
exc_info=None,
|
|
85
|
-
stacklevel=1,
|
|
85
|
+
stacklevel: int = 1,
|
|
86
86
|
tags: dict | None = None,
|
|
87
87
|
**kwargs,
|
|
88
88
|
):
|
|
@@ -99,27 +99,27 @@ class VaranusContext:
|
|
|
99
99
|
)
|
|
100
100
|
)
|
|
101
101
|
|
|
102
|
-
def debug(self, message, *args, **kwargs):
|
|
102
|
+
def debug(self, message: str, *args, **kwargs):
|
|
103
103
|
kwargs.setdefault("stacklevel", 2)
|
|
104
104
|
self.log(logging.DEBUG, message, *args, **kwargs)
|
|
105
105
|
|
|
106
|
-
def info(self, message, *args, **kwargs):
|
|
106
|
+
def info(self, message: str, *args, **kwargs):
|
|
107
107
|
kwargs.setdefault("stacklevel", 2)
|
|
108
108
|
self.log(logging.INFO, message, *args, **kwargs)
|
|
109
109
|
|
|
110
|
-
def warning(self, message, *args, **kwargs):
|
|
110
|
+
def warning(self, message: str, *args, **kwargs):
|
|
111
111
|
kwargs.setdefault("stacklevel", 2)
|
|
112
112
|
self.log(logging.WARNING, message, *args, **kwargs)
|
|
113
113
|
|
|
114
|
-
def error(self, message, *args, **kwargs):
|
|
114
|
+
def error(self, message: str, *args, **kwargs):
|
|
115
115
|
kwargs.setdefault("stacklevel", 2)
|
|
116
116
|
self.log(logging.ERROR, message, *args, **kwargs)
|
|
117
117
|
|
|
118
|
-
def critical(self, message, *args, **kwargs):
|
|
118
|
+
def critical(self, message: str, *args, **kwargs):
|
|
119
119
|
kwargs.setdefault("stacklevel", 2)
|
|
120
120
|
self.log(logging.CRITICAL, message, *args, **kwargs)
|
|
121
121
|
|
|
122
|
-
def exception(self, message, *args, **kwargs):
|
|
122
|
+
def exception(self, message: str, *args, **kwargs):
|
|
123
123
|
kwargs.setdefault("stacklevel", 2)
|
|
124
124
|
kwargs.setdefault("exc_info", sys.exc_info())
|
|
125
125
|
self.log(logging.ERROR, message, *args, **kwargs)
|
|
@@ -128,12 +128,12 @@ class VaranusContext:
|
|
|
128
128
|
if err := Error.from_exception(exception, tags=tags):
|
|
129
129
|
self.errors.append(err)
|
|
130
130
|
|
|
131
|
-
def context(self, name="", tags: dict | None = None):
|
|
131
|
+
def context(self, name: str = "", tags: dict | None = None):
|
|
132
132
|
ctx = VaranusContext(self.client, name, tags)
|
|
133
133
|
self.subcontexts.append(ctx)
|
|
134
134
|
return ctx
|
|
135
135
|
|
|
136
|
-
def metric(self, name, value: float = 0.0, tags: dict | None = None):
|
|
136
|
+
def metric(self, name: str, value: float = 0.0, tags: dict | None = None):
|
|
137
137
|
if name not in self.metrics:
|
|
138
138
|
self.metrics[name] = Metric(name=name, tags=tags or {})
|
|
139
139
|
self.metrics[name].update(value)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import ipaddress
|
|
2
2
|
|
|
3
3
|
from django.core.exceptions import MiddlewareNotUsed
|
|
4
4
|
from django.http import HttpRequest, HttpResponse
|
|
@@ -6,8 +6,6 @@ from django.http import HttpRequest, HttpResponse
|
|
|
6
6
|
from ..events import Request
|
|
7
7
|
from .client import client
|
|
8
8
|
|
|
9
|
-
IP_REGEX = re.compile(r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}")
|
|
10
|
-
|
|
11
9
|
|
|
12
10
|
def get_ip(request: HttpRequest):
|
|
13
11
|
ip_address = request.META.get("HTTP_X_FORWARDED_FOR", "").strip()
|
|
@@ -15,9 +13,11 @@ def get_ip(request: HttpRequest):
|
|
|
15
13
|
ip_address = ip_address.split(",")[0].strip()
|
|
16
14
|
if not ip_address:
|
|
17
15
|
ip_address = request.META.get("REMOTE_ADDR", "127.0.0.1").strip()
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
try:
|
|
17
|
+
# Validate and normalize the IP address.
|
|
18
|
+
return str(ipaddress.ip_address(ip_address))
|
|
19
|
+
except ValueError:
|
|
20
|
+
return ""
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
def request_headers(request: HttpRequest):
|
|
@@ -10,8 +10,9 @@ from .base import BaseTransport
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class ModelTransport(BaseTransport):
|
|
13
|
-
def __init__(self, url: SplitResult, environment: str):
|
|
13
|
+
def __init__(self, url: SplitResult, environment: str, node: str):
|
|
14
14
|
self.environment = environment
|
|
15
|
+
self.node = node
|
|
15
16
|
|
|
16
17
|
def ping(self, info: events.NodeInfo):
|
|
17
18
|
site = models.Site.objects.get()
|
|
@@ -20,9 +21,11 @@ class ModelTransport(BaseTransport):
|
|
|
20
21
|
@transaction.atomic
|
|
21
22
|
def send(self, event: events.Context):
|
|
22
23
|
site = models.Site.objects.get()
|
|
24
|
+
node = site.nodes.get(name=self.node, environment=self.environment)
|
|
23
25
|
models.Context.from_event(
|
|
24
26
|
event,
|
|
25
27
|
event_id=uuid.uuid4(),
|
|
26
28
|
site=site,
|
|
27
29
|
environment=self.environment,
|
|
30
|
+
node=node,
|
|
28
31
|
)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import queue
|
|
3
|
+
import threading
|
|
4
|
+
import time
|
|
5
|
+
from typing import Any
|
|
6
|
+
from urllib.parse import SplitResult, parse_qs
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
import msgspec
|
|
10
|
+
|
|
11
|
+
from varanus import events
|
|
12
|
+
|
|
13
|
+
from .base import BaseTransport
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class HttpTransport(BaseTransport):
|
|
19
|
+
def __init__(self, url: SplitResult, environment: str, node: str):
|
|
20
|
+
path = url.path.rstrip("/")
|
|
21
|
+
self.ping_url = f"{url.scheme}://{url.netloc}{path}/api/ping/"
|
|
22
|
+
self.event_url = f"{url.scheme}://{url.netloc}{path}/api/ingest/"
|
|
23
|
+
self.options = parse_qs(url.query, keep_blank_values=True)
|
|
24
|
+
timeout = 1.0
|
|
25
|
+
if "timeout" in self.options:
|
|
26
|
+
timeout = float(self.options["timeout"][0])
|
|
27
|
+
self.client = httpx.Client(
|
|
28
|
+
headers={
|
|
29
|
+
"X-Varanus-Key": url.username or "",
|
|
30
|
+
"X-Varanus-Environment": environment,
|
|
31
|
+
"X-Varanus-Node": node,
|
|
32
|
+
},
|
|
33
|
+
timeout=timeout,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
def request(self, url: str, obj: Any):
|
|
37
|
+
try:
|
|
38
|
+
self.client.post(url, content=msgspec.json.encode(obj))
|
|
39
|
+
except Exception:
|
|
40
|
+
logger.exception("error sending to %s", url)
|
|
41
|
+
|
|
42
|
+
def ping(self, info: events.NodeInfo):
|
|
43
|
+
self.request(self.ping_url, info)
|
|
44
|
+
|
|
45
|
+
def send(self, event: events.Context):
|
|
46
|
+
self.request(self.event_url, [event])
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def sender(pending: queue.SimpleQueue, client: httpx.Client, url: str, rate: float):
|
|
50
|
+
while True:
|
|
51
|
+
start = time.time()
|
|
52
|
+
events = []
|
|
53
|
+
while True:
|
|
54
|
+
try:
|
|
55
|
+
events.append(pending.get_nowait())
|
|
56
|
+
except queue.Empty:
|
|
57
|
+
break
|
|
58
|
+
if len(events) >= 100:
|
|
59
|
+
break
|
|
60
|
+
if events:
|
|
61
|
+
try:
|
|
62
|
+
client.post(url, content=msgspec.json.encode(events))
|
|
63
|
+
except Exception:
|
|
64
|
+
logger.exception("error sending to %s", url)
|
|
65
|
+
elapsed = time.time() - start
|
|
66
|
+
time.sleep(max(rate - elapsed, 1.0))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class ThreadedHttpTransport(HttpTransport):
|
|
70
|
+
def __init__(self, url: SplitResult, environment: str, node: str):
|
|
71
|
+
super().__init__(url, environment, node)
|
|
72
|
+
self.pending = queue.SimpleQueue()
|
|
73
|
+
rate = 3.0
|
|
74
|
+
if "rate" in self.options:
|
|
75
|
+
rate = float(self.options["rate"][0])
|
|
76
|
+
threading.Thread(
|
|
77
|
+
target=sender,
|
|
78
|
+
args=(self.pending, self.client, self.event_url, rate),
|
|
79
|
+
daemon=True,
|
|
80
|
+
).start()
|
|
81
|
+
|
|
82
|
+
def send(self, event: events.Context):
|
|
83
|
+
self.pending.put(event)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import inspect
|
|
1
2
|
import logging
|
|
2
3
|
from datetime import datetime, timezone
|
|
3
4
|
|
|
@@ -25,6 +26,7 @@ class ErrorLine(Struct):
|
|
|
25
26
|
lineno: int | None
|
|
26
27
|
function: str | None
|
|
27
28
|
module: str | None
|
|
29
|
+
linesrc: str | None
|
|
28
30
|
locals: dict[str, str]
|
|
29
31
|
|
|
30
32
|
|
|
@@ -52,6 +54,13 @@ class Error(Event):
|
|
|
52
54
|
abs_path = f_code.co_filename if f_code else None
|
|
53
55
|
function = f_code.co_name if f_code else None
|
|
54
56
|
lineno = getattr(tb, "tb_lineno", None)
|
|
57
|
+
linesrc = None
|
|
58
|
+
if f_code and lineno:
|
|
59
|
+
try:
|
|
60
|
+
source, start = inspect.getsourcelines(f_code)
|
|
61
|
+
linesrc = source[lineno - start].strip()
|
|
62
|
+
except (OSError, TypeError):
|
|
63
|
+
pass
|
|
55
64
|
module = f_globals.get("__name__", "")
|
|
56
65
|
lines.append(
|
|
57
66
|
ErrorLine(
|
|
@@ -59,6 +68,7 @@ class Error(Event):
|
|
|
59
68
|
lineno=lineno,
|
|
60
69
|
function=function,
|
|
61
70
|
module=module,
|
|
71
|
+
linesrc=linesrc,
|
|
62
72
|
locals={name: repr(val) for name, val in f_locals.items()},
|
|
63
73
|
)
|
|
64
74
|
)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Generated by Django 5.2.8 on 2025-11-17 19:51
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
dependencies = [
|
|
8
|
+
("varanus", "0002_context_node_error_node_log_node_metric_node_and_more"),
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
operations = [
|
|
12
|
+
migrations.AlterField(
|
|
13
|
+
model_name="log",
|
|
14
|
+
name="level",
|
|
15
|
+
field=models.IntegerField(
|
|
16
|
+
blank=True,
|
|
17
|
+
choices=[
|
|
18
|
+
(50, "CRITICAL"),
|
|
19
|
+
(40, "ERROR"),
|
|
20
|
+
(30, "WARNING"),
|
|
21
|
+
(20, "INFO"),
|
|
22
|
+
(10, "DEBUG"),
|
|
23
|
+
(0, "NOTSET"),
|
|
24
|
+
],
|
|
25
|
+
null=True,
|
|
26
|
+
),
|
|
27
|
+
),
|
|
28
|
+
]
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import hashlib
|
|
2
2
|
import http
|
|
3
|
-
import logging
|
|
4
3
|
import secrets
|
|
5
4
|
from typing import Self
|
|
6
5
|
|
|
@@ -161,7 +160,16 @@ class Node(EnvironmentModel):
|
|
|
161
160
|
]
|
|
162
161
|
|
|
163
162
|
def __str__(self):
|
|
164
|
-
return self.name
|
|
163
|
+
return f"{self.environment}@{self.name}"
|
|
164
|
+
|
|
165
|
+
def get_absolute_url(self):
|
|
166
|
+
return reverse(
|
|
167
|
+
"node-details",
|
|
168
|
+
kwargs={
|
|
169
|
+
"slug": self.site.slug,
|
|
170
|
+
"pk": self.pk,
|
|
171
|
+
},
|
|
172
|
+
)
|
|
165
173
|
|
|
166
174
|
@classmethod
|
|
167
175
|
def update(cls, info: events.NodeInfo, site: Site, environment: str = ""):
|
|
@@ -450,9 +458,17 @@ class Error(ContextualModel, FingerprintModel):
|
|
|
450
458
|
|
|
451
459
|
|
|
452
460
|
class Log(ContextualModel, FingerprintModel):
|
|
461
|
+
class Level(models.IntegerChoices):
|
|
462
|
+
CRITICAL = 50, "CRITICAL"
|
|
463
|
+
ERROR = 40, "ERROR"
|
|
464
|
+
WARNING = 30, "WARNING"
|
|
465
|
+
INFO = 20, "INFO"
|
|
466
|
+
DEBUG = 10, "DEBUG"
|
|
467
|
+
NOTSET = 0, "NOTSET"
|
|
468
|
+
|
|
453
469
|
message = models.TextField()
|
|
454
470
|
name = models.CharField(max_length=200, blank=True)
|
|
455
|
-
level = models.IntegerField(null=True, blank=True)
|
|
471
|
+
level = models.IntegerField(null=True, blank=True, choices=Level)
|
|
456
472
|
file = models.TextField(blank=True)
|
|
457
473
|
lineno = models.IntegerField(null=True, blank=True)
|
|
458
474
|
error = models.ForeignKey(
|
|
@@ -481,10 +497,6 @@ class Log(ContextualModel, FingerprintModel):
|
|
|
481
497
|
**extra,
|
|
482
498
|
)
|
|
483
499
|
|
|
484
|
-
@property
|
|
485
|
-
def level_name(self):
|
|
486
|
-
return logging.getLevelName(self.level) if self.level is not None else "UNKNOWN"
|
|
487
|
-
|
|
488
500
|
def fingerprint_parts(self):
|
|
489
501
|
return (self.message, self.name, self.level, self.file, self.lineno)
|
|
490
502
|
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
from typing import ClassVar, Mapping, Self
|
|
1
|
+
from typing import Callable, ClassVar, Literal, Mapping, Self, TypeAlias
|
|
2
2
|
|
|
3
3
|
from django.db.models import QuerySet
|
|
4
4
|
from django.http import HttpRequest
|
|
5
5
|
from django.template import loader
|
|
6
6
|
from django.utils.datastructures import MultiValueDict
|
|
7
|
+
from django.utils.functional import Promise
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
StringValues: TypeAlias = dict[str, list[str]]
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
def string_data(data) -> StringValues:
|
|
@@ -26,21 +27,31 @@ def string_data(data) -> StringValues:
|
|
|
26
27
|
class SearchField:
|
|
27
28
|
template_name: str
|
|
28
29
|
|
|
29
|
-
def __init__(
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
label=None,
|
|
33
|
+
field_name=None,
|
|
34
|
+
empty_label="NULL",
|
|
35
|
+
# These belong on Facet
|
|
36
|
+
choice_label: Callable[..., str | Promise] | None = None,
|
|
37
|
+
order: Literal["value", "count"] = "value",
|
|
38
|
+
):
|
|
30
39
|
self.name = ""
|
|
31
40
|
self.label = label or ""
|
|
32
41
|
self.field_name = field_name or ""
|
|
33
42
|
self.prefix = ""
|
|
34
43
|
self.id = ""
|
|
44
|
+
self.empty_label = empty_label
|
|
45
|
+
self.choice_label = choice_label
|
|
46
|
+
self.order = order
|
|
35
47
|
|
|
36
48
|
def __set_name__(self, owner, name: str):
|
|
37
|
-
# print("__set_name__", owner, name)
|
|
38
49
|
assert issubclass(owner, Search)
|
|
39
50
|
self.name = name
|
|
40
51
|
self.prefix = f"{name}_"
|
|
41
52
|
self.id = f"id_{name}"
|
|
42
53
|
if not self.label:
|
|
43
|
-
self.label = name.capitalize()
|
|
54
|
+
self.label = name.capitalize().replace("__", " ")
|
|
44
55
|
if not self.field_name:
|
|
45
56
|
self.field_name = name
|
|
46
57
|
if not hasattr(owner, "_fields"):
|
|
@@ -48,7 +59,6 @@ class SearchField:
|
|
|
48
59
|
owner._fields.append(self)
|
|
49
60
|
|
|
50
61
|
def __get__(self, instance: "Search", owner=None) -> StringValues | Self:
|
|
51
|
-
# print("__get__", instance, owner)
|
|
52
62
|
if owner is None:
|
|
53
63
|
return self
|
|
54
64
|
return {
|
|
@@ -58,7 +68,7 @@ class SearchField:
|
|
|
58
68
|
}
|
|
59
69
|
|
|
60
70
|
def __set__(self, instance, value):
|
|
61
|
-
|
|
71
|
+
raise AttributeError(f"`{self.name}` is read-only")
|
|
62
72
|
|
|
63
73
|
def apply(self, queryset: QuerySet, field_data: StringValues) -> QuerySet:
|
|
64
74
|
raise NotImplementedError()
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import operator
|
|
3
|
+
from typing import Any, ClassVar
|
|
4
|
+
|
|
5
|
+
from django.db.models import Count, Q, QuerySet
|
|
6
|
+
|
|
7
|
+
from .base import SearchField, StringValues
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Choice:
|
|
11
|
+
id: str
|
|
12
|
+
value: str
|
|
13
|
+
label: str
|
|
14
|
+
count: int
|
|
15
|
+
|
|
16
|
+
def __init__(self, field: SearchField, value: Any, count: int):
|
|
17
|
+
self.value = self.stringify(value)
|
|
18
|
+
if callable(field.choice_label):
|
|
19
|
+
self.label = field.choice_label(value)
|
|
20
|
+
else:
|
|
21
|
+
self.label = self.value or field.empty_label
|
|
22
|
+
self.id = field.id + "_" + self.value.replace(" ", "")
|
|
23
|
+
self.count = count
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def stringify(cls, value):
|
|
27
|
+
return str(value) if value is not None else ""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Facet(SearchField):
|
|
31
|
+
choice_class: ClassVar[type[Choice]] = Choice
|
|
32
|
+
|
|
33
|
+
def get_choices(self, queryset):
|
|
34
|
+
choice_qs = queryset.values_list(self.field_name).annotate(num=Count("*"))
|
|
35
|
+
match self.order:
|
|
36
|
+
case "value":
|
|
37
|
+
choice_qs = choice_qs.order_by(self.field_name)
|
|
38
|
+
case "count":
|
|
39
|
+
choice_qs = choice_qs.order_by("-num")
|
|
40
|
+
return [self.choice_class(self, c, num) for c, num in choice_qs]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class MultiFacet(Facet):
|
|
44
|
+
template_name = "search/multifacet.html"
|
|
45
|
+
|
|
46
|
+
def apply(self, queryset: QuerySet, field_data: StringValues) -> QuerySet:
|
|
47
|
+
filters = []
|
|
48
|
+
if selected := set(field_data.get("s", [])):
|
|
49
|
+
if "" in selected:
|
|
50
|
+
selected.discard("")
|
|
51
|
+
filters.append(Q(**{f"{self.field_name}__isnull": True}))
|
|
52
|
+
if selected:
|
|
53
|
+
filters.append(Q(**{f"{self.field_name}__in": selected}))
|
|
54
|
+
if filters:
|
|
55
|
+
queryset = queryset.filter(functools.reduce(operator.or_, filters))
|
|
56
|
+
return queryset
|
|
57
|
+
|
|
58
|
+
def get_context(self, queryset: QuerySet, field_data: StringValues) -> dict:
|
|
59
|
+
return {
|
|
60
|
+
**super().get_context(queryset, field_data),
|
|
61
|
+
"choices": self.get_choices(queryset),
|
|
62
|
+
"selected": field_data.get("s", []),
|
|
63
|
+
}
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
<form action="" method="get">
|
|
33
33
|
{{ search.render|safe }}
|
|
34
34
|
<button class="btn btn-primary">Filter</button>
|
|
35
|
+
<a href="{{ request.path }}" class="btn btn-secondary">Clear</a>
|
|
35
36
|
</form>
|
|
36
37
|
{% endblock search %}
|
|
37
38
|
</div>
|
|
@@ -42,11 +43,6 @@
|
|
|
42
43
|
</div>
|
|
43
44
|
|
|
44
45
|
<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>
|
|
50
46
|
</div>
|
|
51
47
|
{% endblock content %}
|
|
52
48
|
|