varanus 0.1.0__tar.gz → 0.1.0.dev1__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 (122) hide show
  1. varanus-0.1.0.dev1/PKG-INFO +20 -0
  2. varanus-0.1.0.dev1/README.md +3 -0
  3. {varanus-0.1.0 → varanus-0.1.0.dev1}/pyproject.toml +12 -12
  4. {varanus-0.1.0 → varanus-0.1.0.dev1}/src/varanus/client/__init__.py +0 -1
  5. {varanus-0.1.0 → varanus-0.1.0.dev1}/src/varanus/client/apps.py +1 -1
  6. varanus-0.1.0.dev1/src/varanus/client/client.py +151 -0
  7. {varanus-0.1.0 → varanus-0.1.0.dev1}/src/varanus/client/context.py +11 -22
  8. {varanus-0.1.0 → varanus-0.1.0.dev1}/src/varanus/client/loggers.py +17 -18
  9. {varanus-0.1.0 → varanus-0.1.0.dev1}/src/varanus/client/middleware.py +20 -24
  10. {varanus-0.1.0 → varanus-0.1.0.dev1}/src/varanus/client/transport/base.py +1 -1
  11. varanus-0.1.0.dev1/src/varanus/client/transport/database.py +28 -0
  12. varanus-0.1.0.dev1/src/varanus/client/transport/http.py +37 -0
  13. {varanus-0.1.0 → varanus-0.1.0.dev1}/src/varanus/events.py +9 -49
  14. {varanus-0.1.0 → varanus-0.1.0.dev1}/src/varanus/server/admin.py +15 -85
  15. {varanus-0.1.0 → varanus-0.1.0.dev1}/src/varanus/server/apps.py +0 -3
  16. varanus-0.1.0.dev1/src/varanus/server/context_processors.py +7 -0
  17. {varanus-0.1.0 → varanus-0.1.0.dev1}/src/varanus/server/migrations/0001_initial.py +60 -246
  18. varanus-0.1.0.dev1/src/varanus/server/migrations/0002_context_node_error_node_log_node_metric_node_and_more.py +79 -0
  19. {varanus-0.1.0 → varanus-0.1.0.dev1}/src/varanus/server/models.py +29 -210
  20. {varanus-0.1.0 → varanus-0.1.0.dev1}/src/varanus/server/router.py +0 -4
  21. {varanus-0.1.0/src/varanus → varanus-0.1.0.dev1/src/varanus/server}/search/__init__.py +2 -3
  22. varanus-0.1.0.dev1/src/varanus/server/search/base.py +106 -0
  23. varanus-0.1.0.dev1/src/varanus/server/search/date.py +33 -0
  24. varanus-0.1.0.dev1/src/varanus/server/search/facet.py +59 -0
  25. {varanus-0.1.0 → varanus-0.1.0.dev1}/src/varanus/server/settings.py +6 -83
  26. {varanus-0.1.0 → varanus-0.1.0.dev1}/src/varanus/server/templates/base.html +2 -2
  27. varanus-0.1.0.dev1/src/varanus/server/templates/dashboard.html +4 -0
  28. varanus-0.1.0.dev1/src/varanus/server/templates/search/daterange.html +10 -0
  29. varanus-0.1.0.dev1/src/varanus/server/templates/search/multifacet.html +11 -0
  30. varanus-0.1.0.dev1/src/varanus/server/templates/site/base.html +61 -0
  31. varanus-0.1.0.dev1/src/varanus/server/templates/site/details/error.html +1 -0
  32. varanus-0.1.0.dev1/src/varanus/server/templates/site/details/log.html +1 -0
  33. varanus-0.1.0.dev1/src/varanus/server/templates/site/details/metric.html +1 -0
  34. varanus-0.1.0.dev1/src/varanus/server/templates/site/details/query.html +1 -0
  35. varanus-0.1.0.dev1/src/varanus/server/templates/site/details/request.html +44 -0
  36. varanus-0.1.0.dev1/src/varanus/server/templates/site/errors.html +30 -0
  37. varanus-0.1.0.dev1/src/varanus/server/templates/site/logs.html +32 -0
  38. {varanus-0.1.0 → varanus-0.1.0.dev1}/src/varanus/server/templates/site/metrics.html +5 -17
  39. varanus-0.1.0.dev1/src/varanus/server/templates/site/overview.html +48 -0
  40. varanus-0.1.0.dev1/src/varanus/server/templates/site/queries.html +36 -0
  41. varanus-0.1.0.dev1/src/varanus/server/templates/site/requests.html +32 -0
  42. varanus-0.1.0.dev1/src/varanus/server/urls.py +45 -0
  43. {varanus-0.1.0 → varanus-0.1.0.dev1}/src/varanus/server/views/api.py +16 -14
  44. varanus-0.1.0.dev1/src/varanus/server/views/dashboard.py +11 -0
  45. varanus-0.1.0.dev1/src/varanus/server/views/site.py +112 -0
  46. varanus-0.1.0.dev1/src/varanus/utils.py +10 -0
  47. varanus-0.1.0/PKG-INFO +0 -46
  48. varanus-0.1.0/README.md +0 -25
  49. varanus-0.1.0/src/varanus/.DS_Store +0 -0
  50. varanus-0.1.0/src/varanus/__init__.py +0 -6
  51. varanus-0.1.0/src/varanus/client/.DS_Store +0 -0
  52. varanus-0.1.0/src/varanus/client/client.py +0 -231
  53. varanus-0.1.0/src/varanus/client/transport/.DS_Store +0 -0
  54. varanus-0.1.0/src/varanus/client/transport/database.py +0 -57
  55. varanus-0.1.0/src/varanus/client/transport/http.py +0 -83
  56. varanus-0.1.0/src/varanus/search/.DS_Store +0 -0
  57. varanus-0.1.0/src/varanus/search/base.py +0 -159
  58. varanus-0.1.0/src/varanus/search/fields.py +0 -189
  59. varanus-0.1.0/src/varanus/search/templates/search/.DS_Store +0 -0
  60. varanus-0.1.0/src/varanus/search/templates/search/daterange.html +0 -11
  61. varanus-0.1.0/src/varanus/search/templates/search/filter.html +0 -1
  62. varanus-0.1.0/src/varanus/search/templates/search/hidden.html +0 -12
  63. varanus-0.1.0/src/varanus/search/templates/search/multifacet.html +0 -6
  64. varanus-0.1.0/src/varanus/search/templates/search/search.html +0 -23
  65. varanus-0.1.0/src/varanus/search/utils.py +0 -38
  66. varanus-0.1.0/src/varanus/server/.DS_Store +0 -0
  67. varanus-0.1.0/src/varanus/server/context_processors.py +0 -9
  68. varanus-0.1.0/src/varanus/server/integrations/__init__.py +0 -3
  69. varanus-0.1.0/src/varanus/server/integrations/base.py +0 -38
  70. varanus-0.1.0/src/varanus/server/integrations/squish.py +0 -74
  71. varanus-0.1.0/src/varanus/server/management/commands/.DS_Store +0 -0
  72. varanus-0.1.0/src/varanus/server/management/commands/maintenance.py +0 -21
  73. varanus-0.1.0/src/varanus/server/migrations/0002_drop_scheduledtask.py +0 -13
  74. varanus-0.1.0/src/varanus/server/migrations/0003_site_retention.py +0 -21
  75. varanus-0.1.0/src/varanus/server/migrations/0004_node_version.py +0 -17
  76. varanus-0.1.0/src/varanus/server/static/.DS_Store +0 -0
  77. varanus-0.1.0/src/varanus/server/static/css/.DS_Store +0 -0
  78. varanus-0.1.0/src/varanus/server/static/js/.DS_Store +0 -0
  79. varanus-0.1.0/src/varanus/server/tasks.py +0 -73
  80. varanus-0.1.0/src/varanus/server/templates/.DS_Store +0 -0
  81. varanus-0.1.0/src/varanus/server/templates/dashboard.html +0 -43
  82. varanus-0.1.0/src/varanus/server/templates/site/.DS_Store +0 -0
  83. varanus-0.1.0/src/varanus/server/templates/site/base.html +0 -65
  84. varanus-0.1.0/src/varanus/server/templates/site/details/environment_nodes.html +0 -6
  85. varanus-0.1.0/src/varanus/server/templates/site/details/error.html +0 -47
  86. varanus-0.1.0/src/varanus/server/templates/site/details/log.html +0 -34
  87. varanus-0.1.0/src/varanus/server/templates/site/details/metric.html +0 -38
  88. varanus-0.1.0/src/varanus/server/templates/site/details/node_env.html +0 -24
  89. varanus-0.1.0/src/varanus/server/templates/site/details/node_environments.html +0 -6
  90. varanus-0.1.0/src/varanus/server/templates/site/details/node_packages.html +0 -44
  91. varanus-0.1.0/src/varanus/server/templates/site/details/node_settings.html +0 -24
  92. varanus-0.1.0/src/varanus/server/templates/site/details/query.html +0 -46
  93. varanus-0.1.0/src/varanus/server/templates/site/details/request.html +0 -150
  94. varanus-0.1.0/src/varanus/server/templates/site/errors.html +0 -44
  95. varanus-0.1.0/src/varanus/server/templates/site/logs.html +0 -47
  96. varanus-0.1.0/src/varanus/server/templates/site/overview.html +0 -62
  97. varanus-0.1.0/src/varanus/server/templates/site/queries.html +0 -64
  98. varanus-0.1.0/src/varanus/server/templates/site/requests.html +0 -54
  99. varanus-0.1.0/src/varanus/server/templatetags/varanus.py +0 -20
  100. varanus-0.1.0/src/varanus/server/urls.py +0 -94
  101. varanus-0.1.0/src/varanus/server/utils.py +0 -8
  102. varanus-0.1.0/src/varanus/server/views/.DS_Store +0 -0
  103. varanus-0.1.0/src/varanus/server/views/__init__.py +0 -0
  104. varanus-0.1.0/src/varanus/server/views/dashboard.py +0 -25
  105. varanus-0.1.0/src/varanus/server/views/site.py +0 -254
  106. varanus-0.1.0/src/varanus/utils.py +0 -28
  107. /varanus-0.1.0/src/varanus/version.py → /varanus-0.1.0.dev1/src/varanus/__init__.py +0 -0
  108. {varanus-0.1.0 → varanus-0.1.0.dev1}/src/varanus/client/transport/__init__.py +0 -0
  109. {varanus-0.1.0 → varanus-0.1.0.dev1}/src/varanus/server/__init__.py +0 -0
  110. {varanus-0.1.0 → varanus-0.1.0.dev1}/src/varanus/server/__main__.py +0 -0
  111. {varanus-0.1.0 → varanus-0.1.0.dev1}/src/varanus/server/asgi.py +0 -0
  112. {varanus-0.1.0 → varanus-0.1.0.dev1}/src/varanus/server/management/__init__.py +0 -0
  113. {varanus-0.1.0 → varanus-0.1.0.dev1}/src/varanus/server/management/commands/__init__.py +0 -0
  114. {varanus-0.1.0 → varanus-0.1.0.dev1}/src/varanus/server/management/commands/migrateall.py +0 -0
  115. {varanus-0.1.0 → varanus-0.1.0.dev1}/src/varanus/server/middleware.py +0 -0
  116. {varanus-0.1.0 → varanus-0.1.0.dev1}/src/varanus/server/migrations/__init__.py +0 -0
  117. {varanus-0.1.0 → varanus-0.1.0.dev1}/src/varanus/server/static/css/varanus.css +0 -0
  118. {varanus-0.1.0 → varanus-0.1.0.dev1}/src/varanus/server/static/js/varanus.js +0 -0
  119. {varanus-0.1.0 → varanus-0.1.0.dev1}/src/varanus/server/templates/registration/login.html +0 -0
  120. {varanus-0.1.0/src/varanus/server/templatetags → varanus-0.1.0.dev1/src/varanus/server/views}/__init__.py +0 -0
  121. {varanus-0.1.0 → varanus-0.1.0.dev1}/src/varanus/server/views/base.py +0 -0
  122. {varanus-0.1.0 → varanus-0.1.0.dev1}/src/varanus/server/wsgi.py +0 -0
