Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,85 @@ radar = Radar(
exclude_paths=["/health"], # Paths to exclude from monitoring
theme="auto", # Dashboard theme: "light", "dark", or "auto"
db_path="/path/to/db", # Custom path for radar.duckdb file (default: current directory)
auth_dependency=None, # Optional: Authentication dependency for dashboard and API access
)
```

## Securing the Dashboard

By default, FastAPI Radar is accessible without authentication. For production environments, you should add authentication to protect your monitoring dashboard:

### HTTP Basic Authentication

```python
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials
from fastapi_radar import Radar
import secrets

app = FastAPI()
security = HTTPBasic()

def verify_credentials(credentials: HTTPBasicCredentials = Depends(security)):
correct_username = secrets.compare_digest(credentials.username, "admin")
correct_password = secrets.compare_digest(credentials.password, "secret")
if not (correct_username and correct_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid credentials",
headers={"WWW-Authenticate": "Basic"},
)
return credentials

radar = Radar(app, auth_dependency=verify_credentials)
radar.create_tables()
```

### Bearer Token Authentication

```python
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from fastapi_radar import Radar

app = FastAPI()
security = HTTPBearer()

def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)):
if credentials.credentials != "your-secret-token":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token",
)
return credentials

radar = Radar(app, auth_dependency=verify_token)
radar.create_tables()
```

### Custom Authentication

You can use any FastAPI dependency for authentication:

```python
from fastapi import FastAPI, Depends, HTTPException, Request
from fastapi_radar import Radar

app = FastAPI()

async def custom_auth(request: Request):
# Your custom authentication logic
api_key = request.headers.get("X-API-Key")
if api_key != "your-api-key":
raise HTTPException(status_code=401, detail="Unauthorized")
return True

