fastapi-radar 0.1.8__tar.gz → 0.2.0__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.
Potentially problematic release.
This version of fastapi-radar might be problematic. Click here for more details.
- {fastapi_radar-0.1.8/fastapi_radar.egg-info → fastapi_radar-0.2.0}/PKG-INFO +19 -1
- {fastapi_radar-0.1.8 → fastapi_radar-0.2.0}/README.md +17 -0
- {fastapi_radar-0.1.8 → fastapi_radar-0.2.0}/fastapi_radar/__init__.py +1 -1
- {fastapi_radar-0.1.8 → fastapi_radar-0.2.0}/fastapi_radar/api.py +4 -4
- {fastapi_radar-0.1.8 → fastapi_radar-0.2.0}/fastapi_radar/capture.py +38 -10
- fastapi_radar-0.1.8/fastapi_radar/dashboard/dist/assets/index-By5DXl8Z.js → fastapi_radar-0.2.0/fastapi_radar/dashboard/dist/assets/index-31zorKsE.js +62 -67
- {fastapi_radar-0.1.8 → fastapi_radar-0.2.0}/fastapi_radar/dashboard/dist/index.html +1 -1
- {fastapi_radar-0.1.8 → fastapi_radar-0.2.0}/fastapi_radar/middleware.py +15 -7
- {fastapi_radar-0.1.8 → fastapi_radar-0.2.0}/fastapi_radar/models.py +20 -8
- {fastapi_radar-0.1.8 → fastapi_radar-0.2.0}/fastapi_radar/radar.py +73 -10
- {fastapi_radar-0.1.8 → fastapi_radar-0.2.0}/fastapi_radar/tracing.py +6 -6
- {fastapi_radar-0.1.8 → fastapi_radar-0.2.0/fastapi_radar.egg-info}/PKG-INFO +19 -1
- {fastapi_radar-0.1.8 → fastapi_radar-0.2.0}/fastapi_radar.egg-info/SOURCES.txt +2 -1
- {fastapi_radar-0.1.8 → fastapi_radar-0.2.0}/fastapi_radar.egg-info/requires.txt +1 -0
- {fastapi_radar-0.1.8 → fastapi_radar-0.2.0}/pyproject.toml +2 -1
- {fastapi_radar-0.1.8 → fastapi_radar-0.2.0}/setup.py +1 -1
- fastapi_radar-0.2.0/tests/test_async_radar.py +56 -0
- {fastapi_radar-0.1.8 → fastapi_radar-0.2.0}/CONTRIBUTING.md +0 -0
- {fastapi_radar-0.1.8 → fastapi_radar-0.2.0}/LICENSE +0 -0
- {fastapi_radar-0.1.8 → fastapi_radar-0.2.0}/MANIFEST.in +0 -0
- {fastapi_radar-0.1.8 → fastapi_radar-0.2.0}/fastapi_radar/dashboard/dist/assets/index-XlGcZj49.css +0 -0
- {fastapi_radar-0.1.8 → fastapi_radar-0.2.0}/fastapi_radar/utils.py +0 -0
- {fastapi_radar-0.1.8 → fastapi_radar-0.2.0}/fastapi_radar.egg-info/dependency_links.txt +0 -0
- {fastapi_radar-0.1.8 → fastapi_radar-0.2.0}/fastapi_radar.egg-info/not-zip-safe +0 -0
- {fastapi_radar-0.1.8 → fastapi_radar-0.2.0}/fastapi_radar.egg-info/top_level.txt +0 -0
- {fastapi_radar-0.1.8 → fastapi_radar-0.2.0}/setup.cfg +0 -0
- {fastapi_radar-0.1.8 → fastapi_radar-0.2.0}/tests/__init__.py +0 -0
- {fastapi_radar-0.1.8 → fastapi_radar-0.2.0}/tests/test_radar.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-radar
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: A debugging dashboard for FastAPI applications with real-time monitoring
|
|
5
5
|
Home-page: https://github.com/doganarif/fastapi-radar
|
|
6
6
|
Author: Arif Dogan
|
|
@@ -29,6 +29,7 @@ Requires-Dist: pydantic
|
|
|
29
29
|
Requires-Dist: starlette
|
|
30
30
|
Requires-Dist: duckdb==1.1.3
|
|
31
31
|
Requires-Dist: duckdb-engine==0.17.0
|
|
32
|
+
Requires-Dist: aiosqlite>=0.21.0
|
|
32
33
|
Provides-Extra: dev
|
|
33
34
|
Requires-Dist: pytest; extra == "dev"
|
|
34
35
|
Requires-Dist: pytest-asyncio; extra == "dev"
|
|
@@ -163,6 +164,23 @@ radar = Radar(app, db_path="./data")
|
|
|
163
164
|
|
|
164
165
|
If the specified path cannot be created, FastAPI Radar will fallback to using the current directory with a warning.
|
|
165
166
|
|
|
167
|
+
### Development Mode with Auto-Reload
|
|
168
|
+
|
|
169
|
+
When running your FastAPI application with `fastapi dev` (which uses auto-reload), FastAPI Radar automatically switches to an in-memory database to avoid file locking issues. This means:
|
|
170
|
+
|
|
171
|
+
- **No file locking errors** - The dashboard will work seamlessly in development
|
|
172
|
+
- **Data doesn't persist between reloads** - Each reload starts with a fresh database
|
|
173
|
+
- **Production behavior unchanged** - When using `fastapi run` or deploying, the normal file-based database is used
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
# With fastapi dev (auto-reload enabled):
|
|
177
|
+
# Automatically uses in-memory database - no configuration needed!
|
|
178
|
+
radar = Radar(app)
|
|
179
|
+
radar.create_tables() # Safe to call - handles multiple processes gracefully
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
This behavior only applies when using the development server with auto-reload (`fastapi dev`). In production or when using `fastapi run`, the standard file-based DuckDB storage is used.
|
|
183
|
+
|
|
166
184
|
## What Gets Captured?
|
|
167
185
|
|
|
168
186
|
- ✅ HTTP requests and responses
|
|
@@ -118,6 +118,23 @@ radar = Radar(app, db_path="./data")
|
|
|
118
118
|
|
|
119
119
|
If the specified path cannot be created, FastAPI Radar will fallback to using the current directory with a warning.
|
|
120
120
|
|
|
121
|
+
### Development Mode with Auto-Reload
|
|
122
|
+
|
|
123
|
+
When running your FastAPI application with `fastapi dev` (which uses auto-reload), FastAPI Radar automatically switches to an in-memory database to avoid file locking issues. This means:
|
|
124
|
+
|
|
125
|
+
- **No file locking errors** - The dashboard will work seamlessly in development
|
|
126
|
+
- **Data doesn't persist between reloads** - Each reload starts with a fresh database
|
|
127
|
+
- **Production behavior unchanged** - When using `fastapi run` or deploying, the normal file-based database is used
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
# With fastapi dev (auto-reload enabled):
|
|
131
|
+
# Automatically uses in-memory database - no configuration needed!
|
|
132
|
+
radar = Radar(app)
|
|
133
|
+
radar.create_tables() # Safe to call - handles multiple processes gracefully
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
This behavior only applies when using the development server with auto-reload (`fastapi dev`). In production or when using `fastapi run`, the standard file-based DuckDB storage is used.
|
|
137
|
+
|
|
121
138
|
## What Gets Captured?
|
|
122
139
|
|
|
123
140
|
- ✅ HTTP requests and responses
|
|
@@ -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:
|
|
@@ -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
|