@@ -0,0 +1,20 @@
1
+ Metadata-Version: 2.3
2
+ Name: varanus
3
+ Version: 0.1.0.dev1
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~=5.2.0 ; extra == 'server'
9
+ Requires-Dist: django-passkey-auth>=0.2.0 ; extra == 'server'
10
+ Requires-Dist: granian>=2.4.2 ; extra == 'server'
11
+ Requires-Dist: psycopg[binary]>=3.2.1 ; extra == 'server'
12
+ Requires-Dist: websockets>=13.0 ; extra == 'server'
13
+ Requires-Dist: whitenoise>=6.7.0 ; extra == 'server'
14
+ Requires-Python: >=3.13
15
+ Provides-Extra: server
16
+ Description-Content-Type: text/markdown
17
+
18
+ * Single database mode
19
+ * Multiple schema mode
20
+ * Local mode
@@ -0,0 +1,3 @@
1
+ * Single database mode
2
+ * Multiple schema mode
3
+ * Local mode
@@ -1,35 +1,31 @@
1
1
  [project]
2
2
  name = "varanus"
3
- version = "0.1.0"
3
+ version = "0.1.0.dev1"
4
4
  description = "Django application monitoring."
5
- authors = [
6
- { name = "Dan Watson", email = "watsond@imsweb.com" }
7
- ]
8
5
  readme = "README.md"
