varanus 0.1.0.dev3__tar.gz → 0.1.0.dev5__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 (107) hide show
  1. {varanus-0.1.0.dev3 → varanus-0.1.0.dev5}/PKG-INFO +3 -4
  2. {varanus-0.1.0.dev3 → varanus-0.1.0.dev5}/pyproject.toml +3 -4
  3. {varanus-0.1.0.dev3 → varanus-0.1.0.dev5}/src/varanus/client/client.py +87 -22
  4. {varanus-0.1.0.dev3 → varanus-0.1.0.dev5}/src/varanus/client/loggers.py +18 -17
  5. {varanus-0.1.0.dev3 → varanus-0.1.0.dev5}/src/varanus/client/middleware.py +18 -14
  6. varanus-0.1.0.dev5/src/varanus/client/transport/database.py +55 -0
  7. {varanus-0.1.0.dev3 → varanus-0.1.0.dev5}/src/varanus/events.py +33 -6
  8. {varanus-0.1.0.dev3/src/varanus/server → varanus-0.1.0.dev5/src/varanus}/search/__init__.py +3 -2
  9. varanus-0.1.0.dev5/src/varanus/search/base.py +159 -0
  10. varanus-0.1.0.dev5/src/varanus/search/fields.py +189 -0
  11. varanus-0.1.0.dev5/src/varanus/search/templates/search/daterange.html +11 -0
  12. varanus-0.1.0.dev5/src/varanus/search/templates/search/filter.html +1 -0
  13. varanus-0.1.0.dev5/src/varanus/search/templates/search/hidden.html +12 -0
  14. varanus-0.1.0.dev5/src/varanus/search/templates/search/multifacet.html +6 -0
  15. varanus-0.1.0.dev5/src/varanus/search/templates/search/search.html +23 -0
  16. varanus-0.1.0.dev5/src/varanus/search/utils.py +38 -0
  17. {varanus-0.1.0.dev3 → varanus-0.1.0.dev5}/src/varanus/server/admin.py +52 -12
  18. {varanus-0.1.0.dev3 → varanus-0.1.0.dev5}/src/varanus/server/apps.py +3 -0
  19. varanus-0.1.0.dev5/src/varanus/server/context_processors.py +9 -0
  20. varanus-0.1.0.dev5/src/varanus/server/integrations/__init__.py +3 -0
  21. varanus-0.1.0.dev5/src/varanus/server/integrations/base.py +38 -0
  22. varanus-0.1.0.dev5/src/varanus/server/integrations/squish.py +74 -0
  23. varanus-0.1.0.dev5/src/varanus/server/management/commands/serve.py +64 -0
  24. {varanus-0.1.0.dev3 → varanus-0.1.0.dev5}/src/varanus/server/migrations/0001_initial.py +246 -60
  25. {varanus-0.1.0.dev3 → varanus-0.1.0.dev5}/src/varanus/server/models.py +175 -22
  26. {varanus-0.1.0.dev3 → varanus-0.1.0.dev5}/src/varanus/server/router.py +4 -0
  27. {varanus-0.1.0.dev3 → varanus-0.1.0.dev5}/src/varanus/server/settings.py +58 -1
  28. varanus-0.1.0.dev5/src/varanus/server/tasks.py +64 -0
  29. {varanus-0.1.0.dev3 → varanus-0.1.0.dev5}/src/varanus/server/templates/base.html +2 -2
  30. varanus-0.1.0.dev5/src/varanus/server/templates/dashboard.html +41 -0
  31. varanus-0.1.0.dev5/src/varanus/server/templates/site/base.html +65 -0
  32. {varanus-0.1.0.dev3 → varanus-0.1.0.dev5}/src/varanus/server/templates/site/details/error.html +10 -12
  33. {varanus-0.1.0.dev3 → varanus-0.1.0.dev5}/src/varanus/server/templates/site/details/log.html +3 -5
  34. {varanus-0.1.0.dev3 → varanus-0.1.0.dev5}/src/varanus/server/templates/site/details/metric.html +7 -9
  35. varanus-0.1.0.dev5/src/varanus/server/templates/site/details/node_env.html +24 -0
  36. varanus-0.1.0.dev3/src/varanus/server/templates/site/details/node.html → varanus-0.1.0.dev5/src/varanus/server/templates/site/details/node_packages.html +1 -1
  37. varanus-0.1.0.dev5/src/varanus/server/templates/site/details/node_settings.html +24 -0
  38. varanus-0.1.0.dev5/src/varanus/server/templates/site/details/query.html +46 -0
  39. {varanus-0.1.0.dev3 → varanus-0.1.0.dev5}/src/varanus/server/templates/site/details/request.html +27 -17
  40. varanus-0.1.0.dev5/src/varanus/server/templates/site/errors.html +44 -0
  41. varanus-0.1.0.dev5/src/varanus/server/templates/site/logs.html +47 -0
  42. {varanus-0.1.0.dev3 → varanus-0.1.0.dev5}/src/varanus/server/templates/site/metrics.html +17 -5
  43. {varanus-0.1.0.dev3 → varanus-0.1.0.dev5}/src/varanus/server/templates/site/overview.html +10 -6
  44. varanus-0.1.0.dev5/src/varanus/server/templates/site/queries.html +64 -0
  45. varanus-0.1.0.dev5/src/varanus/server/templates/site/requests.html +54 -0
  46. {varanus-0.1.0.dev3 → varanus-0.1.0.dev5}/src/varanus/server/templatetags/varanus.py +7 -0
  47. {varanus-0.1.0.dev3 → varanus-0.1.0.dev5}/src/varanus/server/urls.py +29 -2
  48. varanus-0.1.0.dev5/src/varanus/server/utils.py +8 -0
  49. {varanus-0.1.0.dev3 → varanus-0.1.0.dev5}/src/varanus/server/views/api.py +10 -25
  50. varanus-0.1.0.dev5/src/varanus/server/views/dashboard.py +25 -0
  51. varanus-0.1.0.dev5/src/varanus/server/views/site.py +254 -0
  52. varanus-0.1.0.dev5/src/varanus/tasks/__init__.py +0 -0
  53. varanus-0.1.0.dev5/src/varanus/tasks/admin.py +15 -0
  54. varanus-0.1.0.dev5/src/varanus/tasks/apps.py +0 -0
  55. varanus-0.1.0.dev5/src/varanus/tasks/backend.py +62 -0
  56. varanus-0.1.0.dev5/src/varanus/tasks/management/__init__.py +0 -0
  57. varanus-0.1.0.dev5/src/varanus/tasks/management/commands/__init__.py +0 -0
  58. varanus-0.1.0.dev5/src/varanus/tasks/management/commands/tasker.py +20 -0
  59. varanus-0.1.0.dev5/src/varanus/tasks/migrations/0001_initial.py +66 -0
  60. varanus-0.1.0.dev5/src/varanus/tasks/migrations/__init__.py +0 -0
  61. varanus-0.1.0.dev5/src/varanus/tasks/models.py +131 -0
  62. varanus-0.1.0.dev5/src/varanus/tasks/runner.py +107 -0
  63. varanus-0.1.0.dev5/src/varanus/utils.py +21 -0
  64. varanus-0.1.0.dev3/src/varanus/client/transport/database.py +0 -31
  65. varanus-0.1.0.dev3/src/varanus/server/context_processors.py +0 -7
  66. varanus-0.1.0.dev3/src/varanus/server/migrations/0002_context_node_error_node_log_node_metric_node_and_more.py +0 -79
  67. varanus-0.1.0.dev3/src/varanus/server/migrations/0003_alter_log_level.py +0 -28
  68. varanus-0.1.0.dev3/src/varanus/server/search/base.py +0 -116
  69. varanus-0.1.0.dev3/src/varanus/server/search/date.py +0 -33
  70. varanus-0.1.0.dev3/src/varanus/server/search/facet.py +0 -63
  71. varanus-0.1.0.dev3/src/varanus/server/templates/dashboard.html +0 -4
  72. varanus-0.1.0.dev3/src/varanus/server/templates/search/daterange.html +0 -10
  73. varanus-0.1.0.dev3/src/varanus/server/templates/search/multifacet.html +0 -11
  74. varanus-0.1.0.dev3/src/varanus/server/templates/site/base.html +0 -57
  75. varanus-0.1.0.dev3/src/varanus/server/templates/site/details/query.html +0 -31
  76. varanus-0.1.0.dev3/src/varanus/server/templates/site/errors.html +0 -30
  77. varanus-0.1.0.dev3/src/varanus/server/templates/site/logs.html +0 -32
  78. varanus-0.1.0.dev3/src/varanus/server/templates/site/queries.html +0 -36
  79. varanus-0.1.0.dev3/src/varanus/server/templates/site/requests.html +0 -32
  80. varanus-0.1.0.dev3/src/varanus/server/views/dashboard.py +0 -11
  81. varanus-0.1.0.dev3/src/varanus/server/views/site.py +0 -165
  82. varanus-0.1.0.dev3/src/varanus/utils.py +0 -10
  83. {varanus-0.1.0.dev3 → varanus-0.1.0.dev5}/README.md +0 -0
  84. {varanus-0.1.0.dev3 → varanus-0.1.0.dev5}/src/varanus/__init__.py +0 -0
  85. {varanus-0.1.0.dev3 → varanus-0.1.0.dev5}/src/varanus/client/__init__.py +0 -0
  86. {varanus-0.1.0.dev3 → varanus-0.1.0.dev5}/src/varanus/client/apps.py +0 -0
  87. {varanus-0.1.0.dev3 → varanus-0.1.0.dev5}/src/varanus/client/context.py +0 -0
  88. {varanus-0.1.0.dev3 → varanus-0.1.0.dev5}/src/varanus/client/transport/__init__.py +0 -0
  89. {varanus-0.1.0.dev3 → varanus-0.1.0.dev5}/src/varanus/client/transport/base.py +0 -0
  90. {varanus-0.1.0.dev3 → varanus-0.1.0.dev5}/src/varanus/client/transport/http.py +0 -0
  91. {varanus-0.1.0.dev3 → varanus-0.1.0.dev5}/src/varanus/server/__init__.py +0 -0
  92. {varanus-0.1.0.dev3 → varanus-0.1.0.dev5}/src/varanus/server/__main__.py +0 -0
  93. {varanus-0.1.0.dev3 → varanus-0.1.0.dev5}/src/varanus/server/asgi.py +0 -0
  94. {varanus-0.1.0.dev3 → varanus-0.1.0.dev5}/src/varanus/server/management/__init__.py +0 -0
  95. {varanus-0.1.0.dev3 → varanus-0.1.0.dev5}/src/varanus/server/management/commands/__init__.py +0 -0
  96. {varanus-0.1.0.dev3 → varanus-0.1.0.dev5}/src/varanus/server/management/commands/migrateall.py +0 -0
  97. {varanus-0.1.0.dev3 → varanus-0.1.0.dev5}/src/varanus/server/middleware.py +0 -0
  98. {varanus-0.1.0.dev3 → varanus-0.1.0.dev5}/src/varanus/server/migrations/__init__.py +0 -0
  99. {varanus-0.1.0.dev3 → varanus-0.1.0.dev5}/src/varanus/server/static/css/varanus.css +0 -0
  100. {varanus-0.1.0.dev3 → varanus-0.1.0.dev5}/src/varanus/server/static/js/varanus.js +0 -0
  101. {varanus-0.1.0.dev3 → varanus-0.1.0.dev5}/src/varanus/server/templates/registration/login.html +0 -0
  102. {varanus-0.1.0.dev3 → varanus-0.1.0.dev5}/src/varanus/server/templates/site/details/environment_nodes.html +0 -0
  103. {varanus-0.1.0.dev3 → varanus-0.1.0.dev5}/src/varanus/server/templates/site/details/node_environments.html +0 -0
  104. {varanus-0.1.0.dev3 → varanus-0.1.0.dev5}/src/varanus/server/templatetags/__init__.py +0 -0
  105. {varanus-0.1.0.dev3 → varanus-0.1.0.dev5}/src/varanus/server/views/__init__.py +0 -0
  106. {varanus-0.1.0.dev3 → varanus-0.1.0.dev5}/src/varanus/server/views/base.py +0 -0
  107. {varanus-0.1.0.dev3 → varanus-0.1.0.dev5}/src/varanus/server/wsgi.py +0 -0
