varanus 0.1.0.dev7__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.
Files changed (88) hide show
  1. varanus-0.1.0.dev7/PKG-INFO +19 -0
  2. varanus-0.1.0.dev7/README.md +3 -0
  3. varanus-0.1.0.dev7/pyproject.toml +38 -0
  4. varanus-0.1.0.dev7/src/varanus/__init__.py +6 -0
  5. varanus-0.1.0.dev7/src/varanus/client/__init__.py +7 -0
  6. varanus-0.1.0.dev7/src/varanus/client/apps.py +8 -0
  7. varanus-0.1.0.dev7/src/varanus/client/client.py +223 -0
  8. varanus-0.1.0.dev7/src/varanus/client/context.py +156 -0
  9. varanus-0.1.0.dev7/src/varanus/client/loggers.py +60 -0
  10. varanus-0.1.0.dev7/src/varanus/client/middleware.py +84 -0
  11. varanus-0.1.0.dev7/src/varanus/client/transport/__init__.py +0 -0
  12. varanus-0.1.0.dev7/src/varanus/client/transport/base.py +14 -0
  13. varanus-0.1.0.dev7/src/varanus/client/transport/database.py +55 -0
  14. varanus-0.1.0.dev7/src/varanus/client/transport/http.py +83 -0
  15. varanus-0.1.0.dev7/src/varanus/events.py +197 -0
  16. varanus-0.1.0.dev7/src/varanus/search/__init__.py +10 -0
  17. varanus-0.1.0.dev7/src/varanus/search/base.py +159 -0
  18. varanus-0.1.0.dev7/src/varanus/search/fields.py +189 -0
  19. varanus-0.1.0.dev7/src/varanus/search/templates/search/daterange.html +11 -0
  20. varanus-0.1.0.dev7/src/varanus/search/templates/search/filter.html +1 -0
  21. varanus-0.1.0.dev7/src/varanus/search/templates/search/hidden.html +12 -0
  22. varanus-0.1.0.dev7/src/varanus/search/templates/search/multifacet.html +6 -0
  23. varanus-0.1.0.dev7/src/varanus/search/templates/search/search.html +23 -0
  24. varanus-0.1.0.dev7/src/varanus/search/utils.py +38 -0
  25. varanus-0.1.0.dev7/src/varanus/server/__init__.py +0 -0
  26. varanus-0.1.0.dev7/src/varanus/server/__main__.py +19 -0
  27. varanus-0.1.0.dev7/src/varanus/server/admin.py +188 -0
  28. varanus-0.1.0.dev7/src/varanus/server/apps.py +10 -0
  29. varanus-0.1.0.dev7/src/varanus/server/asgi.py +7 -0
  30. varanus-0.1.0.dev7/src/varanus/server/context_processors.py +9 -0
  31. varanus-0.1.0.dev7/src/varanus/server/integrations/__init__.py +3 -0
  32. varanus-0.1.0.dev7/src/varanus/server/integrations/base.py +38 -0
  33. varanus-0.1.0.dev7/src/varanus/server/integrations/squish.py +74 -0
  34. varanus-0.1.0.dev7/src/varanus/server/management/__init__.py +0 -0
  35. varanus-0.1.0.dev7/src/varanus/server/management/commands/__init__.py +0 -0
  36. varanus-0.1.0.dev7/src/varanus/server/management/commands/migrateall.py +20 -0
  37. varanus-0.1.0.dev7/src/varanus/server/management/commands/serve.py +64 -0
  38. varanus-0.1.0.dev7/src/varanus/server/middleware.py +0 -0
  39. varanus-0.1.0.dev7/src/varanus/server/migrations/0001_initial.py +702 -0
  40. varanus-0.1.0.dev7/src/varanus/server/migrations/__init__.py +0 -0
  41. varanus-0.1.0.dev7/src/varanus/server/models.py +728 -0
  42. varanus-0.1.0.dev7/src/varanus/server/router.py +72 -0
  43. varanus-0.1.0.dev7/src/varanus/server/settings.py +215 -0
  44. varanus-0.1.0.dev7/src/varanus/server/static/css/varanus.css +4 -0
  45. varanus-0.1.0.dev7/src/varanus/server/static/js/varanus.js +0 -0
  46. varanus-0.1.0.dev7/src/varanus/server/tasks.py +64 -0
  47. varanus-0.1.0.dev7/src/varanus/server/templates/base.html +77 -0
  48. varanus-0.1.0.dev7/src/varanus/server/templates/dashboard.html +41 -0
  49. varanus-0.1.0.dev7/src/varanus/server/templates/registration/login.html +31 -0
  50. varanus-0.1.0.dev7/src/varanus/server/templates/site/base.html +65 -0
  51. varanus-0.1.0.dev7/src/varanus/server/templates/site/details/environment_nodes.html +6 -0
  52. varanus-0.1.0.dev7/src/varanus/server/templates/site/details/error.html +47 -0
  53. varanus-0.1.0.dev7/src/varanus/server/templates/site/details/log.html +34 -0
  54. varanus-0.1.0.dev7/src/varanus/server/templates/site/details/metric.html +38 -0
  55. varanus-0.1.0.dev7/src/varanus/server/templates/site/details/node_env.html +24 -0
  56. varanus-0.1.0.dev7/src/varanus/server/templates/site/details/node_environments.html +6 -0
  57. varanus-0.1.0.dev7/src/varanus/server/templates/site/details/node_packages.html +44 -0
  58. varanus-0.1.0.dev7/src/varanus/server/templates/site/details/node_settings.html +24 -0
  59. varanus-0.1.0.dev7/src/varanus/server/templates/site/details/query.html +46 -0
  60. varanus-0.1.0.dev7/src/varanus/server/templates/site/details/request.html +150 -0
  61. varanus-0.1.0.dev7/src/varanus/server/templates/site/errors.html +44 -0
  62. varanus-0.1.0.dev7/src/varanus/server/templates/site/logs.html +47 -0
  63. varanus-0.1.0.dev7/src/varanus/server/templates/site/metrics.html +48 -0
  64. varanus-0.1.0.dev7/src/varanus/server/templates/site/overview.html +60 -0
  65. varanus-0.1.0.dev7/src/varanus/server/templates/site/queries.html +64 -0
  66. varanus-0.1.0.dev7/src/varanus/server/templates/site/requests.html +54 -0
  67. varanus-0.1.0.dev7/src/varanus/server/templatetags/__init__.py +0 -0
  68. varanus-0.1.0.dev7/src/varanus/server/templatetags/varanus.py +20 -0
  69. varanus-0.1.0.dev7/src/varanus/server/urls.py +94 -0
  70. varanus-0.1.0.dev7/src/varanus/server/utils.py +8 -0
  71. varanus-0.1.0.dev7/src/varanus/server/views/__init__.py +0 -0
  72. varanus-0.1.0.dev7/src/varanus/server/views/api.py +59 -0
  73. varanus-0.1.0.dev7/src/varanus/server/views/base.py +42 -0
  74. varanus-0.1.0.dev7/src/varanus/server/views/dashboard.py +25 -0
  75. varanus-0.1.0.dev7/src/varanus/server/views/site.py +254 -0
  76. varanus-0.1.0.dev7/src/varanus/server/wsgi.py +7 -0
  77. varanus-0.1.0.dev7/src/varanus/tasks/__init__.py +0 -0
  78. varanus-0.1.0.dev7/src/varanus/tasks/admin.py +15 -0
  79. varanus-0.1.0.dev7/src/varanus/tasks/apps.py +0 -0
  80. varanus-0.1.0.dev7/src/varanus/tasks/backend.py +62 -0
  81. varanus-0.1.0.dev7/src/varanus/tasks/management/__init__.py +0 -0
  82. varanus-0.1.0.dev7/src/varanus/tasks/management/commands/__init__.py +0 -0
  83. varanus-0.1.0.dev7/src/varanus/tasks/management/commands/tasker.py +20 -0
  84. varanus-0.1.0.dev7/src/varanus/tasks/migrations/0001_initial.py +66 -0
  85. varanus-0.1.0.dev7/src/varanus/tasks/migrations/__init__.py +0 -0
  86. varanus-0.1.0.dev7/src/varanus/tasks/models.py +131 -0
  87. varanus-0.1.0.dev7/src/varanus/tasks/runner.py +107 -0
  88. varanus-0.1.0.dev7/src/varanus/utils.py +28 -0