9
- requires-python = ">=3.11"
6
+ requires-python = ">=3.13"
10
7
  dependencies = [
11
8
  "httpx>=0.27.0",
12
9
  "msgspec>=0.19.0",
13
10
  ]
14
- classifiers = [
15
- "Intended Audience :: Developers",
16
- "Programming Language :: Python",
17
- "Programming Language :: Python :: 3",
18
- ]
19
11
 
20
12
  [project.optional-dependencies]
21
13
  server = [
22
14
  "cconf>=1.0.0",
23
- "django>=6.0; python_version>='3.12'",
24
- "django-dbtasks[serve]>=0.3.2; python_version>='3.12'",
15
+ "django~=5.2.0",
25
16
  "django-passkey-auth>=0.2.0",
17
+ "granian>=2.4.2",
26
18
  "psycopg[binary]>=3.2.1",
19
+ "websockets>=13.0",
27
20
  "whitenoise>=6.7.0",
28
21
  ]
29
22
 
30
23
  [dependency-groups]
31
24
  dev = []
32
25
 
26
+ [project.scripts]
27
+ manage = "varanus.server.__main__:manage"
28
+
33
29
  [build-system]
34
30
  requires = ["uv_build>=0.9.2,<0.10.0"]
35
31
  build-backend = "uv_build"
