fastapi-radar 0.1.9__py3-none-any.whl → 0.2.0__py3-none-any.whl
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.
Potentially problematic release.
This version of fastapi-radar might be problematic. Click here for more details.
- fastapi_radar/__init__.py +1 -1
- fastapi_radar/capture.py +38 -10
- fastapi_radar/dashboard/dist/assets/{index-Dj9HCQum.js → index-31zorKsE.js} +62 -67
- fastapi_radar/dashboard/dist/index.html +1 -1
- fastapi_radar/middleware.py +15 -7
- {fastapi_radar-0.1.9.dist-info → fastapi_radar-0.2.0.dist-info}/METADATA +2 -1
- fastapi_radar-0.2.0.dist-info/RECORD +19 -0
- tests/test_async_radar.py +56 -0
- fastapi_radar-0.1.9.dist-info/RECORD +0 -18
- {fastapi_radar-0.1.9.dist-info → fastapi_radar-0.2.0.dist-info}/WHEEL +0 -0
- {fastapi_radar-0.1.9.dist-info → fastapi_radar-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {fastapi_radar-0.1.9.dist-info → fastapi_radar-0.2.0.dist-info}/top_level.txt +0 -0
fastapi_radar/__init__.py
CHANGED
fastapi_radar/capture.py
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
"""SQLAlchemy query capture for FastAPI Radar."""
|
|
2
2
|
|
|
3
3
|
import time
|
|
4
|
-
from typing import Any, Callable, Dict, List, Union
|
|
5
|
-
|
|
4
|
+
from typing import Any, Callable, Dict, List, Optional, Union
|
|
6
5
|
from sqlalchemy import event
|
|
7
6
|
from sqlalchemy.engine import Engine
|
|
8
7
|
|
|
8
|
+
try: # SQLAlchemy async support is optional
|
|
9
|
+
from sqlalchemy.ext.asyncio import AsyncEngine
|
|
10
|
+
except Exception: # pragma: no cover - module might not exist in older SQLAlchemy
|
|
11
|
+
AsyncEngine = None # type: ignore[assignment]
|
|
9
12
|
from .middleware import request_context
|
|
10
13
|
from .models import CapturedQuery
|
|
14
|
+
|
|
15
|
+
|
|
11
16
|
from .utils import format_sql
|
|
12
17
|
from .tracing import get_current_trace_context
|
|
13
18
|
|
|
@@ -23,14 +28,20 @@ class QueryCapture:
|
|
|
23
28
|
self.capture_bindings = capture_bindings
|
|
24
29
|
self.slow_query_threshold = slow_query_threshold
|
|
25
30
|
self._query_start_times = {}
|
|
31
|
+
self._registered_engines: Dict[int, Engine] = {}
|
|
26
32
|
|
|
27
33
|
def register(self, engine: Engine) -> None:
|
|
28
|
-
|
|
29
|
-
event.listen(
|
|
34
|
+
sync_engine = self._resolve_engine(engine)
|
|
35
|
+
event.listen(sync_engine, "before_cursor_execute", self._before_cursor_execute)
|
|
36
|
+
event.listen(sync_engine, "after_cursor_execute", self._after_cursor_execute)
|
|
37
|
+
self._registered_engines[id(engine)] = sync_engine
|
|
30
38
|
|
|
31
39
|
def unregister(self, engine: Engine) -> None:
|
|
32
|
-
|
|
33
|
-
|
|
40
|
+
sync_engine = self._registered_engines.pop(id(engine), None)
|
|
41
|
+
if not sync_engine:
|
|
42
|
+
sync_engine = self._resolve_engine(engine)
|
|
43
|
+
event.remove(sync_engine, "before_cursor_execute", self._before_cursor_execute)
|
|
44
|
+
event.remove(sync_engine, "after_cursor_execute", self._after_cursor_execute)
|
|
34
45
|
|
|
35
46
|
def _before_cursor_execute(
|
|
36
47
|
self,
|
|
@@ -44,14 +55,14 @@ class QueryCapture:
|
|
|
44
55
|
request_id = request_context.get()
|
|
45
56
|
if not request_id:
|
|
46
57
|
return
|
|
47
|
-
|
|
48
58
|
context_id = id(context)
|
|
49
59
|
self._query_start_times[context_id] = time.time()
|
|
50
|
-
|
|
60
|
+
setattr(context, "_radar_request_id", request_id)
|
|
51
61
|
trace_ctx = get_current_trace_context()
|
|
52
62
|
if trace_ctx:
|
|
53
63
|
formatted_sql = format_sql(statement)
|
|
54
64
|
operation_type = self._get_operation_type(statement)
|
|
65
|
+
db_tags = self._get_db_tags(conn)
|
|
55
66
|
span_id = trace_ctx.create_span(
|
|
56
67
|
operation_name=f"DB {operation_type}",
|
|
57
68
|
span_kind="client",
|
|
@@ -59,6 +70,7 @@ class QueryCapture:
|
|
|
59
70
|
"db.statement": formatted_sql[:500], # limit SQL length
|
|
60
71
|
"db.operation_type": operation_type,
|
|
61
72
|
"component": "database",
|
|
73
|
+
**db_tags,
|
|
62
74
|
},
|
|
63
75
|
)
|
|
64
76
|
setattr(context, "_radar_span_id", span_id)
|
|
@@ -74,8 +86,9 @@ class QueryCapture:
|
|
|
74
86
|
) -> None:
|
|
75
87
|
request_id = request_context.get()
|
|
76
88
|
if not request_id:
|
|
77
|
-
|
|
78
|
-
|
|
89
|
+
request_id = getattr(context, "_radar_request_id", None)
|
|
90
|
+
if not request_id:
|
|
91
|
+
return
|
|
79
92
|
start_time = self._query_start_times.pop(id(context), None)
|
|
80
93
|
if start_time is None:
|
|
81
94
|
return
|
|
@@ -160,3 +173,18 @@ class QueryCapture:
|
|
|
160
173
|
return {k: str(v) for k, v in list(parameters.items())[:100]}
|
|
161
174
|
|
|
162
175
|
return [str(parameters)]
|
|
176
|
+
|
|
177
|
+
def _resolve_engine(self, engine: Engine) -> Engine:
|
|
178
|
+
if AsyncEngine is not None and isinstance(engine, AsyncEngine):
|
|
179
|
+
return engine.sync_engine
|
|
180
|
+
return engine
|
|
181
|
+
|
|
182
|
+
def _get_db_tags(self, conn: Any) -> Dict[str, Optional[str]]:
|
|
183
|
+
tags: Dict[str, Optional[str]] = {}
|
|
184
|
+
engine = getattr(conn, "engine", None)
|
|
185
|
+
if engine and getattr(engine, "dialect", None):
|
|
186
|
+
tags["db.system"] = engine.dialect.name
|
|
187
|
+
url = getattr(engine, "url", None)
|
|
188
|
+
if url is not None:
|
|
189
|
+
tags["db.name"] = getattr(url, "database", None)
|
|
190
|
+
return tags
|