varanus 0.1.0.dev4__tar.gz → 0.1.0.dev6__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 (90) hide show
  1. {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/PKG-INFO +3 -4
  2. {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/pyproject.toml +3 -4
  3. {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/client/client.py +19 -4
  4. {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/client/loggers.py +15 -16
  5. {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/client/middleware.py +9 -1
  6. {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/client/transport/database.py +10 -10
  7. {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/events.py +4 -2
  8. {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/admin.py +18 -4
  9. {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/apps.py +3 -0
  10. varanus-0.1.0.dev6/src/varanus/server/integrations/__init__.py +3 -0
  11. varanus-0.1.0.dev6/src/varanus/server/integrations/base.py +38 -0
  12. varanus-0.1.0.dev6/src/varanus/server/integrations/squish.py +74 -0
  13. varanus-0.1.0.dev6/src/varanus/server/management/commands/serve.py +64 -0
  14. {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/migrations/0001_initial.py +90 -1
  15. {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/models.py +89 -7
  16. {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/router.py +1 -0
  17. {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/settings.py +33 -7
  18. varanus-0.1.0.dev6/src/varanus/server/tasks.py +64 -0
  19. varanus-0.1.0.dev6/src/varanus/server/templates/dashboard.html +41 -0
  20. {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/templates/site/details/error.html +9 -11
  21. {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/templates/site/details/log.html +3 -5
  22. {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/templates/site/details/metric.html +3 -5
  23. {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/templates/site/details/query.html +5 -7
  24. {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/urls.py +10 -0
  25. {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/views/api.py +14 -24
  26. {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/views/dashboard.py +3 -8
  27. varanus-0.1.0.dev6/src/varanus/tasks/__init__.py +0 -0
  28. varanus-0.1.0.dev6/src/varanus/tasks/admin.py +15 -0
  29. varanus-0.1.0.dev6/src/varanus/tasks/apps.py +0 -0
  30. varanus-0.1.0.dev6/src/varanus/tasks/backend.py +62 -0
  31. varanus-0.1.0.dev6/src/varanus/tasks/management/__init__.py +0 -0
  32. varanus-0.1.0.dev6/src/varanus/tasks/management/commands/__init__.py +0 -0
  33. varanus-0.1.0.dev6/src/varanus/tasks/management/commands/tasker.py +20 -0
  34. varanus-0.1.0.dev6/src/varanus/tasks/migrations/0001_initial.py +66 -0
  35. varanus-0.1.0.dev6/src/varanus/tasks/migrations/__init__.py +0 -0
  36. varanus-0.1.0.dev6/src/varanus/tasks/models.py +131 -0
  37. varanus-0.1.0.dev6/src/varanus/tasks/runner.py +107 -0
  38. varanus-0.1.0.dev6/src/varanus/utils.py +28 -0
  39. varanus-0.1.0.dev4/src/varanus/server/templates/dashboard.html +0 -11
  40. varanus-0.1.0.dev4/src/varanus/utils.py +0 -10
  41. {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/README.md +0 -0
  42. {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/__init__.py +0 -0
  43. {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/client/__init__.py +0 -0
  44. {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/client/apps.py +0 -0
  45. {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/client/context.py +0 -0
  46. {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/client/transport/__init__.py +0 -0
  47. {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/client/transport/base.py +0 -0
  48. {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/client/transport/http.py +0 -0
  49. {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/search/__init__.py +0 -0
  50. {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/search/base.py +0 -0
  51. {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/search/fields.py +0 -0
  52. {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/search/templates/search/daterange.html +0 -0
  53. {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/search/templates/search/filter.html +0 -0
  54. {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/search/templates/search/hidden.html +0 -0
  55. {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/search/templates/search/multifacet.html +0 -0
  56. {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/search/templates/search/search.html +0 -0
  57. {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/search/utils.py +0 -0
  58. {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/__init__.py +0 -0
  59. {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/__main__.py +0 -0
  60. {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/asgi.py +0 -0
  61. {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/context_processors.py +0 -0
  62. {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/management/__init__.py +0 -0
  63. {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/management/commands/__init__.py +0 -0
  64. {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/management/commands/migrateall.py +0 -0
  65. {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/middleware.py +0 -0
  66. {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/migrations/__init__.py +0 -0
  67. {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/static/css/varanus.css +0 -0
  68. {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/static/js/varanus.js +0 -0
  69. {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/templates/base.html +0 -0
  70. {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/templates/registration/login.html +0 -0
  71. {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/templates/site/base.html +0 -0
  72. {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/templates/site/details/environment_nodes.html +0 -0
  73. {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/templates/site/details/node_env.html +0 -0
  74. {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/templates/site/details/node_environments.html +0 -0
  75. {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/templates/site/details/node_packages.html +0 -0
  76. {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/templates/site/details/node_settings.html +0 -0
  77. {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/templates/site/details/request.html +0 -0
  78. {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/templates/site/errors.html +0 -0
  79. {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/templates/site/logs.html +0 -0
  80. {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/templates/site/metrics.html +0 -0
  81. {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/templates/site/overview.html +0 -0
  82. {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/templates/site/queries.html +0 -0
  83. {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/templates/site/requests.html +0 -0
  84. {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/templatetags/__init__.py +0 -0
  85. {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/templatetags/varanus.py +0 -0
  86. {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/utils.py +0 -0
  87. {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/views/__init__.py +0 -0
  88. {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/views/base.py +0 -0
  89. {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/src/varanus/server/views/site.py +0 -0
  90. {varanus-0.1.0.dev4 → varanus-0.1.0.dev6}/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.dev4
3
+ Version: 0.1.0.dev6
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.dev4"
3
+ version = "0.1.0.dev6"
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
 
@@ -40,8 +40,9 @@ def resolve_include_exclude(
40
40
 
41
41
 
42
42
  class VaranusClient:
43
+ # Things we don't want to send, at least by default.
43
44
  sensitive_headers = set(["authorization", "cookie", "proxy-authorization"])
44
- sensitive_settings = set(["DATABASES"])
45
+ sensitive_settings = set(["SECRET_KEY"])
45
46
  sensitive_env = set(["PGPASSWORD"])
46
47
 
47
48
  scheme_transports = {
@@ -63,9 +64,10 @@ class VaranusClient:
63
64
  tags: dict | None = None,
64
65
  include_headers: Iterable[str] | bool = False,
65
66
  exclude_headers: Iterable[str] | None = None,
66
- include_settings: Iterable[str] | bool = False,
67
+ include_settings: Iterable[str] | bool = True,
67
68
  exclude_settings: Iterable[str] | None = None,
68
69
  include_default_settings: bool = False,
70
+ filter_settings: bool = True,
69
71
  include_env: Iterable[str] | bool = False,
70
72
  exclude_env: Iterable[str] | None = None,
71
73
  log_queries: bool | int = False,
@@ -106,6 +108,7 @@ class VaranusClient:
106
108
  else set(exclude_settings)
107
109
  )
108
110
  self.include_default_settings = include_default_settings
111
+ self.filter_settings = filter_settings
109
112
  self.include_env = include_env
110
113
  self.exclude_env = (
111
114
  self.sensitive_env if exclude_env is None else set(exclude_env)
@@ -152,8 +155,20 @@ class VaranusClient:
152
155
  from django import get_version
153
156
  from django.conf import settings
154
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
+
155
170
  include_settings = resolve_include_exclude(
156
- [s for s in dir(settings) if s == s.upper()],
171
+ settings_dict.keys(),
157
172
  self.include_settings,
158
173
  self.exclude_settings,
159
174
  )
@@ -173,7 +188,7 @@ class VaranusClient:
173
188
  framework_version=get_version(),
174
189
  packages={d.name: d.version for d in distributions()},
175
190
  settings={
176
- s: repr(getattr(settings, s))
191
+ s: settings_dict[s]
177
192
  for s in include_settings
178
193
  if self.include_default_settings or settings.is_overridden(s)
179
194
  },
@@ -42,20 +42,19 @@ class QueryLogger:
42
42
  elapsed_ms = (now() - start) // ONE_MS
43
43
  if self.metrics_name:
44
44
  ctx.metric(self.metrics_name, elapsed_ms)
45
- if elapsed_ms < self.threshold:
46
- return
47
- ctx.queries.append(
48
- Query(
49
- timestamp=start,
50
- sql=sql,
51
- params=(
52
- [repr(p) for p in params]
53
- if params and self.log_params
54
- else []
55
- ),
56
- db=context["connection"].alias,
57
- elapsed_ms=elapsed_ms,
58
- success=success,
59
- stack=capture_stack(1) if self.log_stack else [],
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
60
  )
61
- )
@@ -2,6 +2,7 @@ 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
8
  from .client import client, resolve_include_exclude
@@ -40,7 +41,7 @@ class VaranusMiddleware:
40
41
  # TODO: warning
41
42
  print("VaranusClient is not configured -- disabling middleware.")
42
43
  raise MiddlewareNotUsed()
43
- client.ping()
44
+ self.last_ping = None
44
45
  self.get_response = get_response
45
46
 
46
47
  def process_exception(self, request, exception):
@@ -48,6 +49,13 @@ class VaranusMiddleware:
48
49
  client.raw_exception(exception)
49
50
 
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
+
51
59
  with client.context(request.path) as varanus:
52
60
  setattr(request, client.request_attr, varanus)
53
61
  response = self.get_response(request)
@@ -1,7 +1,8 @@
1
1
  import typing
2
- import uuid
3
2
  from urllib.parse import SplitResult
4
3
 
4
+ import msgspec
5
+
5
6
  from varanus import events
6
7
 
7
8
  from .base import BaseTransport
@@ -17,6 +18,7 @@ class ModelTransport(BaseTransport):
17
18
  def __init__(self, url: SplitResult, environment: str, node: str):
18
19
  self.slug = url.netloc
19
20
  self.environment = environment
21
+ self.node_name = node
20
22
 
21
23
  def ensure_site(self):
22
24
  if hasattr(self, "site"):
@@ -43,13 +45,11 @@ class ModelTransport(BaseTransport):
43
45
  )
44
46
 
45
47
  def send(self, event: events.Context):
46
- from varanus.server.models import Context
48
+ from varanus.server.tasks import ingest
47
49
 
48
- with self.site.activated():
49
- Context.from_event(
50
- event,
51
- event_id=uuid.uuid4(),
52
- site=self.site,
53
- environment=self.environment,
54
- node=self.node,
55
- )
50
+ ingest.enqueue(
51
+ self.site.pk,
52
+ self.node_name,
53
+ self.environment,
54
+ msgspec.json.encode(event).decode(),
55
+ )
@@ -4,6 +4,8 @@ from datetime import datetime, timezone
4
4
 
5
5
  from msgspec import Struct, field
6
6
 
7
+ from .utils import safe_repr
8
+
7
9
 
8
10
  def now():
9
11
  return datetime.now(tz=timezone.utc)
@@ -46,7 +48,7 @@ def capture_stack(skip: int = 0, include_locals: bool = False) -> list[StackLine
46
48
  function=frame.function,
47
49
  module=frame.frame.f_globals.get("__name__", ""),
48
50
  locals=(
49
- {name: repr(val) for name, val in frame.frame.f_locals.items()}
51
+ {name: safe_repr(val) for name, val in frame.frame.f_locals.items()}
50
52
  if include_locals
51
53
  else {}
52
54
  ),
@@ -94,7 +96,7 @@ class Error(Event):
94
96
  function=function,
95
97
  module=module,
96
98
  linesrc=linesrc,
97
- locals={name: repr(val) for name, val in f_locals.items()},
99
+ locals={name: safe_repr(val) for name, val in f_locals.items()},
98
100
  )
99
101
  )
100
102
  tb = tb.tb_next
@@ -4,6 +4,7 @@ from django.contrib import admin
4
4
 
5
5
  from .models import (
6
6
  Context,
7
+ ContextIntegration,
7
8
  Error,
8
9
  Log,
9
10
  Metric,
@@ -13,6 +14,7 @@ from .models import (
13
14
  Query,
14
15
  Request,
15
16
  Site,
17
+ SiteIntegration,
16
18
  SiteKey,
17
19
  SiteMember,
18
20
  )
@@ -34,9 +36,14 @@ class SiteKeyInline(admin.TabularInline):
34
36
  extra = 0
35
37
 
36
38
 
39
+ class SiteIntegrationInline(admin.StackedInline):
40
+ model = SiteIntegration
41
+ extra = 0
42
+
43
+
37
44
  class SiteAdmin(admin.ModelAdmin):
38
45
  list_display = ["name", "slug", "schema_name"]
39
- inlines = [SiteMemberInline, SiteKeyInline]
46
+ inlines = [SiteMemberInline, SiteKeyInline, SiteIntegrationInline]
40
47
  prepopulated_fields = {"slug": ["name"], "schema_name": ["name"]}
41
48
  fieldsets = [
42
49
  (
@@ -160,15 +167,22 @@ class NodeUpdateAdmin(admin.ModelAdmin):
160
167
  list_filter = ["node", "node__site", "node__environment"]
161
168
 
162
169
 
170
+ class ContextIntegrationAdmin(admin.ModelAdmin):
171
+ list_display = ["context", "context__site", "integration", "run_date"]
172
+ list_filter = ["context__site", "integration"]
173
+ raw_id_fields = ["context"]
174
+
175
+
163
176
  admin.site.register(Site, SiteAdmin)
177
+ admin.site.register(Node, NodeAdmin)
178
+ admin.site.register(NodePackage, NodePackageAdmin)
179
+ admin.site.register(NodeUpdate, NodeUpdateAdmin)
164
180
 
165
181
  if not settings.VARANUS_USE_SCHEMAS:
166
- admin.site.register(Node, NodeAdmin)
167
- admin.site.register(NodePackage, NodePackageAdmin)
168
- admin.site.register(NodeUpdate, NodeUpdateAdmin)
169
182
  admin.site.register(Request, RequestAdmin)
170
183
  admin.site.register(Log, LogAdmin)
171
184
  admin.site.register(Error, ErrorAdmin)
172
185
  admin.site.register(Context, ContextAdmin)
173
186
  admin.site.register(Metric, MetricAdmin)
174
187
  admin.site.register(Query, QueryAdmin)
188
+ admin.site.register(ContextIntegration, ContextIntegrationAdmin)
@@ -5,3 +5,6 @@ class VaranusServer(AppConfig):
5
5
  default_auto_field = "django.db.models.BigAutoField"
6
6
  name = "varanus.server"
7
7
  label = "varanus"
8
+
9
+ def ready(self):
10
+ pass
@@ -0,0 +1,3 @@
1
+ from .base import DuplicateIntegration, Integration, IntegrationError
2
+
3
+ __all__ = ["DuplicateIntegration", "Integration", "IntegrationError"]
@@ -0,0 +1,38 @@
1
+ from typing import TYPE_CHECKING, Any
2
+
3
+ from varanus import events
4
+
5
+ if TYPE_CHECKING:
6
+ from ..models import Context
7
+
8
+
9
+ class IntegrationError(Exception):
10
+ pass
11
+
12
+
13
+ class DuplicateIntegration(IntegrationError):
14
+ pass
15
+
16
+
17
+ class Integration:
18
+ def __init__(self, settings: dict[str, Any]):
19
+ self.settings = settings
20
+
21
+ def is_valid(self, context: events.Context) -> bool:
22
+ """
23
+ Returns whether the integration should be scheduled to run for the given
24
+ Context event (not the Context model).
25
+ """
26
+ return True
27
+
28
+ def fingerprint(self, context: "Context") -> str | None:
29
+ """
30
+ Given a Context model, returns a fingerprint for debouncing integration calls.
31
+ """
32
+ return None
33
+
34
+ def execute(self, context: "Context") -> Any:
35
+ """
36
+ Runs the integration for the given Context model.
37
+ """
38
+ raise NotImplementedError()
@@ -0,0 +1,74 @@
1
+ import itertools
2
+ import logging
3
+
4
+ import httpx
5
+
6
+ from varanus import events
7
+ from varanus.utils import make_fingerprint
8
+
9
+ from ..models import Context
10
+ from .base import Integration
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class SquishIntegration(Integration):
16
+ def is_valid(self, context: events.Context) -> bool:
17
+ return (
18
+ (len(context.errors) > 0)
19
+ and bool(self.settings.get("endpoint"))
20
+ and bool(self.settings.get("api_key"))
21
+ and bool(self.settings.get("user_key"))
22
+ )
23
+
24
+ def fingerprint(self, context: "Context") -> str | None:
25
+ return make_fingerprint(
26
+ itertools.chain.from_iterable(
27
+ err.fingerprint_parts() for err in context.errors.all()
28
+ )
29
+ )
30
+
31
+ def execute(self, context: Context):
32
+ lines = []
33
+
34
+ for err in context.errors.all():
35
+ lines.append(f"### {err.kind} in {err.module}")
36
+ lines.append(f"> {err.message}")
37
+ lines.append("```")
38
+ for line in err.stack:
39
+ lines.append(
40
+ "{module}.{func} - {file}:{lineno}".format(
41
+ module=line["module"],
42
+ func=line["function"],
43
+ file=line["file"],
44
+ lineno=line["lineno"],
45
+ )
46
+ )
47
+ lines.append(" " + line["linesrc"])
48
+ lines.append("```")
49
+
50
+ for idx, log in enumerate(context.logs.all()):
51
+ if idx == 0:
52
+ lines.append("### Request Logs")
53
+ lines.append(
54
+ "* `{level} - {name}:{lineno}`: *{msg}*".format(
55
+ level=log.get_level_display(), # type: ignore
56
+ name=log.name,
57
+ lineno=log.lineno,
58
+ msg=log.message,
59
+ )
60
+ )
61
+
62
+ subject = f"{context.site.name} ({context.environment}) - {context.name}"
63
+ return httpx.post(
64
+ self.settings["endpoint"],
65
+ json={
66
+ "issue_type": self.settings.get("issue_type", "bug"),
67
+ "subject": subject,
68
+ "description": {"comment": "\n".join(lines), "format": "markdown"},
69
+ },
70
+ headers={
71
+ "X-Squish-API-Key": self.settings["api_key"],
72
+ "X-Squish-User-Key": self.settings["user_key"],
73
+ },
74
+ ).json()
@@ -0,0 +1,64 @@
1
+ import os
2
+ import threading
3
+
4
+ from django.conf import settings
5
+ from django.core.management import BaseCommand
6
+ from granian import Granian
7
+ from granian.constants import Interfaces
8
+
9
+
10
+ def cpus() -> int:
11
+ return os.cpu_count() or 4
12
+
13
+
14
+ class Command(BaseCommand):
15
+ help = "Web server and task runner."
16
+
17
+ def add_arguments(self, parser):
18
+ parser.add_argument("-r", "--reload", action="store_true", default=False)
19
+ parser.add_argument("-w", "--workers", type=int, default=1)
20
+ parser.add_argument("-t", "--threads", type=int, default=cpus())
21
+ parser.add_argument(
22
+ "-k",
23
+ "--tasks",
24
+ nargs="?",
25
+ type=int,
26
+ const=cpus() // 2,
27
+ default=0,
28
+ )
29
+
30
+ def on_startup(self):
31
+ if self.runner:
32
+ threading.Thread(target=self.runner.run).start()
33
+
34
+ def on_reload(self):
35
+ if self.runner:
36
+ self.runner.reload()
37
+
38
+ def on_shutdown(self):
39
+ if self.runner:
40
+ self.runner.stop()
41
+
42
+ def handle(self, *args, **options):
43
+ self.runner = None
44
+ if workers := options["tasks"]:
45
+ from varanus.tasks.runner import Runner
46
+
47
+ self.runner = Runner(workers=workers)
48
+
49
+ server = Granian(
50
+ ":".join(settings.WSGI_APPLICATION.rsplit(".", 1)),
51
+ address="0.0.0.0",
52
+ port=9000,
53
+ interface=Interfaces.WSGI,
54
+ workers=options["workers"],
55
+ blocking_threads=options["threads"],
56
+ log_access=True,
57
+ reload=options["reload"],
58
+ reload_paths=[settings.BASE_DIR / "src"],
59
+ websockets=False,
60
+ )
61
+ server.on_startup(self.on_startup)
62
+ server.on_reload(self.on_reload)
63
+ server.on_shutdown(self.on_shutdown)
64
+ server.serve()
@@ -1,4 +1,4 @@
1
- # Generated by Django 5.2.8 on 2025-11-25 01:37
1
+ # Generated by Django 6.0 on 2025-12-11 00:00
2
2
 
3
3
  import secrets
4
4
 
@@ -491,6 +491,81 @@ class Migration(migrations.Migration):
491
491
  to="varanus.site",
492
492
  ),
493
493
  ),
494
+ migrations.CreateModel(
495
+ name="SiteIntegration",
496
+ fields=[
497
+ (
498
+ "id",
499
+ models.BigAutoField(
500
+ auto_created=True,
501
+ primary_key=True,
502
+ serialize=False,
503
+ verbose_name="ID",
504
+ ),
505
+ ),
506
+ ("name", models.CharField(max_length=100)),
507
+ ("integration_path", models.CharField(max_length=250)),
508
+ ("settings", models.JSONField(blank=True, default=dict)),
509
+ ("event_filter", models.JSONField(blank=True, default=dict)),
510
+ ("debounce", models.CharField(default="30d", max_length=20)),
511
+ (
512
+ "site",
513
+ models.ForeignKey(
514
+ on_delete=django.db.models.deletion.CASCADE,
515
+ related_name="integrations",
516
+ to="varanus.site",
517
+ ),
518
+ ),
519
+ ],
520
+ options={
521
+ "db_table": "site_integration",
522
+ },
523
+ ),
524
+ migrations.CreateModel(
525
+ name="ContextIntegration",
526
+ fields=[
527
+ (
528
+ "id",
529
+ models.BigAutoField(
530
+ auto_created=True,
531
+ primary_key=True,
532
+ serialize=False,
533
+ verbose_name="ID",
534
+ ),
535
+ ),
536
+ (
537
+ "identifier",
538
+ models.CharField(
539
+ blank=True,
540
+ db_index=True,
541
+ default=None,
542
+ max_length=200,
543
+ null=True,
544
+ ),
545
+ ),
546
+ ("result", models.JSONField(blank=True, default=dict)),
547
+ ("run_date", models.DateTimeField(auto_now_add=True)),
548
+ (
549
+ "context",
550
+ models.ForeignKey(
551
+ on_delete=django.db.models.deletion.CASCADE,
552
+ related_name="integrations",
553
+ to="varanus.context",
554
+ ),
555
+ ),
556
+ (
557
+ "integration",
558
+ models.ForeignKey(
559
+ on_delete=django.db.models.deletion.CASCADE,
560
+ related_name="contexts",
561
+ to="varanus.siteintegration",
562
+ ),
563
+ ),
564
+ ],
565
+ options={
566
+ "db_table": "context_integration",
567
+ },
568
+ ),
494
569
  migrations.CreateModel(
495
570
  name="SiteKey",
496
571
  fields=[
@@ -604,6 +679,20 @@ class Migration(migrations.Migration):
604
679
  name="unique_site_node",
605
680
  ),
606
681
  ),
682
+ migrations.AddConstraint(
683
+ model_name="context",
684
+ constraint=models.UniqueConstraint(
685
+ condition=models.Q(("parent__isnull", True)),
686
+ fields=("event_id",),
687
+ name="unique_context_event_id",
688
+ ),
689
+ ),
690
+ migrations.AddConstraint(
691
+ model_name="contextintegration",
692
+ constraint=models.UniqueConstraint(
693
+ fields=("context", "integration"), name="uniq_context_integration"
694
+ ),
695
+ ),
607
696
  migrations.AddConstraint(
608
697
  model_name="sitemember",
609
698
  constraint=models.UniqueConstraint(