@@ -37,3 +33,7 @@ build-backend = "uv_build"
37
33
  [tool.ruff.lint]
38
34
  extend-select = ["I"]
39
35
  isort.known-first-party = ["varanus"]
36
+
37
+ [tool.pytest.ini_options]
38
+ addopts = "--tb=short -s"
39
+ DJANGO_SETTINGS_MODULE = "varanus.server.settings"
@@ -4,4 +4,3 @@ setup = client.setup
4
4
  context = client.context
5
5
  log = client.log
6
6
  metric = client.metric
7
- timer = client.timer
@@ -5,4 +5,4 @@ class VaranusClient(AppConfig):
5
5
  name = "varanus.client"
6
6
 
7
7
  def ready(self):
8
- pass
8
+ print("VaranusClient.ready")
@@ -0,0 +1,151 @@
1
+ import platform
2
+ from importlib.metadata import distributions
3
+ from typing import Union
4
+ from urllib.parse import urlsplit
5
+
6
+ from varanus.events import Context, NodeInfo
7
+
8
+ from ..utils import import_string
9
+ from .context import VaranusContext, current_context
10
+ from .loggers import QueryLogger
11
+ from .transport.base import BaseTransport
12
+
13
+
14
+ def install_query_logger(logger):
15
+ def handler(sender, **kwargs):
16
+ if logger not in kwargs["connection"].execute_wrappers:
17
+ kwargs["connection"].execute_wrappers.append(logger)
18
+
19
+ return handler
20
+
21
+
22
+ class VaranusClient:
23
+ environment: str | None
24
+ transport: BaseTransport
25
+
26
+ request_attr: str
27
+ logger_name: str
28
+ tags: dict
29
+
30
+ include_headers: Union[list, bool, None]
31
+ exclude_headers: list | None
32
+ sensitive_headers = set(
33
+ [
34
+ "authorization",
35
+ "cookie",
36
+ "proxy-authorization",
37
+ ]
38
+ )
39
+
40
+ scheme_transports = {
41
+ "test": "varanus.client.transport.test.TestTransport",
42
+ "http": "varanus.client.transport.http.HttpTransport",
43
+ "https": "varanus.client.transport.http.HttpTransport",
44
+ "db": "varanus.client.transport.database.ModelTransport",
45
+ }
46
+
47
+ send_all: bool
48
+ configured = False
49
+
50
+ def setup(
51
+ self,
52
+ dsn,
53
+ request_attr="varanus",
54
+ environment=None,
55
+ transport_class=None,
56
+ logger_name="varanus.request",
57
+ tags=None,
58
+ include_headers=None,
59
+ exclude_headers=None,
60
+ log_queries: bool | int = False,
61
+ log_query_params=False,
62
+ query_metrics=False,
63
+ send_all=False,
64
+ install=None,
65
+ ):
66
+ url = urlsplit(dsn)
67
+ self.environment = environment
68
+ if transport_class is None:
69
+ transport_class = self.scheme_transports.get(url.scheme)
70
+ if transport_class is None:
71
+ raise ValueError(f"No transport class found for `{url.scheme}`")
72
+ if isinstance(transport_class, str):
73
+ transport_class = import_string(transport_class)
74
+ self.transport = transport_class(url, self.environment)
75
+ self.request_attr = request_attr
76
+ self.logger_name = logger_name
77
+ self.tags = tags or {}
78
+ self.include_headers = include_headers
79
+ self.exclude_headers = exclude_headers
80
+ if log_queries or query_metrics:
81
+ try:
82
+ # The logger is installed as early as possible, and for all connections.
83
+ from django.db.backends.signals import connection_created
84
+
85
+ # Create a single QueryLogger to be used by all connections.
86
+ self.query_logger = QueryLogger(
87
+ log_queries,
88
+ log_query_params,
89
+ query_metrics,
90
+ )
91
+ # Install it in each new connection (if it's not already installed).
92
+ connection_created.connect(
93
+ install_query_logger(self.query_logger),
94
+ weak=False,
95
+ )
96
+ except ImportError:
97
+ pass
98
+ self.send_all = send_all
99
+ self.configured = True
100
+ if install:
101
+ if not isinstance(install, list):
102
+ raise TypeError(
103
+ "The varanus middleware can only be automatically installed into a list."
104
+ )
105
+ if "django.contrib.auth.middleware.AuthenticationMiddleware" in install:
106
+ idx = install.index(
107
+ "django.contrib.auth.middleware.AuthenticationMiddleware"
108
+ )
109
+ install.insert(idx + 1, "varanus.client.middleware.VaranusMiddleware")
110
+ elif "django.middleware.common.CommonMiddleware" in install:
111
+ idx = install.index("django.middleware.common.CommonMiddleware")
112
+ install.insert(idx + 1, "varanus.client.middleware.VaranusMiddleware")
113
+ else:
114
+ install.append("varanus.client.middleware.VaranusMiddleware")
115
+ return self
116
+
117
+ def send(self, *events: Context):
118
+ for e in events:
119
+ self.transport.send(e)
120
+
121
+ def ping(self):
122
+ self.transport.ping(
123
+ NodeInfo(
124
+ name=platform.node(),
125
+ platform=platform.platform(),
126
+ python_version=platform.python_version(),
127
+ packages={d.name: d.version for d in distributions()},
128
+ )
129
+ )
130
+
131
+ def log(self, level, message, *args, **kwargs):
132
+ if ctx := current_context.get():
133
+ kwargs.setdefault("stacklevel", 2)
134
+ ctx.log(level, message, *args, **kwargs)
135
+
136
+ def raw_exception(self, exception, tags: dict | None = None):
137
+ if ctx := current_context.get():
138
+ ctx.raw_exception(exception, tags=tags)
139
+
140
+ def metric(self, name, value: float = 0.0, tags: dict | None = None):
141
+ if ctx := current_context.get():
142
+ ctx.metric(name, value, tags=tags)
143
+
144
+ def context(self, name: str, tags: dict | None = None):
145
+ if ctx := current_context.get():
146
+ return ctx.context(name, tags)
147
+ else:
148
+ return VaranusContext(self, name, tags or self.tags)
149
+
150
+
151
+ client = VaranusClient()
@@ -1,8 +1,6 @@
1
- import contextlib
2
1
  import inspect
