fastapi-radar 0.1.8__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/api.py +4 -4
- fastapi_radar/capture.py +38 -10
- fastapi_radar/dashboard/dist/assets/{index-By5DXl8Z.js → index-31zorKsE.js} +62 -67
- fastapi_radar/dashboard/dist/index.html +1 -1
- fastapi_radar/middleware.py +15 -7
- fastapi_radar/models.py +20 -8
- fastapi_radar/radar.py +73 -10
- fastapi_radar/tracing.py +6 -6
- {fastapi_radar-0.1.8.dist-info → fastapi_radar-0.2.0.dist-info}/METADATA +19 -1
- fastapi_radar-0.2.0.dist-info/RECORD +19 -0
- tests/test_async_radar.py +56 -0
- fastapi_radar-0.1.8.dist-info/RECORD +0 -18
- {fastapi_radar-0.1.8.dist-info → fastapi_radar-0.2.0.dist-info}/WHEEL +0 -0
- {fastapi_radar-0.1.8.dist-info → fastapi_radar-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {fastapi_radar-0.1.8.dist-info → fastapi_radar-0.2.0.dist-info}/top_level.txt +0 -0
fastapi_radar/__init__.py
CHANGED
fastapi_radar/api.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""API endpoints for FastAPI Radar dashboard."""
|
|
2
2
|
|
|
3
|
-
from datetime import datetime, timedelta
|
|
3
|
+
from datetime import datetime, timedelta, timezone
|
|
4
4
|
from typing import Any, Dict, List, Optional, Union
|
|
5
5
|
|
|
6
6
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
@@ -302,7 +302,7 @@ def create_api_router(get_session_context) -> APIRouter:
|
|
|
302
302
|
slow_threshold: int = Query(100),
|
|
303
303
|
session: Session = Depends(get_db),
|
|
304
304
|
):
|
|
305
|
-
since = datetime.
|
|
305
|
+
since = datetime.now(timezone.utc) - timedelta(hours=hours)
|
|
306
306
|
|
|
307
307
|
requests = (
|
|
308
308
|
session.query(CapturedRequest)
|
|
@@ -359,7 +359,7 @@ def create_api_router(get_session_context) -> APIRouter:
|
|
|
359
359
|
older_than_hours: Optional[int] = None, session: Session = Depends(get_db)
|
|
360
360
|
):
|
|
361
361
|
if older_than_hours:
|
|
362
|
-
cutoff = datetime.
|
|
362
|
+
cutoff = datetime.now(timezone.utc) - timedelta(hours=older_than_hours)
|
|
363
363
|
session.query(CapturedRequest).filter(
|
|
364
364
|
CapturedRequest.created_at < cutoff
|
|
365
365
|
).delete()
|
|
@@ -382,7 +382,7 @@ def create_api_router(get_session_context) -> APIRouter:
|
|
|
382
382
|
session: Session = Depends(get_db),
|
|
383
383
|
):
|
|
384
384
|
"""List traces."""
|
|
385
|
-
since = datetime.
|
|
385
|
+
since = datetime.now(timezone.utc) - timedelta(hours=hours)
|
|
386
386
|
query = session.query(Trace).filter(Trace.created_at >= since)
|
|
387
387
|
|
|
388
388
|
if status:
|
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
|