varanus 0.1.0__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 (94) hide show
  1. varanus-0.1.0/PKG-INFO +46 -0
  2. varanus-0.1.0/README.md +25 -0
  3. varanus-0.1.0/pyproject.toml +39 -0
  4. varanus-0.1.0/src/varanus/.DS_Store +0 -0
  5. varanus-0.1.0/src/varanus/__init__.py +6 -0
  6. varanus-0.1.0/src/varanus/client/.DS_Store +0 -0
  7. varanus-0.1.0/src/varanus/client/__init__.py +7 -0
  8. varanus-0.1.0/src/varanus/client/apps.py +8 -0
  9. varanus-0.1.0/src/varanus/client/client.py +231 -0
  10. varanus-0.1.0/src/varanus/client/context.py +156 -0
  11. varanus-0.1.0/src/varanus/client/loggers.py +60 -0
  12. varanus-0.1.0/src/varanus/client/middleware.py +84 -0
  13. varanus-0.1.0/src/varanus/client/transport/.DS_Store +0 -0
  14. varanus-0.1.0/src/varanus/client/transport/__init__.py +0 -0
  15. varanus-0.1.0/src/varanus/client/transport/base.py +14 -0
  16. varanus-0.1.0/src/varanus/client/transport/database.py +57 -0
  17. varanus-0.1.0/src/varanus/client/transport/http.py +83 -0
  18. varanus-0.1.0/src/varanus/events.py +198 -0
  19. varanus-0.1.0/src/varanus/search/.DS_Store +0 -0
  20. varanus-0.1.0/src/varanus/search/__init__.py +10 -0
  21. varanus-0.1.0/src/varanus/search/base.py +159 -0
  22. varanus-0.1.0/src/varanus/search/fields.py +189 -0
  23. varanus-0.1.0/src/varanus/search/templates/search/.DS_Store +0 -0
  24. varanus-0.1.0/src/varanus/search/templates/search/daterange.html +11 -0
  25. varanus-0.1.0/src/varanus/search/templates/search/filter.html +1 -0
  26. varanus-0.1.0/src/varanus/search/templates/search/hidden.html +12 -0
  27. varanus-0.1.0/src/varanus/search/templates/search/multifacet.html +6 -0
  28. varanus-0.1.0/src/varanus/search/templates/search/search.html +23 -0
  29. varanus-0.1.0/src/varanus/search/utils.py +38 -0
  30. varanus-0.1.0/src/varanus/server/.DS_Store +0 -0
  31. varanus-0.1.0/src/varanus/server/__init__.py +0 -0
  32. varanus-0.1.0/src/varanus/server/__main__.py +19 -0
  33. varanus-0.1.0/src/varanus/server/admin.py +218 -0
  34. varanus-0.1.0/src/varanus/server/apps.py +10 -0
  35. varanus-0.1.0/src/varanus/server/asgi.py +7 -0
  36. varanus-0.1.0/src/varanus/server/context_processors.py +9 -0
  37. varanus-0.1.0/src/varanus/server/integrations/__init__.py +3 -0
  38. varanus-0.1.0/src/varanus/server/integrations/base.py +38 -0
  39. varanus-0.1.0/src/varanus/server/integrations/squish.py +74 -0
  40. varanus-0.1.0/src/varanus/server/management/__init__.py +0 -0
  41. varanus-0.1.0/src/varanus/server/management/commands/.DS_Store +0 -0
  42. varanus-0.1.0/src/varanus/server/management/commands/__init__.py +0 -0
  43. varanus-0.1.0/src/varanus/server/management/commands/maintenance.py +21 -0
  44. varanus-0.1.0/src/varanus/server/management/commands/migrateall.py +20 -0
  45. varanus-0.1.0/src/varanus/server/middleware.py +0 -0
  46. varanus-0.1.0/src/varanus/server/migrations/0001_initial.py +702 -0
  47. varanus-0.1.0/src/varanus/server/migrations/0002_drop_scheduledtask.py +13 -0
  48. varanus-0.1.0/src/varanus/server/migrations/0003_site_retention.py +21 -0
  49. varanus-0.1.0/src/varanus/server/migrations/0004_node_version.py +17 -0
  50. varanus-0.1.0/src/varanus/server/migrations/__init__.py +0 -0
  51. varanus-0.1.0/src/varanus/server/models.py +744 -0
  52. varanus-0.1.0/src/varanus/server/router.py +72 -0
  53. varanus-0.1.0/src/varanus/server/settings.py +234 -0
  54. varanus-0.1.0/src/varanus/server/static/.DS_Store +0 -0
  55. varanus-0.1.0/src/varanus/server/static/css/.DS_Store +0 -0
  56. varanus-0.1.0/src/varanus/server/static/css/varanus.css +4 -0
  57. varanus-0.1.0/src/varanus/server/static/js/.DS_Store +0 -0
  58. varanus-0.1.0/src/varanus/server/static/js/varanus.js +0 -0
  59. varanus-0.1.0/src/varanus/server/tasks.py +73 -0
  60. varanus-0.1.0/src/varanus/server/templates/.DS_Store +0 -0
  61. varanus-0.1.0/src/varanus/server/templates/base.html +77 -0
  62. varanus-0.1.0/src/varanus/server/templates/dashboard.html +43 -0
  63. varanus-0.1.0/src/varanus/server/templates/registration/login.html +31 -0
  64. varanus-0.1.0/src/varanus/server/templates/site/.DS_Store +0 -0
  65. varanus-0.1.0/src/varanus/server/templates/site/base.html +65 -0
  66. varanus-0.1.0/src/varanus/server/templates/site/details/environment_nodes.html +6 -0
  67. varanus-0.1.0/src/varanus/server/templates/site/details/error.html +47 -0
  68. varanus-0.1.0/src/varanus/server/templates/site/details/log.html +34 -0
  69. varanus-0.1.0/src/varanus/server/templates/site/details/metric.html +38 -0
  70. varanus-0.1.0/src/varanus/server/templates/site/details/node_env.html +24 -0
  71. varanus-0.1.0/src/varanus/server/templates/site/details/node_environments.html +6 -0
  72. varanus-0.1.0/src/varanus/server/templates/site/details/node_packages.html +44 -0
  73. varanus-0.1.0/src/varanus/server/templates/site/details/node_settings.html +24 -0
  74. varanus-0.1.0/src/varanus/server/templates/site/details/query.html +46 -0
  75. varanus-0.1.0/src/varanus/server/templates/site/details/request.html +150 -0
  76. varanus-0.1.0/src/varanus/server/templates/site/errors.html +44 -0
  77. varanus-0.1.0/src/varanus/server/templates/site/logs.html +47 -0
  78. varanus-0.1.0/src/varanus/server/templates/site/metrics.html +48 -0
  79. varanus-0.1.0/src/varanus/server/templates/site/overview.html +62 -0
  80. varanus-0.1.0/src/varanus/server/templates/site/queries.html +64 -0
  81. varanus-0.1.0/src/varanus/server/templates/site/requests.html +54 -0
  82. varanus-0.1.0/src/varanus/server/templatetags/__init__.py +0 -0
  83. varanus-0.1.0/src/varanus/server/templatetags/varanus.py +20 -0
  84. varanus-0.1.0/src/varanus/server/urls.py +94 -0
  85. varanus-0.1.0/src/varanus/server/utils.py +8 -0
  86. varanus-0.1.0/src/varanus/server/views/.DS_Store +0 -0
  87. varanus-0.1.0/src/varanus/server/views/__init__.py +0 -0
  88. varanus-0.1.0/src/varanus/server/views/api.py +59 -0
  89. varanus-0.1.0/src/varanus/server/views/base.py +42 -0
  90. varanus-0.1.0/src/varanus/server/views/dashboard.py +25 -0
  91. varanus-0.1.0/src/varanus/server/views/site.py +254 -0
  92. varanus-0.1.0/src/varanus/server/wsgi.py +7 -0
  93. varanus-0.1.0/src/varanus/utils.py +28 -0
  94. varanus-0.1.0/src/varanus/version.py +6 -0