@@ -1,15 +1,14 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: varanus
3
- Version: 0.1.0.dev3
3
+ Version: 0.1.0.dev5
4
4
  Summary: Django application monitoring.
5
5
  Requires-Dist: httpx>=0.27.0
6
6
  Requires-Dist: msgspec>=0.19.0
7
7
  Requires-Dist: cconf>=1.0.0 ; extra == 'server'
8
- Requires-Dist: django~=5.2.0 ; extra == 'server'
8
+ Requires-Dist: django>=6.0 ; python_full_version >= '3.12' and extra == 'server'
9
9
  Requires-Dist: django-passkey-auth>=0.2.0 ; extra == 'server'
10
- Requires-Dist: granian>=2.4.2 ; extra == 'server'
10
+ Requires-Dist: granian[reload]>=2.4.2 ; extra == 'server'
11
11
  Requires-Dist: psycopg[binary]>=3.2.1 ; extra == 'server'
12
- Requires-Dist: websockets>=13.0 ; extra == 'server'
13
12
  Requires-Dist: whitenoise>=6.7.0 ; extra == 'server'
14
13
  Requires-Python: >=3.11
15
14
  Provides-Extra: server
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "varanus"
3
- version = "0.1.0.dev3"
3
+ version = "0.1.0.dev5"
4
4
  description = "Django application monitoring."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -12,11 +12,10 @@ dependencies = [
12
12
  [project.optional-dependencies]
13
13
  server = [
14
14
  "cconf>=1.0.0",
15
- "django~=5.2.0",
15
+ "django>=6.0; python_version>='3.12'",
16
16
  "django-passkey-auth>=0.2.0",
17
- "granian>=2.4.2",
17
+ "granian[reload]>=2.4.2",
18
18
  "psycopg[binary]>=3.2.1",
19
- "websockets>=13.0",
20
19
  "whitenoise>=6.7.0",
21
20
  ]
22
21
 
@@ -1,5 +1,7 @@
1
+ import os
1
2
  import platform
2
3
  from importlib.metadata import distributions
4
+ from typing import Iterable
3
5
  from urllib.parse import urlsplit
4
6
 
5
7
  from varanus.events import Context, NodeInfo
@@ -18,23 +20,30 @@ def install_query_logger(logger):
18
20
  return handler
19
21
 
20
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
+
21
42
  class VaranusClient:
22
- environment: str
23
- transport: BaseTransport
24
-
25
- request_attr: str
26
- logger_name: str
27
- tags: dict
28
-
29
- include_headers: list | bool | None
30
- exclude_headers: list | None
31
- sensitive_headers = set(
32
- [
33
- "authorization",
34
- "cookie",
35
- "proxy-authorization",
36
- ]
37
- )
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"])
38
47
 
39
48
  scheme_transports = {
40
49
  "http": "varanus.client.transport.http.ThreadedHttpTransport",
@@ -42,22 +51,28 @@ class VaranusClient:
42
51
  "db": "varanus.client.transport.database.ModelTransport",
43
52
  }
44
53
 
45
- send_all: bool
46
54
  configured = False
47
55
 
48
56
  def setup(
49
57
  self,
50
58
  dsn: str,
59
+ environment: str,
51
60
  node: str | None = None,
52
- environment: str = "",
53
61
  transport_class: str | type[BaseTransport] | None = None,
54
62
  request_attr: str = "varanus",
55
63
  logger_name: str = "varanus.request",
56
64
  tags: dict | None = None,
57
- include_headers: list | bool | None = None,
58
- exclude_headers: list | 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,
59
73
  log_queries: bool | int = False,
60
74
  log_query_params: bool = False,
75
+ log_query_stack: bool = False,
61
76
  query_metrics: bool | str = False,
62
77
  send_all: bool = False,
63
78
  install: list | None = None,
@@ -83,7 +98,21 @@ class VaranusClient:
83
98
  self.logger_name = logger_name
84
99
  self.tags = tags or {}
85
100
  self.include_headers = include_headers
86
- self.exclude_headers = exclude_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
+ )
87
116
  if log_queries or query_metrics:
88
117
  try:
89
118
  # The logger is installed as early as possible, and for all connections.
@@ -93,6 +122,7 @@ class VaranusClient:
93
122
  self.query_logger = QueryLogger(
94
123
  log_queries,
95
124
  log_query_params,
125
+ log_query_stack,
96
126
  query_metrics,
97
127
  )
98
128
  # Install it in each new connection (if it's not already installed).
@@ -122,12 +152,47 @@ class VaranusClient:
122
152
  self.transport.send(e)
123
153
 
124
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
+
125
181
  self.transport.ping(
126
182
  NodeInfo(
127
183
  name=self.node,
128
184
  platform=platform.platform(),
129
- python_version=platform.python_version(),
185
+ language=platform.python_implementation(),
186
+ language_version=platform.python_version(),
187
+ framework="Django",
188
+ framework_version=get_version(),
130
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},
131
196
  )
132
197
  )