3
2
  import logging
4
3
  import sys
5
- import time
6
4
  from contextvars import ContextVar, Token
7
5
  from datetime import timedelta
8
6
  from typing import TYPE_CHECKING
@@ -80,11 +78,11 @@ class VaranusContext:
80
78
 
81
79
  def log(
82
80
  self,
83
- level: int,
84
- message: str,
81
+ level,
82
+ message,
85
83
  *args,
86
84
  exc_info=None,
87
- stacklevel: int = 1,
85
+ stacklevel=1,
88
86
  tags: dict | None = None,
89
87
  **kwargs,
90
88
  ):
@@ -101,27 +99,27 @@ class VaranusContext:
101
99
  )
102
100
  )
103
101
 
104
- def debug(self, message: str, *args, **kwargs):
102
+ def debug(self, message, *args, **kwargs):
105
103
  kwargs.setdefault("stacklevel", 2)
106
104
  self.log(logging.DEBUG, message, *args, **kwargs)
107
105
 
108
- def info(self, message: str, *args, **kwargs):
106
+ def info(self, message, *args, **kwargs):
109
107
  kwargs.setdefault("stacklevel", 2)
110
108
  self.log(logging.INFO, message, *args, **kwargs)
111
109
 
112
- def warning(self, message: str, *args, **kwargs):
110
+ def warning(self, message, *args, **kwargs):
113
111
  kwargs.setdefault("stacklevel", 2)