@@ -0,0 +1,19 @@
1
+ Metadata-Version: 2.3
2
+ Name: varanus
3
+ Version: 0.1.0.dev7
4
+ Summary: Django application monitoring.
5
+ Requires-Dist: httpx>=0.27.0
6
+ Requires-Dist: msgspec>=0.19.0
7
+ Requires-Dist: cconf>=1.0.0 ; extra == 'server'
8
+ Requires-Dist: django>=6.0 ; python_full_version >= '3.12' and extra == 'server'
9
+ Requires-Dist: django-passkey-auth>=0.2.0 ; extra == 'server'
10
+ Requires-Dist: granian[reload]>=2.4.2 ; extra == 'server'
11
+ Requires-Dist: psycopg[binary]>=3.2.1 ; extra == 'server'
12
+ Requires-Dist: whitenoise>=6.7.0 ; extra == 'server'
13
+ Requires-Python: >=3.11
14
+ Provides-Extra: server
15
+ Description-Content-Type: text/markdown
16
+
17
+ * Single database mode
18
+ * Multiple schema mode
19
+ * Local mode
@@ -0,0 +1,3 @@
1
+ * Single database mode
2
+ * Multiple schema mode
3
+ * Local mode
@@ -0,0 +1,38 @@
1
+ [project]
2
+ name = "varanus"
3
+ version = "0.1.0.dev7"
4
+ description = "Django application monitoring."
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ dependencies = [
8
+ "httpx>=0.27.0",
9
+ "msgspec>=0.19.0",
10
+ ]
11
+
12
+ [project.optional-dependencies]
13
+ server = [
14
+ "cconf>=1.0.0",
15
+ "django>=6.0; python_version>='3.12'",
16
+ "django-passkey-auth>=0.2.0",
17
+ "granian[reload]>=2.4.2",
18
+ "psycopg[binary]>=3.2.1",
19
+ "whitenoise>=6.7.0",
20
+ ]
21
+
22
+ [dependency-groups]
23
+ dev = []
24
+
25
+ [project.scripts]
26
+ manage = "varanus.server.__main__:manage"
27
+
28
+ [build-system]
29
+ requires = ["uv_build>=0.9.2,<0.10.0"]
30
+ build-backend = "uv_build"
31
+
32
+ [tool.ruff.lint]
33
+ extend-select = ["I"]
34
+ isort.known-first-party = ["varanus"]
35
+
36
+ [tool.pytest.ini_options]
37
+ addopts = "--tb=short -s"
38
+ DJANGO_SETTINGS_MODULE = "varanus.server.settings"
@@ -0,0 +1,6 @@
1
+ import importlib.metadata
2
+
3
+ __version__ = importlib.metadata.version("varanus")
4
+ __version_info__ = tuple(
5
+ int(num) if num.isdigit() else num for num in __version__.split(".")
6
+ )
@@ -0,0 +1,7 @@
1
+ from .client import client
2
+
3
+ setup = client.setup
4
+ context = client.context
5
+ log = client.log
6
+ metric = client.metric
7
+ timer = client.timer
@@ -0,0 +1,8 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class VaranusClient(AppConfig):
5
+ name = "varanus.client"
6
+
7
+ def ready(self):
8
+ pass
@@ -0,0 +1,223 @@
1
+ import os
2
+ import platform
3
+ from importlib.metadata import distributions
4
+ from typing import Iterable
5
+ from urllib.parse import urlsplit
6
+
7
+ from varanus.events import Context, NodeInfo
8
+
9
+ from ..utils import import_string
10
+ from .context import VaranusContext, current_context
11
+ from .loggers import QueryLogger
12
+ from .transport.base import BaseTransport
13
+
14
+
15
+ def install_query_logger(logger):
16
+ def handler(sender, **kwargs):
17
+ if logger not in kwargs["connection"].execute_wrappers:
18
+ kwargs["connection"].execute_wrappers.append(logger)
19
+
20
+ return handler
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
+
42
+ class VaranusClient:
43
+ # Things we don't want to send, at least by default.
44
+ sensitive_headers = set(["authorization", "cookie", "proxy-authorization"])
45
+ sensitive_settings = set(["SECRET_KEY"])
46
+ sensitive_env = set(["PGPASSWORD"])
47
+
48
+ scheme_transports = {
49
+ "http": "varanus.client.transport.http.ThreadedHttpTransport",
50
+ "https": "varanus.client.transport.http.ThreadedHttpTransport",
51
+ "db": "varanus.client.transport.database.ModelTransport",
52
+ }
53
+
54
+ configured = False
55
+
56
+ def setup(
57
+ self,
58
+ dsn: str,
59
+ environment: str,
60
+ node: str | None = None,
61
+ transport_class: str | type[BaseTransport] | None = None,
62
+ request_attr: str = "varanus",
63
+ logger_name: str = "varanus.request",
64
+ tags: dict | None = None,
65
+ include_headers: Iterable[str] | bool = False,
66
+ exclude_headers: Iterable[str] | None = None,
67
+ include_settings: Iterable[str] | bool = True,
68
+ exclude_settings: Iterable[str] | None = None,
69
+ include_default_settings: bool = False,
70
+ filter_settings: bool = True,
71
+ include_env: Iterable[str] | bool = False,
72
+ exclude_env: Iterable[str] | None = None,
73
+ log_queries: bool | int = False,
74
+ log_query_params: bool = False,
75
+ log_query_stack: bool = False,
76
+ query_metrics: bool | str = False,
77
+ send_all: bool = False,
78
+ install: list | None = None,
79
+ ):
80
+ url = urlsplit(dsn)
81
+ self.environment = environment
82
+ self.node = node or platform.node()
83
+ if transport_class is None:
84
+ if url.scheme not in self.scheme_transports:
85
+ raise ValueError(f"No transport class found for `{url.scheme}`")
86
+ resolved_class = import_string(self.scheme_transports[url.scheme])
87
+ elif isinstance(transport_class, str):
88
+ resolved_class = import_string(transport_class)
89
+ else:
90
+ resolved_class = transport_class
91
+ if not issubclass(resolved_class, BaseTransport):
92
+ raise ValueError(
93
+ f"Transport class `{transport_class}` must be a subclass of"
94
+ "BaseTransport."
95
+ )
96
+ self.transport = resolved_class(url, self.environment, self.node)
97
+ self.request_attr = request_attr
98
+ self.logger_name = logger_name
99
+ self.tags = tags or {}
100
+ self.include_headers = include_headers
101
+ self.exclude_headers = (
102
+ self.sensitive_headers if exclude_headers is None else set(exclude_headers)
103
+ )
104
+ self.include_settings = include_settings
105
+ self.exclude_settings = (
106
+ self.sensitive_settings
107
+ if exclude_settings is None
108
+ else set(exclude_settings)
109
+ )
110
+ self.include_default_settings = include_default_settings
111
+ self.filter_settings = filter_settings
112
+ self.include_env = include_env
113
+ self.exclude_env = (
114
+ self.sensitive_env if exclude_env is None else set(exclude_env)
115
+ )
116
+ if log_queries or query_metrics:
117
+ try:
118
+ # The logger is installed as early as possible, and for all connections.
119
+ from django.db.backends.signals import connection_created
120
+
121
+ # Create a single QueryLogger to be used by all connections.
122
+ self.query_logger = QueryLogger(
123
+ log_queries,
124
+ log_query_params,
125
+ log_query_stack,
126
+ query_metrics,
127
+ )
128
+ # Install it in each new connection (if it's not already installed).
129
+ connection_created.connect(
130
+ install_query_logger(self.query_logger),
131
+ weak=False,
132
+ )
133
+ except ImportError:
134
+ pass
135
+ self.send_all = send_all
136
+ self.configured = True
137
+ if install:
138
+ if "django.contrib.auth.middleware.AuthenticationMiddleware" in install:
139
+ idx = install.index(
140
+ "django.contrib.auth.middleware.AuthenticationMiddleware"
141
+ )
142
+ install.insert(idx + 1, "varanus.client.middleware.VaranusMiddleware")
143
+ elif "django.middleware.common.CommonMiddleware" in install:
144
+ idx = install.index("django.middleware.common.CommonMiddleware")
145
+ install.insert(idx + 1, "varanus.client.middleware.VaranusMiddleware")
146
+ else:
147
+ install.append("varanus.client.middleware.VaranusMiddleware")
148
+ return self
149
+
150
+ def send(self, *events: Context):
151
+ for e in events:
152
+ self.transport.send(e)
153
+
154
+ def ping(self):
155
+ from django import get_version
156
+ from django.conf import settings
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
+
170
+ include_settings = resolve_include_exclude(
171
+ settings_dict.keys(),
172
+ self.include_settings,
173
+ self.exclude_settings,
174
+ )
175
+ include_env = resolve_include_exclude(
176
+ os.environ.keys(),
177
+ self.include_env,
178
+ self.exclude_env,
179
+ )
180
+
181
+ self.transport.ping(
182
+ NodeInfo(
183
+ name=self.node,
184
+ platform=platform.platform(),
185
+ language=platform.python_implementation(),
186
+ language_version=platform.python_version(),
187
+ framework="Django",
188
+ framework_version=get_version(),
189
+ packages={d.name: d.version for d in distributions()},
190
+ settings={
191
+ s: settings_dict[s]
192
+ for s in include_settings
193
+ if self.include_default_settings or settings.is_overridden(s)
194
+ },
195
+ environment={e: os.environ[e] for e in include_env},
196
+ )
197
+ )
198
+
199
+ def log(self, level: int, message: str, *args, **kwargs):
200
+ if ctx := current_context.get():
201
+ kwargs.setdefault("stacklevel", 2)
202
+ ctx.log(level, message, *args, **kwargs)
203
+
204
+ def raw_exception(self, exception, tags: dict | None = None):
205
+ if ctx := current_context.get():
206
+ ctx.raw_exception(exception, tags=tags)
207
+
208
+ def metric(self, name: str, value: float = 0.0, tags: dict | None = None):
209
+ if ctx := current_context.get():
210
+ ctx.metric(name, value, tags=tags)
211
+
212
+ def timer(self, name: str, tags: dict | None = None):
213
+ if ctx := current_context.get():
214
+ return ctx.timer(name, tags=tags)
215
+
216
+ def context(self, name: str, tags: dict | None = None):
217
+ if ctx := current_context.get():
218
+ return ctx.context(name, tags)
219
+ else:
220
+ return VaranusContext(self, name, tags or self.tags)
221
+
222
+
223
+ 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
@@ -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,55 @@
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
+ ingest.enqueue(
51
+ self.site.pk,
52
+ self.node_name,
53
+ self.environment,
54
+ msgspec.json.encode(event).decode(),
55
+ )