""" 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" )