radar = Radar(app, auth_dependency=custom_auth)
radar.create_tables()
```

The `auth_dependency` parameter accepts any FastAPI dependency function, giving you full flexibility to implement OAuth2, JWT, API keys, or any other authentication mechanism.

### Custom Database Location

By default, FastAPI Radar stores its monitoring data in a `radar.duckdb` file in your current working directory. You can customize this location using the `db_path` parameter:
Expand Down
19 changes: 19 additions & 0 deletions example_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,13 +95,32 @@ class Config:
version="1.0.0",
)

# Optional: Setup authentication for Radar dashboard
# Uncomment the following lines to secure the dashboard with HTTP Basic Auth
# from fastapi.security import HTTPBasic, HTTPBasicCredentials
# import secrets
#
# security = HTTPBasic()
#
# def verify_radar_credentials(credentials: HTTPBasicCredentials = Depends(security)):
# correct_username = secrets.compare_digest(credentials.username, "admin")
# correct_password = secrets.compare_digest(credentials.password, "secret")
# if not (correct_username and correct_password):
# raise HTTPException(
# status_code=401,
# detail="Invalid credentials",
# headers={"WWW-Authenticate": "Basic"},
# )
# return credentials

# Initialize Radar - automatically adds middleware and mounts dashboard
radar = Radar(
app,
db_engine=engine,
dashboard_path="/__radar",
slow_query_threshold=50,
theme="auto",
# auth_dependency=verify_radar_credentials, # Uncomment to enable authentication
)
radar.create_tables()

Expand Down
74 changes: 74 additions & 0 deletions example_auth_app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
"""Minimal example showing how to secure FastAPI Radar with authentication."""

import secrets
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials
from sqlalchemy import create_engine

from fastapi_radar import Radar

# Create FastAPI app
app = FastAPI(title="FastAPI Radar with Authentication")

# Setup HTTP Basic Authentication
security = HTTPBasic()


def verify_radar_access(credentials: HTTPBasicCredentials = Depends(security)):
"""Verify credentials for Radar dashboard access."""
correct_username = secrets.compare_digest(credentials.username, "admin")
correct_password = secrets.compare_digest(credentials.password, "secret")

if not (correct_username and correct_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid credentials",
headers={"WWW-Authenticate": "Basic"},
)
return credentials


# Setup database (optional)
engine = create_engine("sqlite:///./app.db")

# Initialize Radar with authentication
radar = Radar(
app,
db_engine=engine,
auth_dependency=verify_radar_access, # Secure the dashboard
)
radar.create_tables()


# Your regular API endpoints (not protected by Radar auth)
@app.get("/")
async def root():
return {
"message": "Public API endpoint",
"dashboard": "Visit /__radar (requires auth: admin/secret)",
}


@app.get("/public")
async def public_endpoint():
return {"message": "This endpoint is public"}


if __name__ == "__main__":
import uvicorn

print("\n" + "=" * 60)
print("🔒 FastAPI Radar with Authentication")
print("=" * 60)
print("\nCredentials:")
print(" Username: admin")
print(" Password: secret")
print("\nEndpoints:")
print(" API (public): http://localhost:8000")
print(" Dashboard: http://localhost:8000/__radar (protected)")
print("\nTry accessing the dashboard:")
print(" Browser: http://localhost:8000/__radar")
print(" CLI: curl -u admin:secret http://localhost:8000/__radar/api/stats")
print("=" * 60 + "\n")

uvicorn.run(app, host="0.0.0.0", port=8000)
48 changes: 48 additions & 0 deletions example_bearer_auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"""Minimal example showing Bearer token authentication for FastAPI Radar."""

from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials

from fastapi_radar import Radar

app = FastAPI(title="FastAPI Radar with Bearer Token")
security = HTTPBearer()


def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)):
"""Verify bearer token for Radar access."""
if credentials.credentials != "my-secret-token-123":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token",
)
return credentials


# Initialize Radar with token authentication
radar = Radar(app, auth_dependency=verify_token)
radar.create_tables()


@app.get("/")
async def root():
return {
"message": "Use Bearer token to access dashboard",
"token": "my-secret-token-123",
"dashboard": "/__radar",
}


if __name__ == "__main__":
import uvicorn

print("\n" + "=" * 60)
print("🔑 FastAPI Radar with Bearer Token")
print("=" * 60)
print("\nToken: my-secret-token-123")
print("\nAccess dashboard:")
print(' curl -H "Authorization: Bearer my-secret-token-123" \\')
print(" http://localhost:8000/__radar/api/stats")
print("=" * 60 + "\n")

uvicorn.run(app, host="0.0.0.0", port=8000)
45 changes: 45 additions & 0 deletions example_custom_auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""Minimal example showing custom authentication for FastAPI Radar."""

from fastapi import FastAPI, HTTPException, Request

from fastapi_radar import Radar

app = FastAPI(title="FastAPI Radar with Custom Auth")


async def verify_api_key(request: Request):
"""Custom authentication using API key from header."""
api_key = request.headers.get("X-API-Key")

if api_key != "radar-secret-key":
raise HTTPException(status_code=401, detail="Invalid or missing API key")

return True


# Initialize Radar with custom authentication
radar = Radar(app, auth_dependency=verify_api_key)
radar.create_tables()


@app.get("/")
async def root():
return {
"message": "Use X-API-Key header to access dashboard",
"header": "X-API-Key: radar-secret-key",
}


if __name__ == "__main__":
import uvicorn

print("\n" + "=" * 60)
print("🔐 FastAPI Radar with Custom API Key")
print("=" * 60)
print("\nAPI Key: radar-secret-key")
print("\nAccess dashboard:")
print(' curl -H "X-API-Key: radar-secret-key" \\')
print(" http://localhost:8000/__radar/api/stats")
print("=" * 60 + "\n")

uvicorn.run(app, host="0.0.0.0", port=8000)
2 changes: 1 addition & 1 deletion fastapi_radar/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
from .radar import Radar
from .background import track_background_task

__version__ = "0.3.3"
__version__ = "0.3.4"
__all__ = ["Radar", "track_background_task"]
13 changes: 10 additions & 3 deletions fastapi_radar/api.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""API endpoints for FastAPI Radar dashboard."""

from datetime import datetime, timedelta, timezone
from typing import Any, Dict, List, Optional, Union
from typing import Any, Callable, Dict, List, Optional, Union
import uuid

from fastapi import APIRouter, Depends, HTTPException, Query
Expand Down Expand Up @@ -142,8 +142,15 @@ class TraceDetail(BaseModel):
spans: List[WaterfallSpan]


def create_api_router(get_session_context) -> APIRouter:
router = APIRouter(prefix="/__radar/api", tags=["radar"])
def create_api_router(
get_session_context, auth_dependency: Optional[Callable] = None
) -> APIRouter:
# Build dependencies list for the router
dependencies = []
if auth_dependency:
dependencies.append(Depends(auth_dependency))

router = APIRouter(prefix="/__radar/api", tags=["radar"], dependencies=dependencies)

def get_db():
"""Dependency function for FastAPI to get database session."""
Expand Down
14 changes: 11 additions & 3 deletions fastapi_radar/radar.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import sys
import multiprocessing
from pathlib import Path
from typing import List, Optional, Union
from typing import Callable, List, Optional, Union
import asyncio

from fastapi import FastAPI
Expand Down Expand Up @@ -61,6 +61,7 @@ def __init__(
service_name: str = "fastapi-app",
include_in_schema: bool = True,
db_path: Optional[str] = None,
auth_dependency: Optional[Callable] = None,
):
self.app = app
self.db_engine = db_engine
Expand All @@ -74,6 +75,7 @@ def __init__(
self.enable_tracing = enable_tracing
self.service_name = service_name
self.db_path = db_path
self.auth_dependency = auth_dependency
self.query_capture = None

if dashboard_path not in self.exclude_paths:
Expand Down Expand Up @@ -209,12 +211,12 @@ def _setup_query_capture(self) -> None:

def _setup_api(self, include_in_schema: bool) -> None:
"""Mount API endpoints."""
api_router = create_api_router(self.get_session)
api_router = create_api_router(self.get_session, self.auth_dependency)
self.app.include_router(api_router, include_in_schema=include_in_schema)

def _setup_dashboard(self, include_in_schema: bool) -> None:
"""Mount dashboard static files."""
from fastapi import Request
from fastapi import Depends, Request
from fastapi.responses import FileResponse

dashboard_dir = Path(__file__).parent / "dashboard" / "dist"
Expand All @@ -231,9 +233,15 @@ def _setup_dashboard(self, include_in_schema: bool) -> None:
print(" npm run build")
print("=" * 60 + "\n")

# Prepare dependencies list for the route
dependencies = []
if self.auth_dependency:
dependencies.append(Depends(self.auth_dependency))

@self.app.get(
f"{self.dashboard_path}/{{full_path:path}}",
include_in_schema=include_in_schema,
dependencies=dependencies,
)
async def serve_dashboard(request: Request, full_path: str = ""):
if full_path and any(
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "fastapi-radar"
version = "0.3.3"
version = "0.3.4"
description = "A debugging dashboard for FastAPI applications with real-time monitoring"
readme = "README.md"
requires-python = ">=3.9"
Expand Down