114
112
  self.log(logging.WARNING, message, *args, **kwargs)
115
113
 
116
- def error(self, message: str, *args, **kwargs):
114
+ def error(self, message, *args, **kwargs):
117
115
  kwargs.setdefault("stacklevel", 2)
118
116
  self.log(logging.ERROR, message, *args, **kwargs)
119
117
 
120
- def critical(self, message: str, *args, **kwargs):
118
+ def critical(self, message, *args, **kwargs):
121
119
  kwargs.setdefault("stacklevel", 2)
122
120
  self.log(logging.CRITICAL, message, *args, **kwargs)
123
121
 
124
- def exception(self, message: str, *args, **kwargs):
122
+ def exception(self, message, *args, **kwargs):
125
123
  kwargs.setdefault("stacklevel", 2)
126
124
  kwargs.setdefault("exc_info", sys.exc_info())
127
125
  self.log(logging.ERROR, message, *args, **kwargs)
@@ -130,25 +128,16 @@ class VaranusContext:
130
128
  if err := Error.from_exception(exception, tags=tags):
131
129
  self.errors.append(err)
132
130
 
133
- def context(self, name: str = "", tags: dict | None = None):
131
+ def context(self, name="", tags: dict | None = None):
134
132
  ctx = VaranusContext(self.client, name, tags)
135
133
  self.subcontexts.append(ctx)
136
134
  return ctx
137
135
 
138
- def metric(self, name: str, value: float = 0.0, tags: dict | None = None):
136
+ def metric(self, name, value: float = 0.0, tags: dict | None = None):
139
137
  if name not in self.metrics:
140
138
  self.metrics[name] = Metric(name=name, tags=tags or {})
141
139
  self.metrics[name].update(value)
142
140
 
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
141
 
153
142
  current_context: ContextVar[VaranusContext | None] = ContextVar(
154
143
  "current_context",
@@ -1,7 +1,7 @@
1
1
  import logging
2
2
  import sys
3
3
 
4
- from ..events import Log, Query, capture_stack, now
4
+ from ..events import Log, Query, 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, log_stack, metrics):
15
+ def __init__(self, threshold, log_params, metrics):
16
16
  # TODO: add callback for tagging?
17
17
  if threshold is True:
18
18
  self.threshold = 0
@@ -21,7 +21,6 @@ class QueryLogger:
21
21
  else:
22
22
  self.threshold = int(threshold)
23
23
  self.log_params = log_params
24
- self.log_stack = log_stack
25
24
  if isinstance(metrics, str):
26
25
  self.metrics_name = metrics
27
26
  else:
@@ -42,19 +41,19 @@ class QueryLogger:
42
41
  elapsed_ms = (now() - start) // ONE_MS
43
42
  if self.metrics_name:
44
43
  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
- )
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,
60
58
  )
59
+ )
@@ -1,11 +1,12 @@
1
- import ipaddress
1
+ import re
2
2
 
3
3
  from django.core.exceptions import MiddlewareNotUsed
4
4
  from django.http import HttpRequest, HttpResponse
5
- from django.utils import timezone
6
5
 
7
6
  from ..events import Request
