varanus 0.1.0__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/.DS_Store +0 -0
- varanus/__init__.py +6 -0
- varanus/client/.DS_Store +0 -0
- varanus/client/__init__.py +7 -0
- varanus/client/apps.py +8 -0
- varanus/client/client.py +231 -0
- varanus/client/context.py +156 -0
- varanus/client/loggers.py +60 -0
- varanus/client/middleware.py +84 -0
- varanus/client/transport/.DS_Store +0 -0
- varanus/client/transport/__init__.py +0 -0
- varanus/client/transport/base.py +14 -0
- varanus/client/transport/database.py +57 -0
- varanus/client/transport/http.py +83 -0
- varanus/events.py +198 -0
- varanus/search/.DS_Store +0 -0
- varanus/search/__init__.py +10 -0
- varanus/search/base.py +159 -0
- varanus/search/fields.py +189 -0
- varanus/search/templates/search/.DS_Store +0 -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/.DS_Store +0 -0
- varanus/server/__init__.py +0 -0
- varanus/server/__main__.py +19 -0
- varanus/server/admin.py +218 -0
- varanus/server/apps.py +10 -0
- varanus/server/asgi.py +7 -0
- varanus/server/context_processors.py +9 -0
- varanus/server/integrations/__init__.py +3 -0
- varanus/server/integrations/base.py +38 -0
- varanus/server/integrations/squish.py +74 -0
- varanus/server/management/__init__.py +0 -0
- varanus/server/management/commands/.DS_Store +0 -0
- varanus/server/management/commands/__init__.py +0 -0
- varanus/server/management/commands/maintenance.py +21 -0
- varanus/server/management/commands/migrateall.py +20 -0
- varanus/server/middleware.py +0 -0
- varanus/server/migrations/0001_initial.py +702 -0
- varanus/server/migrations/0002_drop_scheduledtask.py +13 -0
- varanus/server/migrations/0003_site_retention.py +21 -0
- varanus/server/migrations/0004_node_version.py +17 -0
- varanus/server/migrations/__init__.py +0 -0
- varanus/server/models.py +744 -0
- varanus/server/router.py +72 -0
- varanus/server/settings.py +234 -0
- varanus/server/static/.DS_Store +0 -0
- varanus/server/static/css/.DS_Store +0 -0
- varanus/server/static/css/varanus.css +4 -0
- varanus/server/static/js/.DS_Store +0 -0
- varanus/server/static/js/varanus.js +0 -0
- varanus/server/tasks.py +73 -0
- varanus/server/templates/.DS_Store +0 -0
- varanus/server/templates/base.html +77 -0
- varanus/server/templates/dashboard.html +43 -0
- varanus/server/templates/registration/login.html +31 -0
- varanus/server/templates/site/.DS_Store +0 -0
- varanus/server/templates/site/base.html +65 -0
- varanus/server/templates/site/details/environment_nodes.html +6 -0
- varanus/server/templates/site/details/error.html +47 -0
- varanus/server/templates/site/details/log.html +34 -0
- varanus/server/templates/site/details/metric.html +38 -0
- varanus/server/templates/site/details/node_env.html +24 -0
- varanus/server/templates/site/details/node_environments.html +6 -0
- varanus/server/templates/site/details/node_packages.html +44 -0
- varanus/server/templates/site/details/node_settings.html +24 -0
- varanus/server/templates/site/details/query.html +46 -0
- varanus/server/templates/site/details/request.html +150 -0
- varanus/server/templates/site/errors.html +44 -0
- varanus/server/templates/site/logs.html +47 -0
- varanus/server/templates/site/metrics.html +48 -0
- varanus/server/templates/site/overview.html +62 -0
- varanus/server/templates/site/queries.html +64 -0
- varanus/server/templates/site/requests.html +54 -0
- varanus/server/templatetags/__init__.py +0 -0
- varanus/server/templatetags/varanus.py +20 -0
- varanus/server/urls.py +94 -0
- varanus/server/utils.py +8 -0
- varanus/server/views/.DS_Store +0 -0
- varanus/server/views/__init__.py +0 -0
- varanus/server/views/api.py +59 -0
- varanus/server/views/base.py +42 -0
- varanus/server/views/dashboard.py +25 -0
- varanus/server/views/site.py +254 -0
- varanus/server/wsgi.py +7 -0
- varanus/utils.py +28 -0
- varanus/version.py +6 -0
- varanus-0.1.0.dist-info/METADATA +46 -0
- varanus-0.1.0.dist-info/RECORD +94 -0
- varanus-0.1.0.dist-info/WHEEL +4 -0
varanus/.DS_Store
ADDED
|
Binary file
|
varanus/__init__.py
ADDED
varanus/client/.DS_Store
ADDED
|
Binary file
|
varanus/client/apps.py
ADDED
varanus/client/client.py
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import platform
|
|
4
|
+
import warnings
|
|
5
|
+
from importlib.metadata import distributions
|
|
6
|
+
from typing import Iterable
|
|
7
|
+
from urllib.parse import urlsplit
|
|
8
|
+
|
|
9
|
+
import varanus
|
|
10
|
+
from varanus.events import Context, NodeInfo
|
|
11
|
+
|
|
12
|
+
from ..utils import import_string
|
|
13
|
+
from .context import VaranusContext, current_context
|
|
14
|
+
from .loggers import QueryLogger
|
|
15
|
+
from .transport.base import BaseTransport
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def install_query_logger(logger):
|
|
19
|
+
def handler(sender, **kwargs):
|
|
20
|
+
if logger not in kwargs["connection"].execute_wrappers:
|
|
21
|
+
kwargs["connection"].execute_wrappers.append(logger)
|
|
22
|
+
|
|
23
|
+
return handler
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def resolve_include_exclude(
|
|
27
|
+
items: Iterable[str],
|
|
28
|
+
include: Iterable[str] | bool,
|
|
29
|
+
exclude: Iterable[str] | None,
|
|
30
|
+
) -> set[str]:
|
|
31
|
+
if not include:
|
|
32
|
+
return set()
|
|
33
|
+
|
|
34
|
+
if include is True:
|
|
35
|
+
resolved = set(items)
|
|
36
|
+
else:
|
|
37
|
+
resolved = set(items).intersection(include)
|
|
38
|
+
|
|
39
|
+
if exclude is not None:
|
|
40
|
+
resolved.difference_update(exclude)
|
|
41
|
+
|
|
42
|
+
return resolved
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class VaranusClient:
|
|
46
|
+
# Things we don't want to send, at least by default.
|
|
47
|
+
sensitive_headers = set(["authorization", "cookie", "proxy-authorization"])
|
|
48
|
+
sensitive_settings = set(["SECRET_KEY"])
|
|
49
|
+
sensitive_env = set(["PGPASSWORD"])
|
|
50
|
+
|
|
51
|
+
scheme_transports = {
|
|
52
|
+
"http": "varanus.client.transport.http.ThreadedHttpTransport",
|
|
53
|
+
"https": "varanus.client.transport.http.ThreadedHttpTransport",
|
|
54
|
+
"db": "varanus.client.transport.database.ModelTransport",
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
configured = False
|
|
58
|
+
|
|
59
|
+
def setup(
|
|
60
|
+
self,
|
|
61
|
+
dsn: str,
|
|
62
|
+
environment: str,
|
|
63
|
+
node: str | None = None,
|
|
64
|
+
transport_class: str | type[BaseTransport] | None = None,
|
|
65
|
+
request_attr: str = "varanus",
|
|
66
|
+
logger_name: str = "varanus.request",
|
|
67
|
+
log_warnings: bool = True,
|
|
68
|
+
tags: dict | None = None,
|
|
69
|
+
include_headers: Iterable[str] | bool = False,
|
|
70
|
+
exclude_headers: Iterable[str] | None = None,
|
|
71
|
+
include_settings: Iterable[str] | bool = True,
|
|
72
|
+
exclude_settings: Iterable[str] | None = None,
|
|
73
|
+
include_default_settings: bool = False,
|
|
74
|
+
filter_settings: bool = True,
|
|
75
|
+
include_env: Iterable[str] | bool = False,
|
|
76
|
+
exclude_env: Iterable[str] | None = None,
|
|
77
|
+
log_queries: bool | int = False,
|
|
78
|
+
log_query_params: bool = False,
|
|
79
|
+
log_query_stack: bool = False,
|
|
80
|
+
query_metrics: bool | str = False,
|
|
81
|
+
send_all: bool = False,
|
|
82
|
+
install: list | None = None,
|
|
83
|
+
):
|
|
84
|
+
url = urlsplit(dsn)
|
|
85
|
+
self.environment = environment
|
|
86
|
+
self.node = node or platform.node()
|
|
87
|
+
if transport_class is None:
|
|
88
|
+
if url.scheme not in self.scheme_transports:
|
|
89
|
+
raise ValueError(f"No transport class found for `{url.scheme}`")
|
|
90
|
+
resolved_class = import_string(self.scheme_transports[url.scheme])
|
|
91
|
+
elif isinstance(transport_class, str):
|
|
92
|
+
resolved_class = import_string(transport_class)
|
|
93
|
+
else:
|
|
94
|
+
resolved_class = transport_class
|
|
95
|
+
if not issubclass(resolved_class, BaseTransport):
|
|
96
|
+
raise ValueError(
|
|
97
|
+
f"Transport class `{transport_class}` must be a subclass of"
|
|
98
|
+
"BaseTransport."
|
|
99
|
+
)
|
|
100
|
+
self.transport = resolved_class(url, self.environment, self.node)
|
|
101
|
+
self.request_attr = request_attr
|
|
102
|
+
self.logger_name = logger_name
|
|
103
|
+
self.tags = tags or {}
|
|
104
|
+
self.include_headers = include_headers
|
|
105
|
+
self.exclude_headers = (
|
|
106
|
+
self.sensitive_headers if exclude_headers is None else set(exclude_headers)
|
|
107
|
+
)
|
|
108
|
+
self.include_settings = include_settings
|
|
109
|
+
self.exclude_settings = (
|
|
110
|
+
self.sensitive_settings
|
|
111
|
+
if exclude_settings is None
|
|
112
|
+
else set(exclude_settings)
|
|
113
|
+
)
|
|
114
|
+
self.include_default_settings = include_default_settings
|
|
115
|
+
self.filter_settings = filter_settings
|
|
116
|
+
self.include_env = include_env
|
|
117
|
+
self.exclude_env = (
|
|
118
|
+
self.sensitive_env if exclude_env is None else set(exclude_env)
|
|
119
|
+
)
|
|
120
|
+
if log_queries or query_metrics:
|
|
121
|
+
try:
|
|
122
|
+
# The logger is installed as early as possible, and for all connections.
|
|
123
|
+
from django.db.backends.signals import connection_created
|
|
124
|
+
|
|
125
|
+
# Create a single QueryLogger to be used by all connections.
|
|
126
|
+
self.query_logger = QueryLogger(
|
|
127
|
+
log_queries,
|
|
128
|
+
log_query_params,
|
|
129
|
+
log_query_stack,
|
|
130
|
+
query_metrics,
|
|
131
|
+
)
|
|
132
|
+
# Install it in each new connection (if it's not already installed).
|
|
133
|
+
connection_created.connect(
|
|
134
|
+
install_query_logger(self.query_logger),
|
|
135
|
+
weak=False,
|
|
136
|
+
)
|
|
137
|
+
except ImportError:
|
|
138
|
+
pass
|
|
139
|
+
self.send_all = send_all
|
|
140
|
+
if log_warnings:
|
|
141
|
+
warnings.simplefilter("default")
|
|
142
|
+
logging.captureWarnings(True)
|
|
143
|
+
self.configured = True
|
|
144
|
+
if install:
|
|
145
|
+
if "django.contrib.auth.middleware.AuthenticationMiddleware" in install:
|
|
146
|
+
idx = install.index(
|
|
147
|
+
"django.contrib.auth.middleware.AuthenticationMiddleware"
|
|
148
|
+
)
|
|
149
|
+
install.insert(idx + 1, "varanus.client.middleware.VaranusMiddleware")
|
|
150
|
+
elif "django.middleware.common.CommonMiddleware" in install:
|
|
151
|
+
idx = install.index("django.middleware.common.CommonMiddleware")
|
|
152
|
+
install.insert(idx + 1, "varanus.client.middleware.VaranusMiddleware")
|
|
153
|
+
else:
|
|
154
|
+
install.append("varanus.client.middleware.VaranusMiddleware")
|
|
155
|
+
return self
|
|
156
|
+
|
|
157
|
+
def send(self, *events: Context):
|
|
158
|
+
for e in events:
|
|
159
|
+
self.transport.send(e)
|
|
160
|
+
|
|
161
|
+
def ping(self):
|
|
162
|
+
from django import get_version
|
|
163
|
+
from django.conf import settings
|
|
164
|
+
|
|
165
|
+
if self.filter_settings:
|
|
166
|
+
from django.views.debug import SafeExceptionReporterFilter
|
|
167
|
+
|
|
168
|
+
settings_dict = {
|
|
169
|
+
k: repr(v)
|
|
170
|
+
for k, v in SafeExceptionReporterFilter().get_safe_settings().items()
|
|
171
|
+
}
|
|
172
|
+
else:
|
|
173
|
+
settings_dict = {
|
|
174
|
+
s: repr(getattr(settings, s)) for s in dir(settings) if s.isupper()
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
include_settings = resolve_include_exclude(
|
|
178
|
+
settings_dict.keys(),
|
|
179
|
+
self.include_settings,
|
|
180
|
+
self.exclude_settings,
|
|
181
|
+
)
|
|
182
|
+
include_env = resolve_include_exclude(
|
|
183
|
+
os.environ.keys(),
|
|
184
|
+
self.include_env,
|
|
185
|
+
self.exclude_env,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
self.transport.ping(
|
|
189
|
+
NodeInfo(
|
|
190
|
+
name=self.node,
|
|
191
|
+
version=varanus.__version__,
|
|
192
|
+
platform=platform.platform(),
|
|
193
|
+
language=platform.python_implementation(),
|
|
194
|
+
language_version=platform.python_version(),
|
|
195
|
+
framework="Django",
|
|
196
|
+
framework_version=get_version(),
|
|
197
|
+
packages={d.name: d.version for d in distributions()},
|
|
198
|
+
settings={
|
|
199
|
+
s: settings_dict[s]
|
|
200
|
+
for s in include_settings
|
|
201
|
+
if self.include_default_settings or settings.is_overridden(s)
|
|
202
|
+
},
|
|
203
|
+
environment={e: os.environ[e] for e in include_env},
|
|
204
|
+
)
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
def log(self, level: int, message: str, *args, **kwargs):
|
|
208
|
+
if ctx := current_context.get():
|
|
209
|
+
kwargs.setdefault("stacklevel", 2)
|
|
210
|
+
ctx.log(level, message, *args, **kwargs)
|
|
211
|
+
|
|
212
|
+
def raw_exception(self, exception, tags: dict | None = None):
|
|
213
|
+
if ctx := current_context.get():
|
|
214
|
+
ctx.raw_exception(exception, tags=tags)
|
|
215
|
+
|
|
216
|
+
def metric(self, name: str, value: float = 0.0, tags: dict | None = None):
|
|
217
|
+
if ctx := current_context.get():
|
|
218
|
+
ctx.metric(name, value, tags=tags)
|
|
219
|
+
|
|
220
|
+
def timer(self, name: str, tags: dict | None = None):
|
|
221
|
+
if ctx := current_context.get():
|
|
222
|
+
return ctx.timer(name, tags=tags)
|
|
223
|
+
|
|
224
|
+
def context(self, name: str, tags: dict | None = None):
|
|
225
|
+
if ctx := current_context.get():
|
|
226
|
+
return ctx.context(name, tags)
|
|
227
|
+
else:
|
|
228
|
+
return VaranusContext(self, name, tags or self.tags)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
client = VaranusClient()
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
import inspect
|
|
3
|
+
import logging
|
|
4
|
+
import sys
|
|
5
|
+
import time
|
|
6
|
+
from contextvars import ContextVar, Token
|
|
7
|
+
from datetime import timedelta
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
from ..events import Context, Error, Log, Metric, Request, now
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from .client import VaranusClient
|
|
14
|
+
|
|
15
|
+
ONE_MS = timedelta(milliseconds=1)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class VaranusContext:
|
|
19
|
+
"""
|
|
20
|
+
Shared public interface for how clients add logs/errors/metrics/etc. to Varanus.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
request: Request | None
|
|
24
|
+
|
|
25
|
+
def __init__(self, client: "VaranusClient", name, tags: dict | None = None):
|
|
26
|
+
self.client = client
|
|
27
|
+
self.name = name
|
|
28
|
+
self.logs = []
|
|
29
|
+
self.errors = []
|
|
30
|
+
self.metrics: dict[str, Metric] = {}
|
|
31
|
+
self.queries = []
|
|
32
|
+
self.subcontexts = []
|
|
33
|
+
self.tags = tags or {}
|
|
34
|
+
self.request = None
|
|
35
|
+
|
|
36
|
+
def build(self):
|
|
37
|
+
return Context(
|
|
38
|
+
timestamp=self.started,
|
|
39
|
+
tags=self.tags,
|
|
40
|
+
name=self.name,
|
|
41
|
+
elapsed_ms=self.elapsed_ms,
|
|
42
|
+
request=self.request,
|
|
43
|
+
logs=self.logs,
|
|
44
|
+
errors=self.errors,
|
|
45
|
+
metrics=list(self.metrics.values()),
|
|
46
|
+
queries=self.queries,
|
|
47
|
+
subcontexts=[ctx.build() for ctx in self.subcontexts],
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
def should_send(self):
|
|
51
|
+
if self.client.send_all:
|
|
52
|
+
return True
|
|
53
|
+
if self.logs or self.errors or self.metrics or self.queries:
|
|
54
|
+
return True
|
|
55
|
+
for ctx in self.subcontexts:
|
|
56
|
+
if ctx.should_send():
|
|
57
|
+
return True
|
|
58
|
+
return False
|
|
59
|
+
|
|
60
|
+
def __enter__(self):
|
|
61
|
+
self.token = current_context.set(self)
|
|
62
|
+
self.started = now()
|
|
63
|
+
return self
|
|
64
|
+
|
|
65
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
|
66
|
+
self.raw_exception((exc_type, exc_value, traceback))
|
|
67
|
+
self.elapsed_ms = (now() - self.started) // ONE_MS
|
|
68
|
+
current_context.reset(self.token)
|
|
69
|
+
if self.token.old_value is Token.MISSING and self.should_send():
|
|
70
|
+
# We're the bottom of the stack, build and send the event.
|
|
71
|
+
try:
|
|
72
|
+
self.client.send(self.build())
|
|
73
|
+
except Exception as e:
|
|
74
|
+
import traceback
|
|
75
|
+
|
|
76
|
+
traceback.print_exception(e)
|
|
77
|
+
|
|
78
|
+
def __setitem__(self, name, value):
|
|
79
|
+
self.tags[name] = value
|
|
80
|
+
|
|
81
|
+
def log(
|
|
82
|
+
self,
|
|
83
|
+
level: int,
|
|
84
|
+
message: str,
|
|
85
|
+
*args,
|
|
86
|
+
exc_info=None,
|
|
87
|
+
stacklevel: int = 1,
|
|
88
|
+
tags: dict | None = None,
|
|
89
|
+
**kwargs,
|
|
90
|
+
):
|
|
91
|
+
frame = inspect.stack()[stacklevel]
|
|
92
|
+
self.logs.append(
|
|
93
|
+
Log(
|
|
94
|
+
tags=tags or {},
|
|
95
|
+
message=message % args,
|
|
96
|
+
name=self.client.logger_name,
|
|
97
|
+
level=level,
|
|
98
|
+
file=frame.filename,
|
|
99
|
+
lineno=frame.lineno,
|
|
100
|
+
error=Error.from_exception(exc_info),
|
|
101
|
+
)
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
def debug(self, message: str, *args, **kwargs):
|
|
105
|
+
kwargs.setdefault("stacklevel", 2)
|
|
106
|
+
self.log(logging.DEBUG, message, *args, **kwargs)
|
|
107
|
+
|
|
108
|
+
def info(self, message: str, *args, **kwargs):
|
|
109
|
+
kwargs.setdefault("stacklevel", 2)
|
|
110
|
+
self.log(logging.INFO, message, *args, **kwargs)
|
|
111
|
+
|
|
112
|
+
def warning(self, message: str, *args, **kwargs):
|
|
113
|
+
kwargs.setdefault("stacklevel", 2)
|
|
114
|
+
self.log(logging.WARNING, message, *args, **kwargs)
|
|
115
|
+
|
|
116
|
+
def error(self, message: str, *args, **kwargs):
|
|
117
|
+
kwargs.setdefault("stacklevel", 2)
|
|
118
|
+
self.log(logging.ERROR, message, *args, **kwargs)
|
|
119
|
+
|
|
120
|
+
def critical(self, message: str, *args, **kwargs):
|
|
121
|
+
kwargs.setdefault("stacklevel", 2)
|
|
122
|
+
self.log(logging.CRITICAL, message, *args, **kwargs)
|
|
123
|
+
|
|
124
|
+
def exception(self, message: str, *args, **kwargs):
|
|
125
|
+
kwargs.setdefault("stacklevel", 2)
|
|
126
|
+
kwargs.setdefault("exc_info", sys.exc_info())
|
|
127
|
+
self.log(logging.ERROR, message, *args, **kwargs)
|
|
128
|
+
|
|
129
|
+
def raw_exception(self, exception, tags: dict | None = None):
|
|
130
|
+
if err := Error.from_exception(exception, tags=tags):
|
|
131
|
+
self.errors.append(err)
|
|
132
|
+
|
|
133
|
+
def context(self, name: str = "", tags: dict | None = None):
|
|
134
|
+
ctx = VaranusContext(self.client, name, tags)
|
|
135
|
+
self.subcontexts.append(ctx)
|
|
136
|
+
return ctx
|
|
137
|
+
|
|
138
|
+
def metric(self, name: str, value: float = 0.0, tags: dict | None = None):
|
|
139
|
+
if name not in self.metrics:
|
|
140
|
+
self.metrics[name] = Metric(name=name, tags=tags or {})
|
|
141
|
+
self.metrics[name].update(value)
|
|
142
|
+
|
|
143
|
+
@contextlib.contextmanager
|
|
144
|
+
def timer(self, name: str, tags: dict | None = None):
|
|
145
|
+
start = time.monotonic()
|
|
146
|
+
try:
|
|
147
|
+
yield self
|
|
148
|
+
finally:
|
|
149
|
+
elapsed = time.monotonic() - start
|
|
150
|
+
self.metric(name, elapsed, tags=tags)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
current_context: ContextVar[VaranusContext | None] = ContextVar(
|
|
154
|
+
"current_context",
|
|
155
|
+
default=None,
|
|
156
|
+
)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
from ..events import Log, Query, capture_stack, now
|
|
5
|
+
from .context import ONE_MS, current_context
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class VaranusHandler(logging.Handler):
|
|
9
|
+
def emit(self, record: logging.LogRecord):
|
|
10
|
+
if ctx := current_context.get():
|
|
11
|
+
ctx.logs.append(Log.from_logrecord(record, tags=ctx.tags))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class QueryLogger:
|
|
15
|
+
def __init__(self, threshold, log_params, log_stack, metrics):
|
|
16
|
+
# TODO: add callback for tagging?
|
|
17
|
+
if threshold is True:
|
|
18
|
+
self.threshold = 0
|
|
19
|
+
elif threshold is False:
|
|
20
|
+
self.threshold = sys.maxsize
|
|
21
|
+
else:
|
|
22
|
+
self.threshold = int(threshold)
|
|
23
|
+
self.log_params = log_params
|
|
24
|
+
self.log_stack = log_stack
|
|
25
|
+
if isinstance(metrics, str):
|
|
26
|
+
self.metrics_name = metrics
|
|
27
|
+
else:
|
|
28
|
+
self.metrics_name = "queries" if metrics else None
|
|
29
|
+
|
|
30
|
+
def __call__(self, execute, sql, params, many, context):
|
|
31
|
+
start = now()
|
|
32
|
+
success = True
|
|
33
|
+
try:
|
|
34
|
+
result = execute(sql, params, many, context)
|
|
35
|
+
except Exception:
|
|
36
|
+
success = False
|
|
37
|
+
raise
|
|
38
|
+
else:
|
|
39
|
+
return result
|
|
40
|
+
finally:
|
|
41
|
+
if ctx := current_context.get():
|
|
42
|
+
elapsed_ms = (now() - start) // ONE_MS
|
|
43
|
+
if self.metrics_name:
|
|
44
|
+
ctx.metric(self.metrics_name, elapsed_ms)
|
|
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
|
+
)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import ipaddress
|
|
2
|
+
|
|
3
|
+
from django.core.exceptions import MiddlewareNotUsed
|
|
4
|
+
from django.http import HttpRequest, HttpResponse
|
|
5
|
+
from django.utils import timezone
|
|
6
|
+
|
|
7
|
+
from ..events import Request
|
|
8
|
+
from .client import client, resolve_include_exclude
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_ip(request: HttpRequest):
|
|
12
|
+
ip_address = request.META.get("HTTP_X_FORWARDED_FOR", "").strip()
|
|
13
|
+
if ip_address:
|
|
14
|
+
ip_address = ip_address.split(",")[0].strip()
|
|
15
|
+
if not ip_address:
|
|
16
|
+
ip_address = request.META.get("REMOTE_ADDR", "127.0.0.1").strip()
|
|
17
|
+
try:
|
|
18
|
+
# Validate and normalize the IP address.
|
|
19
|
+
return str(ipaddress.ip_address(ip_address))
|
|
20
|
+
except ValueError:
|
|
21
|
+
return ""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def request_headers(request: HttpRequest):
|
|
25
|
+
headers = {}
|
|
26
|
+
include_headers = resolve_include_exclude(
|
|
27
|
+
[name.lower() for name in request.headers],
|
|
28
|
+
client.include_headers,
|
|
29
|
+
client.exclude_headers,
|
|
30
|
+
)
|
|
31
|
+
for name in include_headers:
|
|
32
|
+
value = request.headers.get(name)
|
|
33
|
+
if value is not None:
|
|
34
|
+
headers[name] = value
|
|
35
|
+
return headers
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class VaranusMiddleware:
|
|
39
|
+
def __init__(self, get_response):
|
|
40
|
+
if not client.configured:
|
|
41
|
+
# TODO: warning
|
|
42
|
+
print("VaranusClient is not configured -- disabling middleware.")
|
|
43
|
+
raise MiddlewareNotUsed()
|
|
44
|
+
self.last_ping = None
|
|
45
|
+
self.get_response = get_response
|
|
46
|
+
|
|
47
|
+
def process_exception(self, request, exception):
|
|
48
|
+
# Any value in using request.varanus instead of current context here?
|
|
49
|
+
client.raw_exception(exception)
|
|
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
|
+
|
|
59
|
+
with client.context(request.path) as varanus:
|
|
60
|
+
setattr(request, client.request_attr, varanus)
|
|
61
|
+
response = self.get_response(request)
|
|
62
|
+
# TODO: any need for request tags separate from context tags?
|
|
63
|
+
varanus.request = Request(
|
|
64
|
+
host=request.get_host(),
|
|
65
|
+
method=request.method or "",
|
|
66
|
+
path=request.path,
|
|
67
|
+
query=request.META.get("QUERY_STRING", ""),
|
|
68
|
+
status=response.status_code,
|
|
69
|
+
headers=request_headers(request),
|
|
70
|
+
size=(
|
|
71
|
+
len(response.content)
|
|
72
|
+
if isinstance(response, HttpResponse)
|
|
73
|
+
else None
|
|
74
|
+
),
|
|
75
|
+
ip=get_ip(request),
|
|
76
|
+
user=(
|
|
77
|
+
request.user.get_username()
|
|
78
|
+
if hasattr(request, "user")
|
|
79
|
+
and request.user
|
|
80
|
+
and request.user.is_authenticated
|
|
81
|
+
else None
|
|
82
|
+
),
|
|
83
|
+
)
|
|
84
|
+
return response
|
|
Binary file
|
|
File without changes
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from urllib.parse import SplitResult
|
|
2
|
+
|
|
3
|
+
from varanus import events
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class BaseTransport:
|
|
7
|
+
def __init__(self, url: SplitResult, environment: str, node: str):
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
def ping(self, info: events.NodeInfo):
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
def send(self, event: events.Context):
|
|
14
|
+
pass
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
from urllib.parse import SplitResult
|
|
3
|
+
|
|
4
|
+
import msgspec
|
|
5
|
+
|
|
6
|
+
from varanus import events
|
|
7
|
+
|
|
8
|
+
from .base import BaseTransport
|
|
9
|
+
|
|
10
|
+
if typing.TYPE_CHECKING:
|
|
11
|
+
from varanus.server.models import Node, Site
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ModelTransport(BaseTransport):
|
|
15
|
+
site: "Site"
|
|
16
|
+
node: "Node"
|
|
17
|
+
|
|
18
|
+
def __init__(self, url: SplitResult, environment: str, node: str):
|
|
19
|
+
self.slug = url.netloc
|
|
20
|
+
self.environment = environment
|
|
21
|
+
self.node_name = node
|
|
22
|
+
|
|
23
|
+
def ensure_site(self):
|
|
24
|
+
if hasattr(self, "site"):
|
|
25
|
+
return
|
|
26
|
+
|
|
27
|
+
from varanus.server.models import Site
|
|
28
|
+
|
|
29
|
+
self.site, created = Site.objects.get_or_create(
|
|
30
|
+
slug=self.slug,
|
|
31
|
+
defaults={
|
|
32
|
+
"name": self.slug,
|
|
33
|
+
"schema_name": self.slug,
|
|
34
|
+
},
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
def ping(self, info: events.NodeInfo):
|
|
38
|
+
from varanus.server.models import Node
|
|
39
|
+
|
|
40
|
+
self.ensure_site()
|
|
41
|
+
|
|
42
|
+
with self.site.activated():
|
|
43
|
+
self.node, created, updates = Node.update(
|
|
44
|
+
info, site=self.site, environment=self.environment
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
def send(self, event: events.Context):
|
|
48
|
+
from varanus.server.tasks import ingest
|
|
49
|
+
|
|
50
|
+
self.ensure_site()
|
|
51
|
+
|
|
52
|
+
ingest.enqueue(
|
|
53
|
+
self.site.pk,
|
|
54
|
+
self.node_name,
|
|
55
|
+
self.environment,
|
|
56
|
+
msgspec.json.encode(event).decode(),
|
|
57
|
+
)
|