varanus 0.1.0.dev2__py3-none-any.whl → 0.1.0.dev4__py3-none-any.whl
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/client/apps.py +1 -1
- varanus/client/client.py +100 -47
- varanus/client/context.py +11 -11
- varanus/client/loggers.py +4 -2
- varanus/client/middleware.py +15 -19
- varanus/client/transport/base.py +1 -1
- varanus/client/transport/database.py +41 -14
- varanus/client/transport/http.py +55 -9
- varanus/events.py +33 -6
- varanus/{server/search → search}/__init__.py +3 -2
- varanus/search/base.py +159 -0
- varanus/search/fields.py +189 -0
- varanus/search/templates/search/daterange.html +11 -0
- varanus/search/templates/search/filter.html +1 -0
- varanus/search/templates/search/hidden.html +12 -0
- varanus/search/templates/search/multifacet.html +6 -0
- varanus/search/templates/search/search.html +23 -0
- varanus/search/utils.py +38 -0
- varanus/server/admin.py +40 -14
- varanus/server/context_processors.py +6 -4
- varanus/server/migrations/0001_initial.py +157 -60
- varanus/server/models.py +87 -16
- varanus/server/router.py +3 -0
- varanus/server/settings.py +32 -0
- varanus/server/templates/base.html +2 -2
- varanus/server/templates/dashboard.html +7 -0
- varanus/server/templates/site/base.html +40 -32
- varanus/server/templates/site/details/error.html +1 -1
- varanus/server/templates/site/details/metric.html +4 -4
- varanus/server/templates/site/details/node_env.html +24 -0
- varanus/server/templates/site/details/{node.html → node_packages.html} +1 -1
- varanus/server/templates/site/details/node_settings.html +24 -0
- varanus/server/templates/site/details/query.html +17 -0
- varanus/server/templates/site/details/request.html +27 -17
- varanus/server/templates/site/errors.html +18 -4
- varanus/server/templates/site/logs.html +20 -5
- varanus/server/templates/site/metrics.html +17 -5
- varanus/server/templates/site/overview.html +10 -6
- varanus/server/templates/site/queries.html +38 -10
- varanus/server/templates/site/requests.html +30 -8
- varanus/server/templatetags/varanus.py +7 -0
- varanus/server/urls.py +19 -2
- varanus/server/utils.py +8 -0
- varanus/server/views/api.py +20 -12
- varanus/server/views/dashboard.py +21 -2
- varanus/server/views/site.py +119 -30
- {varanus-0.1.0.dev2.dist-info → varanus-0.1.0.dev4.dist-info}/METADATA +1 -1
- varanus-0.1.0.dev4.dist-info/RECORD +73 -0
- {varanus-0.1.0.dev2.dist-info → varanus-0.1.0.dev4.dist-info}/WHEEL +1 -1
- varanus/server/migrations/0002_context_node_error_node_log_node_metric_node_and_more.py +0 -79
- varanus/server/migrations/0003_alter_log_level.py +0 -28
- varanus/server/search/base.py +0 -118
- varanus/server/search/date.py +0 -33
- varanus/server/search/facet.py +0 -63
- varanus/server/templates/search/daterange.html +0 -10
- varanus/server/templates/search/multifacet.html +0 -11
- varanus-0.1.0.dev2.dist-info/RECORD +0 -69
- {varanus-0.1.0.dev2.dist-info → varanus-0.1.0.dev4.dist-info}/entry_points.txt +0 -0
varanus/client/apps.py
CHANGED
varanus/client/client.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import os
|
|
1
2
|
import platform
|
|
2
3
|
from importlib.metadata import distributions
|
|
3
|
-
from typing import
|
|
4
|
+
from typing import Iterable
|
|
4
5
|
from urllib.parse import urlsplit
|
|
5
6
|
|
|
6
7
|
from varanus.events import Context, NodeInfo
|
|
@@ -19,64 +20,96 @@ def install_query_logger(logger):
|
|
|
19
20
|
return handler
|
|
20
21
|
|
|
21
22
|
|
|
23
|
+
def resolve_include_exclude(
|
|
24
|
+
items: Iterable[str],
|
|
25
|
+
include: Iterable[str] | bool,
|
|
26
|
+
exclude: Iterable[str] | None,
|
|
27
|
+
) -> set[str]:
|
|
28
|
+
if not include:
|
|
29
|
+
return set()
|
|
30
|
+
|
|
31
|
+
if include is True:
|
|
32
|
+
resolved = set(items)
|
|
33
|
+
else:
|
|
34
|
+
resolved = set(items).intersection(include)
|
|
35
|
+
|
|
36
|
+
if exclude is not None:
|
|
37
|
+
resolved.difference_update(exclude)
|
|
38
|
+
|
|
39
|
+
return resolved
|
|
40
|
+
|
|
41
|
+
|
|
22
42
|
class VaranusClient:
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
request_attr: str
|
|
27
|
-
logger_name: str
|
|
28
|
-
tags: dict
|
|
29
|
-
|
|
30
|
-
include_headers: Union[list, bool, None]
|
|
31
|
-
exclude_headers: list | None
|
|
32
|
-
sensitive_headers = set(
|
|
33
|
-
[
|
|
34
|
-
"authorization",
|
|
35
|
-
"cookie",
|
|
36
|
-
"proxy-authorization",
|
|
37
|
-
]
|
|
38
|
-
)
|
|
43
|
+
sensitive_headers = set(["authorization", "cookie", "proxy-authorization"])
|
|
44
|
+
sensitive_settings = set(["DATABASES"])
|
|
45
|
+
sensitive_env = set(["PGPASSWORD"])
|
|
39
46
|
|
|
40
47
|
scheme_transports = {
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"https": "varanus.client.transport.http.HttpTransport",
|
|
48
|
+
"http": "varanus.client.transport.http.ThreadedHttpTransport",
|
|
49
|
+
"https": "varanus.client.transport.http.ThreadedHttpTransport",
|
|
44
50
|
"db": "varanus.client.transport.database.ModelTransport",
|
|
45
51
|
}
|
|
46
52
|
|
|
47
|
-
send_all: bool
|
|
48
53
|
configured = False
|
|
49
54
|
|
|
50
55
|
def setup(
|
|
51
56
|
self,
|
|
52
|
-
dsn,
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
transport_class=None,
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
dsn: str,
|
|
58
|
+
environment: str,
|
|
59
|
+
node: str | None = None,
|
|
60
|
+
transport_class: str | type[BaseTransport] | None = None,
|
|
61
|
+
request_attr: str = "varanus",
|
|
62
|
+
logger_name: str = "varanus.request",
|
|
63
|
+
tags: dict | None = None,
|
|
64
|
+
include_headers: Iterable[str] | bool = False,
|
|
65
|
+
exclude_headers: Iterable[str] | None = None,
|
|
66
|
+
include_settings: Iterable[str] | bool = False,
|
|
67
|
+
exclude_settings: Iterable[str] | None = None,
|
|
68
|
+
include_default_settings: bool = False,
|
|
69
|
+
include_env: Iterable[str] | bool = False,
|
|
70
|
+
exclude_env: Iterable[str] | None = None,
|
|
60
71
|
log_queries: bool | int = False,
|
|
61
|
-
log_query_params=False,
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
72
|
+
log_query_params: bool = False,
|
|
73
|
+
log_query_stack: bool = False,
|
|
74
|
+
query_metrics: bool | str = False,
|
|
75
|
+
send_all: bool = False,
|
|
76
|
+
install: list | None = None,
|
|
65
77
|
):
|
|
66
78
|
url = urlsplit(dsn)
|
|
67
79
|
self.environment = environment
|
|
80
|
+
self.node = node or platform.node()
|
|
68
81
|
if transport_class is None:
|
|
69
|
-
|
|
70
|
-
if transport_class is None:
|
|
82
|
+
if url.scheme not in self.scheme_transports:
|
|
71
83
|
raise ValueError(f"No transport class found for `{url.scheme}`")
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
84
|
+
resolved_class = import_string(self.scheme_transports[url.scheme])
|
|
85
|
+
elif isinstance(transport_class, str):
|
|
86
|
+
resolved_class = import_string(transport_class)
|
|
87
|
+
else:
|
|
88
|
+
resolved_class = transport_class
|
|
89
|
+
if not issubclass(resolved_class, BaseTransport):
|
|
90
|
+
raise ValueError(
|
|
91
|
+
f"Transport class `{transport_class}` must be a subclass of"
|
|
92
|
+
"BaseTransport."
|
|
93
|
+
)
|
|
94
|
+
self.transport = resolved_class(url, self.environment, self.node)
|
|
75
95
|
self.request_attr = request_attr
|
|
76
96
|
self.logger_name = logger_name
|
|
77
97
|
self.tags = tags or {}
|
|
78
98
|
self.include_headers = include_headers
|
|
79
|
-
self.exclude_headers =
|
|
99
|
+
self.exclude_headers = (
|
|
100
|
+
self.sensitive_headers if exclude_headers is None else set(exclude_headers)
|
|
101
|
+
)
|
|
102
|
+
self.include_settings = include_settings
|
|
103
|
+
self.exclude_settings = (
|
|
104
|
+
self.sensitive_settings
|
|
105
|
+
if exclude_settings is None
|
|
106
|
+
else set(exclude_settings)
|
|
107
|
+
)
|
|
108
|
+
self.include_default_settings = include_default_settings
|
|
109
|
+
self.include_env = include_env
|
|
110
|
+
self.exclude_env = (
|
|
111
|
+
self.sensitive_env if exclude_env is None else set(exclude_env)
|
|
112
|
+
)
|
|
80
113
|
if log_queries or query_metrics:
|
|
81
114
|
try:
|
|
82
115
|
# The logger is installed as early as possible, and for all connections.
|
|
@@ -86,6 +119,7 @@ class VaranusClient:
|
|
|
86
119
|
self.query_logger = QueryLogger(
|
|
87
120
|
log_queries,
|
|
88
121
|
log_query_params,
|
|
122
|
+
log_query_stack,
|
|
89
123
|
query_metrics,
|
|
90
124
|
)
|
|
91
125
|
# Install it in each new connection (if it's not already installed).
|
|
@@ -98,10 +132,6 @@ class VaranusClient:
|
|
|
98
132
|
self.send_all = send_all
|
|
99
133
|
self.configured = True
|
|
100
134
|
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
135
|
if "django.contrib.auth.middleware.AuthenticationMiddleware" in install:
|
|
106
136
|
idx = install.index(
|
|
107
137
|
"django.contrib.auth.middleware.AuthenticationMiddleware"
|
|
@@ -119,16 +149,39 @@ class VaranusClient:
|
|
|
119
149
|
self.transport.send(e)
|
|
120
150
|
|
|
121
151
|
def ping(self):
|
|
152
|
+
from django import get_version
|
|
153
|
+
from django.conf import settings
|
|
154
|
+
|
|
155
|
+
include_settings = resolve_include_exclude(
|
|
156
|
+
[s for s in dir(settings) if s == s.upper()],
|
|
157
|
+
self.include_settings,
|
|
158
|
+
self.exclude_settings,
|
|
159
|
+
)
|
|
160
|
+
include_env = resolve_include_exclude(
|
|
161
|
+
os.environ.keys(),
|
|
162
|
+
self.include_env,
|
|
163
|
+
self.exclude_env,
|
|
164
|
+
)
|
|
165
|
+
|
|
122
166
|
self.transport.ping(
|
|
123
167
|
NodeInfo(
|
|
124
|
-
name=
|
|
168
|
+
name=self.node,
|
|
125
169
|
platform=platform.platform(),
|
|
126
|
-
|
|
170
|
+
language=platform.python_implementation(),
|
|
171
|
+
language_version=platform.python_version(),
|
|
172
|
+
framework="Django",
|
|
173
|
+
framework_version=get_version(),
|
|
127
174
|
packages={d.name: d.version for d in distributions()},
|
|
175
|
+
settings={
|
|
176
|
+
s: repr(getattr(settings, s))
|
|
177
|
+
for s in include_settings
|
|
178
|
+
if self.include_default_settings or settings.is_overridden(s)
|
|
179
|
+
},
|
|
180
|
+
environment={e: os.environ[e] for e in include_env},
|
|
128
181
|
)
|
|
129
182
|
)
|
|
130
183
|
|
|
131
|
-
def log(self, level, message, *args, **kwargs):
|
|
184
|
+
def log(self, level: int, message: str, *args, **kwargs):
|
|
132
185
|
if ctx := current_context.get():
|
|
133
186
|
kwargs.setdefault("stacklevel", 2)
|
|
134
187
|
ctx.log(level, message, *args, **kwargs)
|
|
@@ -137,7 +190,7 @@ class VaranusClient:
|
|
|
137
190
|
if ctx := current_context.get():
|
|
138
191
|
ctx.raw_exception(exception, tags=tags)
|
|
139
192
|
|
|
140
|
-
def metric(self, name, value: float = 0.0, tags: dict | None = None):
|
|
193
|
+
def metric(self, name: str, value: float = 0.0, tags: dict | None = None):
|
|
141
194
|
if ctx := current_context.get():
|
|
142
195
|
ctx.metric(name, value, tags=tags)
|
|
143
196
|
|
varanus/client/context.py
CHANGED
|
@@ -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)
|
varanus/client/loggers.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import sys
|
|
3
3
|
|
|
4
|
-
from ..events import Log, Query, now
|
|
4
|
+
from ..events import Log, Query, capture_stack, now
|
|
5
5
|
from .context import ONE_MS, current_context
|
|
6
6
|
|
|
7
7
|
|
|
@@ -12,7 +12,7 @@ class VaranusHandler(logging.Handler):
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class QueryLogger:
|
|
15
|
-
def __init__(self, threshold, log_params, metrics):
|
|
15
|
+
def __init__(self, threshold, log_params, log_stack, metrics):
|
|
16
16
|
# TODO: add callback for tagging?
|
|
17
17
|
if threshold is True:
|
|
18
18
|
self.threshold = 0
|
|
@@ -21,6 +21,7 @@ class QueryLogger:
|
|
|
21
21
|
else:
|
|
22
22
|
self.threshold = int(threshold)
|
|
23
23
|
self.log_params = log_params
|
|
24
|
+
self.log_stack = log_stack
|
|
24
25
|
if isinstance(metrics, str):
|
|
25
26
|
self.metrics_name = metrics
|
|
26
27
|
else:
|
|
@@ -55,5 +56,6 @@ class QueryLogger:
|
|
|
55
56
|
db=context["connection"].alias,
|
|
56
57
|
elapsed_ms=elapsed_ms,
|
|
57
58
|
success=success,
|
|
59
|
+
stack=capture_stack(1) if self.log_stack else [],
|
|
58
60
|
)
|
|
59
61
|
)
|
varanus/client/middleware.py
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
import
|
|
1
|
+
import ipaddress
|
|
2
2
|
|
|
3
3
|
from django.core.exceptions import MiddlewareNotUsed
|
|
4
4
|
from django.http import HttpRequest, HttpResponse
|
|
5
5
|
|
|
6
6
|
from ..events import Request
|
|
7
|
-
from .client import client
|
|
8
|
-
|
|
9
|
-
IP_REGEX = re.compile(r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}")
|
|
7
|
+
from .client import client, resolve_include_exclude
|
|
10
8
|
|
|
11
9
|
|
|
12
10
|
def get_ip(request: HttpRequest):
|
|
@@ -15,24 +13,21 @@ 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):
|
|
24
24
|
headers = {}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if client.exclude_headers is None:
|
|
32
|
-
exclude = client.sensitive_headers
|
|
33
|
-
else:
|
|
34
|
-
exclude = set(name.lower() for name in client.exclude_headers)
|
|
35
|
-
for name in sorted(include - exclude):
|
|
25
|
+
include_headers = resolve_include_exclude(
|
|
26
|
+
[name.lower() for name in request.headers],
|
|
27
|
+
client.include_headers,
|
|
28
|
+
client.exclude_headers,
|
|
29
|
+
)
|
|
30
|
+
for name in include_headers:
|
|
36
31
|
value = request.headers.get(name)
|
|
37
32
|
if value is not None:
|
|
38
33
|
headers[name] = value
|
|
@@ -59,8 +54,9 @@ class VaranusMiddleware:
|
|
|
59
54
|
# TODO: any need for request tags separate from context tags?
|
|
60
55
|
varanus.request = Request(
|
|
61
56
|
host=request.get_host(),
|
|
62
|
-
path=request.path,
|
|
63
57
|
method=request.method or "",
|
|
58
|
+
path=request.path,
|
|
59
|
+
query=request.META.get("QUERY_STRING", ""),
|
|
64
60
|
status=response.status_code,
|
|
65
61
|
headers=request_headers(request),
|
|
66
62
|
size=(
|
varanus/client/transport/base.py
CHANGED
|
@@ -1,28 +1,55 @@
|
|
|
1
|
+
import typing
|
|
1
2
|
import uuid
|
|
2
3
|
from urllib.parse import SplitResult
|
|
3
4
|
|
|
4
|
-
from django.db import transaction
|
|
5
|
-
|
|
6
5
|
from varanus import events
|
|
7
|
-
from varanus.server import models
|
|
8
6
|
|
|
9
7
|
from .base import BaseTransport
|
|
10
8
|
|
|
9
|
+
if typing.TYPE_CHECKING:
|
|
10
|
+
from varanus.server.models import Node, Site
|
|
11
|
+
|
|
11
12
|
|
|
12
13
|
class ModelTransport(BaseTransport):
|
|
13
|
-
|
|
14
|
+
site: "Site"
|
|
15
|
+
node: "Node"
|
|
16
|
+
|
|
17
|
+
def __init__(self, url: SplitResult, environment: str, node: str):
|
|
18
|
+
self.slug = url.netloc
|
|
14
19
|
self.environment = environment
|
|
15
20
|
|
|
21
|
+
def ensure_site(self):
|
|
22
|
+
if hasattr(self, "site"):
|
|
23
|
+
return
|
|
24
|
+
|
|
25
|
+
from varanus.server.models import Site
|
|
26
|
+
|
|
27
|
+
self.site, created = Site.objects.get_or_create(
|
|
28
|
+
slug=self.slug,
|
|
29
|
+
defaults={
|
|
30
|
+
"name": self.slug,
|
|
31
|
+
"schema_name": self.slug,
|
|
32
|
+
},
|
|
33
|
+
)
|
|
34
|
+
|
|
16
35
|
def ping(self, info: events.NodeInfo):
|
|
17
|
-
|
|
18
|
-
|
|
36
|
+
from varanus.server.models import Node
|
|
37
|
+
|
|
38
|
+
self.ensure_site()
|
|
39
|
+
|
|
40
|
+
with self.site.activated():
|
|
41
|
+
self.node, created, updates = Node.update(
|
|
42
|
+
info, site=self.site, environment=self.environment
|
|
43
|
+
)
|
|
19
44
|
|
|
20
|
-
@transaction.atomic
|
|
21
45
|
def send(self, event: events.Context):
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
46
|
+
from varanus.server.models import Context
|
|
47
|
+
|
|
48
|
+
with self.site.activated():
|
|
49
|
+
Context.from_event(
|
|
50
|
+
event,
|
|
51
|
+
event_id=uuid.uuid4(),
|
|
52
|
+
site=self.site,
|
|
53
|
+
environment=self.environment,
|
|
54
|
+
node=self.node,
|
|
55
|
+
)
|
varanus/client/transport/http.py
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import
|
|
1
|
+
import logging
|
|
2
|
+
import queue
|
|
3
|
+
import threading
|
|
4
|
+
import time
|
|
2
5
|
from typing import Any
|
|
3
|
-
from urllib.parse import SplitResult
|
|
6
|
+
from urllib.parse import SplitResult, parse_qs
|
|
4
7
|
|
|
5
8
|
import httpx
|
|
6
9
|
import msgspec
|
|
@@ -9,29 +12,72 @@ from varanus import events
|
|
|
9
12
|
|
|
10
13
|
from .base import BaseTransport
|
|
11
14
|
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
12
17
|
|
|
13
18
|
class HttpTransport(BaseTransport):
|
|
14
|
-
def __init__(self, url: SplitResult, environment: str):
|
|
19
|
+
def __init__(self, url: SplitResult, environment: str, node: str):
|
|
15
20
|
path = url.path.rstrip("/")
|
|
16
21
|
self.ping_url = f"{url.scheme}://{url.netloc}{path}/api/ping/"
|
|
17
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])
|
|
18
27
|
self.client = httpx.Client(
|
|
19
28
|
headers={
|
|
20
29
|
"X-Varanus-Key": url.username or "",
|
|
21
|
-
"X-Varanus-Environment": environment
|
|
22
|
-
"X-Varanus-Node":
|
|
30
|
+
"X-Varanus-Environment": environment,
|
|
31
|
+
"X-Varanus-Node": node,
|
|
23
32
|
},
|
|
24
|
-
timeout=
|
|
33
|
+
timeout=timeout,
|
|
25
34
|
)
|
|
26
35
|
|
|
27
36
|
def request(self, url: str, obj: Any):
|
|
28
37
|
try:
|
|
29
38
|
self.client.post(url, content=msgspec.json.encode(obj))
|
|
30
|
-
except Exception
|
|
31
|
-
|
|
39
|
+
except Exception:
|
|
40
|
+
logger.exception("error sending to %s", url)
|
|
32
41
|
|
|
33
42
|
def ping(self, info: events.NodeInfo):
|
|
34
43
|
self.request(self.ping_url, info)
|
|
35
44
|
|
|
36
45
|
def send(self, event: events.Context):
|
|
37
|
-
self.request(self.event_url, event)
|
|
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)
|
varanus/events.py
CHANGED
|
@@ -17,11 +17,16 @@ class Event(Struct, kw_only=True, omit_defaults=True):
|
|
|
17
17
|
class NodeInfo(Struct):
|
|
18
18
|
name: str
|
|
19
19
|
platform: str
|
|
20
|
-
|
|
20
|
+
language: str
|
|
21
|
+
language_version: str
|
|
22
|
+
framework: str
|
|
23
|
+
framework_version: str
|
|
21
24
|
packages: dict[str, str]
|
|
25
|
+
settings: dict[str, str]
|
|
26
|
+
environment: dict[str, str]
|
|
22
27
|
|
|
23
28
|
|
|
24
|
-
class
|
|
29
|
+
class StackLine(Struct):
|
|
25
30
|
file: str | None
|
|
26
31
|
lineno: int | None
|
|
27
32
|
function: str | None
|
|
@@ -30,11 +35,31 @@ class ErrorLine(Struct):
|
|
|
30
35
|
locals: dict[str, str]
|
|
31
36
|
|
|
32
37
|
|
|
38
|
+
def capture_stack(skip: int = 0, include_locals: bool = False) -> list[StackLine]:
|
|
39
|
+
lines = []
|
|
40
|
+
for frame in inspect.stack()[skip + 1 :]:
|
|
41
|
+
lines.append(
|
|
42
|
+
StackLine(
|
|
43
|
+
file=frame.filename,
|
|
44
|
+
lineno=frame.lineno,
|
|
45
|
+
linesrc=frame.code_context[0].strip() if frame.code_context else "",
|
|
46
|
+
function=frame.function,
|
|
47
|
+
module=frame.frame.f_globals.get("__name__", ""),
|
|
48
|
+
locals=(
|
|
49
|
+
{name: repr(val) for name, val in frame.frame.f_locals.items()}
|
|
50
|
+
if include_locals
|
|
51
|
+
else {}
|
|
52
|
+
),
|
|
53
|
+
)
|
|
54
|
+
)
|
|
55
|
+
return lines
|
|
56
|
+
|
|
57
|
+
|
|
33
58
|
class Error(Event):
|
|
34
59
|
kind: str
|
|
35
60
|
module: str
|
|
36
61
|
message: str
|
|
37
|
-
|
|
62
|
+
stack: list[StackLine] = []
|
|
38
63
|
|
|
39
64
|
@classmethod
|
|
40
65
|
def from_exception(cls, exc_info, tags=None):
|
|
@@ -63,7 +88,7 @@ class Error(Event):
|
|
|
63
88
|
pass
|
|
64
89
|
module = f_globals.get("__name__", "")
|
|
65
90
|
lines.append(
|
|
66
|
-
|
|
91
|
+
StackLine(
|
|
67
92
|
file=abs_path,
|
|
68
93
|
lineno=lineno,
|
|
69
94
|
function=function,
|
|
@@ -78,7 +103,7 @@ class Error(Event):
|
|
|
78
103
|
kind=kind,
|
|
79
104
|
module=module,
|
|
80
105
|
message=message,
|
|
81
|
-
|
|
106
|
+
stack=lines,
|
|
82
107
|
)
|
|
83
108
|
|
|
84
109
|
|
|
@@ -144,12 +169,14 @@ class Query(Event):
|
|
|
144
169
|
db: str
|
|
145
170
|
elapsed_ms: int
|
|
146
171
|
success: bool
|
|
172
|
+
stack: list[StackLine] = []
|
|
147
173
|
|
|
148
174
|
|
|
149
175
|
class Request(Event):
|
|
150
176
|
host: str
|
|
151
|
-
path: str
|
|
152
177
|
method: str
|
|
178
|
+
path: str
|
|
179
|
+
query: str
|
|
153
180
|
status: int
|
|
154
181
|
headers: dict = {}
|
|
155
182
|
size: int | None = None
|