varanus-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,46 @@
1
+ Metadata-Version: 2.3
2
+ Name: varanus
3
+ Version: 0.1.0
4
+ Summary: Django application monitoring.
5
+ Author: Dan Watson
6
+ Author-email: Dan Watson <watsond@imsweb.com>
7
+ Classifier: Intended Audience :: Developers
8
+ Classifier: Programming Language :: Python
9
+ Classifier: Programming Language :: Python :: 3
10
+ Requires-Dist: httpx>=0.27.0
11
+ Requires-Dist: msgspec>=0.19.0
12
+ Requires-Dist: cconf>=1.0.0 ; extra == 'server'
13
+ Requires-Dist: django>=6.0 ; python_full_version >= '3.12' and extra == 'server'
14
+ Requires-Dist: django-dbtasks[serve]>=0.3.2 ; python_full_version >= '3.12' and extra == 'server'
15
+ Requires-Dist: django-passkey-auth>=0.2.0 ; extra == 'server'
16
+ Requires-Dist: psycopg[binary]>=3.2.1 ; extra == 'server'
17
+ Requires-Dist: whitenoise>=6.7.0 ; extra == 'server'
18
+ Requires-Python: >=3.11
19
+ Provides-Extra: server
20
+ Description-Content-Type: text/markdown
21
+
22
+ # Varanus
23
+
24
+ ## Installation
25
+
26
+ ```
27
+ pip install varanus
28
+ ```
29
+
30
+ ## Quickstart for Django
31
+
32
+ In your `settings.py`:
33
+
34
+ ```python
35
+ try:
36
+ import varanus.client
37
+ varanus.client.setup(
38
+ "https://APIKEY@varanus.example.com",
39
+ environment=os.getenv("VARANUS_ENV", "local"),
40
+ install=MIDDLEWARE,
41
+ )
42
+ except ImportError:
43
+ pass
44
+ ```
45
+
46
+ For more information about how to configure the Varanus client, see [the configuration docs](docs/configuration.md). If nothing else, note that when using uWSGI, you'll need to enable threads with the [enable-threads](https://uwsgi-docs.readthedocs.io/en/latest/Options.html#enable-threads) option.
@@ -0,0 +1,25 @@
1
+ # Varanus
2
+
3
+ ## Installation
4
+
5
+ ```
6
+ pip install varanus
7
+ ```
8
+
9
+ ## Quickstart for Django
10
+
11
+ In your `settings.py`:
12
+
13
+ ```python
14
+ try:
15
+ import varanus.client
16
+ varanus.client.setup(
17
+ "https://APIKEY@varanus.example.com",
18
+ environment=os.getenv("VARANUS_ENV", "local"),
19
+ install=MIDDLEWARE,
20
+ )
21
+ except ImportError:
22
+ pass
23
+ ```
24
+
25
+ For more information about how to configure the Varanus client, see [the configuration docs](docs/configuration.md). If nothing else, note that when using uWSGI, you'll need to enable threads with the [enable-threads](https://uwsgi-docs.readthedocs.io/en/latest/Options.html#enable-threads) option.
@@ -0,0 +1,39 @@
1
+ [project]
2
+ name = "varanus"
3
+ version = "0.1.0"
4
+ description = "Django application monitoring."
5
+ authors = [
6
+ { name = "Dan Watson", email = "watsond@imsweb.com" }
7
+ ]
8
+ readme = "README.md"
9
+ requires-python = ">=3.11"
10
+ dependencies = [
11
+ "httpx>=0.27.0",
12
+ "msgspec>=0.19.0",
13
+ ]
14
+ classifiers = [
15
+ "Intended Audience :: Developers",
16
+ "Programming Language :: Python",
17
+ "Programming Language :: Python :: 3",
18
+ ]
19
+
20
+ [project.optional-dependencies]
21
+ server = [
22
+ "cconf>=1.0.0",
23
+ "django>=6.0; python_version>='3.12'",
24
+ "django-dbtasks[serve]>=0.3.2; python_version>='3.12'",
25
+ "django-passkey-auth>=0.2.0",
26
+ "psycopg[binary]>=3.2.1",
27
+ "whitenoise>=6.7.0",
28
+ ]
29
+
30
+ [dependency-groups]
31
+ dev = []
32
+
33
+ [build-system]
34
+ requires = ["uv_build>=0.9.2,<0.10.0"]
35
+ build-backend = "uv_build"
36
+
37
+ [tool.ruff.lint]
38
+ extend-select = ["I"]
39
+ isort.known-first-party = ["varanus"]
Binary file
@@ -0,0 +1,6 @@
1
+ from .version import __version__, __version_info__
2
+
3
+ __all__ = [
4
+ "__version__",
5
+ "__version_info__",
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,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
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