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 CHANGED
@@ -2,5 +2,5 @@
2
2
 
3
3
  from .radar import Radar
4
4
 
5
- __version__ = "0.1.8"
5
+ __version__ = "0.2.0"
6
6
  __all__ = ["Radar"]
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.utcnow() - timedelta(hours=hours)
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.utcnow() - timedelta(hours=older_than_hours)
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.utcnow() - timedelta(hours=hours)
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
- event.listen(engine, "before_cursor_execute", self._before_cursor_execute)
29
- event.listen(engine, "after_cursor_execute", self._after_cursor_execute)
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
- event.remove(engine, "before_cursor_execute", self._before_cursor_execute)
33
- event.remove(engine, "after_cursor_execute", self._after_cursor_execute)
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
- return
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