8
- from .client import client, resolve_include_exclude
7
+ from .client import client
8
+
9
+ IP_REGEX = re.compile(r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}")
9
10
 
10
11
 
11
12
  def get_ip(request: HttpRequest):
@@ -14,21 +15,24 @@ def get_ip(request: HttpRequest):
14
15
  ip_address = ip_address.split(",")[0].strip()
15
16
  if not ip_address:
16
17
  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 ""
18
+ if not IP_REGEX.match(ip_address):
19
+ ip_address = None
20
+ return ip_address
22
21
 
23
22
 
24
23
  def request_headers(request: HttpRequest):
25
24
  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:
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):
32
36
  value = request.headers.get(name)
33
37
  if value is not None:
34
38
  headers[name] = value
@@ -41,7 +45,7 @@ class VaranusMiddleware:
41
45
  # TODO: warning
42
46
  print("VaranusClient is not configured -- disabling middleware.")
43
47
  raise MiddlewareNotUsed()
44
- self.last_ping = None
48
+ client.ping()
45
49
  self.get_response = get_response
46
50
 
47
51
  def process_exception(self, request, exception):
@@ -49,22 +53,14 @@ class VaranusMiddleware:
49
53
  client.raw_exception(exception)
50
54
 
51
55
  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
56
  with client.context(request.path) as varanus:
60
57
  setattr(request, client.request_attr, varanus)
61
58
  response = self.get_response(request)
62
59
  # TODO: any need for request tags separate from context tags?