133
198
 
@@ -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:
@@ -41,19 +42,19 @@ class QueryLogger:
41
42
  elapsed_ms = (now() - start) // ONE_MS
42
43
  if self.metrics_name:
43
44
  ctx.metric(self.metrics_name, elapsed_ms)
44
- if elapsed_ms < self.threshold:
45
- return
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,
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
+ )
58
60
  )
59
- )
@@ -2,9 +2,10 @@ import ipaddress
2
2
 
3
3
  from django.core.exceptions import MiddlewareNotUsed
4
4
  from django.http import HttpRequest, HttpResponse
5
+ from django.utils import timezone
5
6
 
6
7
  from ..events import Request
7
- from .client import client
8
+ from .client import client, resolve_include_exclude
8
9
 
9
10
 
10
11
  def get_ip(request: HttpRequest):
@@ -22,17 +23,12 @@ def get_ip(request: HttpRequest):
22
23
 
23
24
  def request_headers(request: HttpRequest):
24
25
  headers = {}
25
- if not client.include_headers:
26
- return headers
27
- if client.include_headers is True:
28
- include = set(name.lower() for name in request.headers)
29
- else:
30
- include = set(name.lower() for name in client.include_headers)
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):
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:
36
32
  value = request.headers.get(name)
37
33
  if value is not None:
