pet_health_triage_v2 / app /api /monitoring.py
abhijeet071's picture
Pet Health Triage AI v2
545a4e2
Raw
History Blame Contribute Delete
7.02 kB
"""
Monitoring endpoints for token usage, performance metrics, and health checks.
Endpoints:
- GET /metrics/tokens - Token usage statistics
- GET /metrics/tokens/recent - Recent token usage records
- GET /metrics/performance - Performance metrics
- GET /metrics/health - Detailed health status
"""
from fastapi import APIRouter, Depends, Query
from datetime import datetime, timedelta
from typing import Optional
from app.utils.token_tracker import token_tracker
from app.utils.logging_config import get_owner_id
from app.conversation.store import conversation_store
from app.config.settings import settings
import psutil
import os
router = APIRouter(prefix="/metrics", tags=["monitoring"])
@router.get("/tokens")
async def get_token_stats(
owner_id: Optional[str] = Query(None),
):
"""
Get token usage statistics.
**Query Parameters:**
- `owner_id` (optional): Filter stats for specific user (defaults to current user from header)
**Headers:**
- `owner-id`: User identifier (required)
**Response:**
```json
{
"total_requests": 150,
"total_tokens": 45000,
"total_cost_usd": 0.85,
"average_tokens_per_request": 300,
"average_cost_per_request": 0.0057,
"by_feature": {
"text_triage": {
"requests": 100,
"tokens": 30000,
"cost_usd": 0.60
}
},
"by_status": {
"success": {
"count": 148,
"total_tokens": 44000,
"total_cost_usd": 0.84
}
}
}
```
"""
# Get user from context (set by middleware)
current_user = get_owner_id()
# Allow query param to override (for admin viewing other users)
target_user = owner_id or current_user
if not target_user:
return {
"error": "owner-id header required",
"total_requests": 0,
"total_tokens": 0,
"total_cost_usd": 0
}
return token_tracker.get_stats(target_user)
@router.get("/tokens/recent")
async def get_recent_token_records(limit: int = Query(50, le=500)):
"""
Get recent token usage records.
**Query Parameters:**
- `limit` (optional): Number of records to return, max 500 (default: 50)
**Headers:**
- `owner-id`: User identifier (required)
**Response:**
```json
[
{
"timestamp": "2026-01-25T12:30:45.123456",
"feature": "text_triage",
"input_tokens": 250,
"output_tokens": 150,
"total_tokens": 400,
"cost_usd": 0.007,
"owner_id": "user_123",
"model": "gemini-1.5-flash",
"status": "success",
"latency_ms": 2345.6
}
]
```
"""
# Get user from context
current_user = get_owner_id()
if not current_user:
return {"error": "owner-id header required", "records": []}
records = token_tracker.get_recent_records(limit)
# Filter to only show user's own records
filtered = [r for r in records if r["owner_id"] == current_user]
return filtered
@router.get("/performance")
async def get_performance_metrics():
"""
Get system performance metrics.
**Response:**
```json
{
"cpu_percent": 12.5,
"memory_percent": 45.2,
"memory_mb": 1234,
"disk_percent": 67.8,
"process_threads": 8,
"uptime_hours": 24.5
}
```
"""
process = psutil.Process(os.getpid())
return {
"timestamp": datetime.utcnow().isoformat(),
"cpu_percent": process.cpu_percent(interval=1),
"memory_mb": process.memory_info().rss / 1024 / 1024,
"memory_percent": process.memory_percent(),
"disk_percent": psutil.disk_usage('/').percent,
"process_threads": process.num_threads(),
"conversation_store_size": len(conversation_store.conversations),
"total_token_records": len(token_tracker.records)
}
@router.get("/health/detailed")
async def get_detailed_health():
"""
Get detailed health status with all subsystems.
**Response:**
```json
{
"status": "healthy",
"timestamp": "2026-01-25T12:30:45.123456",
"components": {
"api": {
"status": "healthy",
"response_time_ms": 1.2
},
"conversation_store": {
"status": "healthy",
"conversations": 45,
"memory_mb": 12.5
},
"logging": {
"status": "healthy",
"records": 1234
},
"rate_limiter": {
"status": "healthy",
"active_users": 12
}
},
"uptime": {
"hours": 24.5,
"requests_processed": 5000,
"errors": 2
}
}
```
"""
process = psutil.Process(os.getpid())
return {
"status": "healthy",
"timestamp": datetime.utcnow().isoformat(),
"components": {
"api": {
"status": "healthy",
"version": settings.api_version
},
"conversation_store": {
"status": "healthy",
"conversations": len(conversation_store.conversations),
"conversations_by_user": len(set(
state.user_id for state in conversation_store.conversations.values()
)),
"stats": conversation_store.get_stats()
},
"token_tracking": {
"status": "healthy",
"records": len(token_tracker.records),
"current_month_cost_usd": token_tracker.get_stats().get("total_cost_usd", 0)
},
"performance": {
"cpu_percent": process.cpu_percent(interval=0.5),
"memory_percent": process.memory_percent(),
"memory_mb": process.memory_info().rss / 1024 / 1024,
"disk_percent": psutil.disk_usage('/').percent
}
}
}
@router.get("/tokens/export")
async def export_token_records(
owner_id: Optional[str] = Query(None)
):
"""
Export token records as CSV.
**Query Parameters:**
- `owner_id` (optional): Filter export for specific user (defaults to current user from header)
**Headers:**
- `owner-id`: User identifier (required if owner_id not provided)
**Returns:** CSV file download
"""
import tempfile
from fastapi.responses import FileResponse
current_user = get_owner_id()
target_user = owner_id or current_user
if not target_user:
return {"error": "owner-id header required"}
# Create temporary file
with tempfile.NamedTemporaryFile(mode='w', suffix='.csv', delete=False) as f:
temp_path = f.name
token_tracker.export_csv(temp_path, target_user)
return FileResponse(
path=temp_path,
filename=f"token_usage_{target_user}_{datetime.utcnow().strftime('%Y%m%d')}.csv",
media_type="text/csv"
)