63
60
  varanus.request = Request(
64
61
  host=request.get_host(),
65
- method=request.method or "",
66
62
  path=request.path,
67
- query=request.META.get("QUERY_STRING", ""),
63
+ method=request.method or "",
68
64
  status=response.status_code,
69
65
  headers=request_headers(request),
70
66
  size=(
@@ -4,7 +4,7 @@ from varanus import events
4
4
 
5
5
 
6
6
  class BaseTransport:
7
- def __init__(self, url: SplitResult, environment: str, node: str):
7
+ def __init__(self, url: SplitResult, environment: str):
8
8
  pass
9
9
 
10
10
  def ping(self, info: events.NodeInfo):
@@ -0,0 +1,28 @@
1
+ import uuid
2
+ from urllib.parse import SplitResult
3
+
4
+ from django.db import transaction
5
+
6
+ from varanus import events
7
+ from varanus.server import models
8
+
9
+ from .base import BaseTransport
10
+
11
+
12
+ class ModelTransport(BaseTransport):
13
+ def __init__(self, url: SplitResult, environment: str):
14
+ self.environment = environment
15
+
16
+ def ping(self, info: events.NodeInfo):
17
+ site = models.Site.objects.get()
18
+ models.Node.update(info, site=site, environment=self.environment)
19
+
20
+ @transaction.atomic
21
+ def send(self, event: events.Context):
22
+ site = models.Site.objects.get()
23
+ models.Context.from_event(
24
+ event,
25
+ event_id=uuid.uuid4(),
26
+ site=site,
27
+ environment=self.environment,
28
+ )
@@ -0,0 +1,37 @@
1
+ import platform
2
+ from typing import Any
3
+ from urllib.parse import SplitResult
4
+
5
+ import httpx
6
+ import msgspec
7
+
8
+ from varanus import events
9
+
10
+ from .base import BaseTransport
11
+
12
+
13
+ class HttpTransport(BaseTransport):
14
+ def __init__(self, url: SplitResult, environment: str):
15
+ path = url.path.rstrip("/")
16
+ self.ping_url = f"{url.scheme}://{url.netloc}{path}/api/ping/"
17
+ self.event_url = f"{url.scheme}://{url.netloc}{path}/api/ingest/"
18
+ self.client = httpx.Client(
19
+ headers={
20
+ "X-Varanus-Key": url.username or "",
21
+ "X-Varanus-Environment": environment or "",
22
+ "X-Varanus-Node": platform.node(),
23
+ },
24
+ timeout=1.0,
25
+ )
26
+
27
+ def request(self, url: str, obj: Any):
28
+ try:
29
+ self.client.post(url, content=msgspec.json.encode(obj))
30
+ except Exception as ex:
31
+ print(f"error sending to {url}: {ex}")
32
+
33
+ def ping(self, info: events.NodeInfo):
34
+ self.request(self.ping_url, info)
35
+
36
+ def send(self, event: events.Context):
37
+ self.request(self.event_url, event)
@@ -1,11 +1,8 @@
1
- import inspect
2
1
  import logging
3
2
  from datetime import datetime, timezone
4
3
 
5
4
  from msgspec import Struct, field
6
5
 
7
- from .utils import safe_repr
8
-
9
6
 
10
7
  def now():
11
8
  return datetime.now(tz=timezone.utc)
@@ -19,50 +16,23 @@ class Event(Struct, kw_only=True, omit_defaults=True):
19
16
  class NodeInfo(Struct):
20
17
  name: str
21
18
  platform: str
22
- language: str
23
- language_version: str
24
- framework: str
25
- framework_version: str
19
+ python_version: str
26
20
  packages: dict[str, str]
27
- settings: dict[str, str]
28
- environment: dict[str, str]
29
- version: str = ""
30
21
 
31
22
 
32
- class StackLine(Struct):
23
+ class ErrorLine(Struct):
33
24
  file: str | None
34
25
  lineno: int | None
35
26
  function: str | None
36
27
  module: str | None
37
- linesrc: str | None
38
28
  locals: dict[str, str]
39
29
 
40
30
 
41
- def capture_stack(skip: int = 0, include_locals: bool = False) -> list[StackLine]:
42
- lines = []
43
- for frame in inspect.stack()[skip + 1 :]:
44
- lines.append(
45
- StackLine(
46
- file=frame.filename,
47
- lineno=frame.lineno,
48
- linesrc=frame.code_context[0].strip() if frame.code_context else "",
49
- function=frame.function,
50
- module=frame.frame.f_globals.get("__name__", ""),
51
- locals=(
52
- {name: safe_repr(val) for name, val in frame.frame.f_locals.items()}
53
- if include_locals
54
- else {}
55
- ),
56
- )
57
- )
58
- return lines
59
-
60
-
61
31
  class Error(Event):
62
32
  kind: str
63
33
  module: str
64
34
  message: str
65
- stack: list[StackLine] = []
35
+ lines: list[ErrorLine] = []
66
36
 
67
37
  @classmethod
68
38
  def from_exception(cls, exc_info, tags=None):
@@ -82,22 +52,14 @@ class Error(Event):
82
52
  abs_path = f_code.co_filename if f_code else None
83
53
  function = f_code.co_name if f_code else None
84
54
  lineno = getattr(tb, "tb_lineno", None)
85
- linesrc = None
86
- if f_code and lineno:
87
- try:
88
- source, start = inspect.getsourcelines(f_code)
89
- linesrc = source[lineno - start].strip()
90
- except (OSError, TypeError):
91
- pass
92
55
  module = f_globals.get("__name__", "")
93
56
  lines.append(
94
- StackLine(
57
+ ErrorLine(
95
58
  file=abs_path,
96
59
  lineno=lineno,
97
60
  function=function,
98
61
  module=module,
99
- linesrc=linesrc,
100
- locals={name: safe_repr(val) for name, val in f_locals.items()},
62
+ locals={name: repr(val) for name, val in f_locals.items()},
101
63
  )
102
64
  )
103
65
  tb = tb.tb_next
@@ -106,7 +68,7 @@ class Error(Event):
106
68
  kind=kind,
107
69
  module=module,
108
70
  message=message,
109
- stack=lines,
71
+ lines=lines,
110
72
  )
111
73
 
112
74
 
@@ -154,8 +116,8 @@ class Metric(Event):
154
116
  agg_count: int = 0
155
117
  agg_sum: float = 0.0
156
118
  agg_avg: float = 0.0
157
- agg_min: float = float("inf")
158
- agg_max: float = float("-inf")
119
+ agg_min: float = 0.0
120
+ agg_max: float = 0.0
159
121
 
160
122
  def update(self, value: float):
161
123
  self.agg_count += 1
@@ -172,14 +134,12 @@ class Query(Event):
172
134
  db: str
173
135
  elapsed_ms: int
174
136
  success: bool
175
- stack: list[StackLine] = []
176
137
 
177
138
 
178
139
  class Request(Event):
179
140
  host: str
180
- method: str
181
141
  path: str
182
- query: str
142
+ method: str
183
143
  status: int
184
144
  headers: dict = {}
185
145
  size: int | None = None