38
34
  headers[name] = value
@@ -45,7 +41,7 @@ class VaranusMiddleware:
45
41
  # TODO: warning
46
42
  print("VaranusClient is not configured -- disabling middleware.")
47
43
  raise MiddlewareNotUsed()
48
- client.ping()
44
+ self.last_ping = None
49
45
  self.get_response = get_response
50
46
 
51
47
  def process_exception(self, request, exception):
@@ -53,14 +49,22 @@ class VaranusMiddleware:
53
49
  client.raw_exception(exception)
54
50
 
55
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
+
56
59
  with client.context(request.path) as varanus:
57
60
  setattr(request, client.request_attr, varanus)
58
61
  response = self.get_response(request)
59
62
  # TODO: any need for request tags separate from context tags?
60
63
  varanus.request = Request(
61
64
  host=request.get_host(),
62
- path=request.path,
63
65
  method=request.method or "",
66
+ path=request.path,
67
+ query=request.META.get("QUERY_STRING", ""),
64
68
  status=response.status_code,
65
69
  headers=request_headers(request),
66
70
  size=(
@@ -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
+ )
@@ -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
- python_version: str
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 ErrorLine(Struct):
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
- lines: list[ErrorLine] = []
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
- ErrorLine(
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
- lines=lines,
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
@@ -1,9 +1,10 @@
1
1
  from .base import Search
2
- from .date import DateRange
3
- from .facet import MultiFacet
2
+ from .fields import DateRange, Filter, Hidden, MultiFacet
4
3
 
5
4
  __all__ = [
6
5
  "Search",
7
6
  "DateRange",
7
+ "Filter",
8
+ "Hidden",
8
9
  "MultiFacet",
9
10
  ]
@@ -0,0 +1,159 @@
1
+ from typing import Callable, ClassVar, Iterable, Literal, Self
2
+
3
+ from django.db.models import QuerySet
4
+ from django.http import HttpRequest
5
+ from django.template import loader
6
+ from django.utils.datastructures import MultiValueDict
7
+ from django.utils.functional import Promise
8
+ from django.utils.safestring import SafeString
9
+
10
+ from .utils import Namer, StringValues, string_data
11
+
12
+
13
+ class SkipRender(Exception):
14
+ pass
15
+
16
+
17
+ class SearchField:
18
+ template_name: str
19
+ prefixed = False
20
+ default = False
21
+
22
+ def __init__(
23
+ self,
24
+ label=None,
25
+ field_name: str | Iterable[str] | None = None,
26
+ empty_label="NULL",
27
+ # These belong on Facet
28
+ choice_label: Callable[..., str | Promise] | None = None,
29
+ order: Literal["value", "count"] = "value",
30
+ default: bool = False,
31
+ ):
32
+ self.name = ""
33
+ self.label = label or ""
34
+ self.field_name = field_name or ""
35
+ self.prefix = ""
36
+ self.id = ""
37
+ self.empty_label = empty_label
38
+ self.choice_label = choice_label
39
+ self.order = order
40
+ self.default = default
41
+
42
+ def __set_name__(self, owner, name: str):
43
+ assert issubclass(owner, Search)
44
+ self.name = name
45
+ self.prefix = f"{name}_" if self.prefixed else ""
46
+ self.id = f"id_{name}"
47
+ if not self.label:
48
+ self.label = name.capitalize().replace("__", " ")
49
+ if not self.field_name:
50
+ self.field_name = name
51
+ if not hasattr(owner, "_fields"):
52
+ owner._fields = []
53
+ owner._fields.append(self)
54
+
55
+ def __get__(self, instance: "Search", owner=None) -> StringValues | Self:
56
+ if owner is None:
57
+ return self
58
+ if self.prefixed:
59
+ return {
60
+ key[len(self.prefix) :]: values
61
+ for key, values in instance._data.items()
62
+ if key.startswith(self.prefix)
63
+ }
64
+ else:
65
+ return (
66
+ {self.name: instance._data[self.name]}
67
+ if self.name in instance._data
68
+ else {}
69
+ )
70
+
71
+ def __set__(self, instance, value):
72
+ raise AttributeError(f"`{self.name}` is read-only")
73
+
74
+ @property
75
+ def named(self):
76
+ """
77
+ Allows for {{ field.named.something }} in templates.
78
+ """
79
+ return Namer(self.prefix)
80
+
81
+ def apply(
82
+ self,
83
+ queryset: QuerySet,
84
+ field_data: StringValues,
85
+ request: HttpRequest | None = None,
86
+ ) -> QuerySet:
87
+ raise NotImplementedError()
88
+
89
+ def has_value(
90
+ self,
91
+ queryset: QuerySet,
92
+ field_data: StringValues,
93
+ request: HttpRequest | None = None,
94
+ ) -> bool:
95
+ return any(any(v) for v in field_data.values())
96
+
97
+ def get_context(
98
+ self,
99
+ queryset: QuerySet,
100
+ field_data: StringValues,
101
+ request: HttpRequest | None = None,
102
+ ) -> dict:
103
+ return {
104
+ "field": self,
105
+ }
106
+
107
+ def render(
108
+ self,
109
+ queryset: QuerySet,
110
+ field_data: StringValues,
111
+ request: HttpRequest | None = None,
112
+ ) -> SafeString:
113
+ return loader.render_to_string(
114
+ self.template_name,
115
+ self.get_context(queryset, field_data, request=request),
116
+ request,
117
+ )
118
+
119
+
120
+ class Search:
121
+ _fields: ClassVar[list[SearchField]]
122
+ template_name = "search/search.html"
123
+
124
+ def __init__(
125
+ self,
126
+ queryset: QuerySet,
127
+ data: dict | MultiValueDict | None = None,
128
+ request: HttpRequest | None = None,
129
+ ):
130
+ self._queryset = queryset
131
+ self._data = string_data(data)
132
+ self._request = request
133
+
134
+ def queryset(self, for_field=None):
135
+ qs = self._queryset
136
+ for field in self._fields:
137
+ if field == for_field:
138
+ continue
139
+ field_data = getattr(self, field.name)
140
+ qs = field.apply(qs, field_data, request=self._request)
141
+ return qs
142
+
143
+ def render(self) -> SafeString:
144
+ context = {"fields": []}
145
+ initial = not bool(self._data)
146
+ for field in self._fields:
147
+ qs = self.queryset(for_field=field)
148
+ field_data = getattr(self, field.name)
149
+ try:
150
+ rendered = field.render(qs, field_data, request=self._request)
151
+ default = (initial and field.default) or field.has_value(
152
+ qs,
153
+ field_data,
154
+ request=self._request,
155
+ )
156
+ context["fields"].append((field, default, rendered))
157
+ except SkipRender:
158
+ continue
159
+ return loader.render_to_string(self.template_name, context, self._request)