| """
|
| REST API Endpoints for Crypto API Monitoring System
|
| Implements comprehensive monitoring, status tracking, and management endpoints
|
| """
|
|
|
| from datetime import datetime, timedelta
|
| from typing import Optional, List, Dict, Any
|
| from fastapi import APIRouter, HTTPException, Query, Body
|
| from pydantic import BaseModel, Field
|
|
|
|
|
| from database.db_manager import db_manager
|
| from config import config
|
| from monitoring.health_checker import HealthChecker
|
| from monitoring.rate_limiter import rate_limiter
|
| from utils.logger import setup_logger
|
|
|
|
|
| logger = setup_logger("api_endpoints")
|
|
|
|
|
| router = APIRouter(prefix="/api", tags=["monitoring"])
|
|
|
|
|
|
|
|
|
|
|
|
|
| class TriggerCheckRequest(BaseModel):
|
| """Request model for triggering immediate health check"""
|
| provider: str = Field(..., description="Provider name to check")
|
|
|
|
|
| class TestKeyRequest(BaseModel):
|
| """Request model for testing API key"""
|
| provider: str = Field(..., description="Provider name to test")
|
|
|
|
|
|
|
|
|
|
|
|
|
| @router.get("/status")
|
| async def get_system_status():
|
| """
|
| Get comprehensive system status overview
|
|
|
| Returns:
|
| System overview with provider counts, health metrics, and last update
|
| """
|
| try:
|
|
|
| latest_metrics = db_manager.get_latest_system_metrics()
|
|
|
| if latest_metrics:
|
| return {
|
| "total_apis": latest_metrics.total_providers,
|
| "online": latest_metrics.online_count,
|
| "degraded": latest_metrics.degraded_count,
|
| "offline": latest_metrics.offline_count,
|
| "avg_response_time_ms": round(latest_metrics.avg_response_time_ms, 2),
|
| "last_update": latest_metrics.timestamp.isoformat(),
|
| "system_health": latest_metrics.system_health
|
| }
|
|
|
|
|
| providers = db_manager.get_all_providers()
|
|
|
|
|
| status_counts = {"online": 0, "degraded": 0, "offline": 0}
|
| response_times = []
|
|
|
| for provider in providers:
|
| attempts = db_manager.get_connection_attempts(
|
| provider_id=provider.id,
|
| hours=1,
|
| limit=10
|
| )
|
|
|
| if attempts:
|
| recent = attempts[0]
|
| if recent.status == "success" and recent.response_time_ms and recent.response_time_ms < 2000:
|
| status_counts["online"] += 1
|
| response_times.append(recent.response_time_ms)
|
| elif recent.status == "success":
|
| status_counts["degraded"] += 1
|
| if recent.response_time_ms:
|
| response_times.append(recent.response_time_ms)
|
| else:
|
| status_counts["offline"] += 1
|
| else:
|
| status_counts["offline"] += 1
|
|
|
| avg_response_time = sum(response_times) / len(response_times) if response_times else 0
|
|
|
|
|
| total = len(providers)
|
| online_pct = (status_counts["online"] / total * 100) if total > 0 else 0
|
|
|
| if online_pct >= 90:
|
| system_health = "healthy"
|
| elif online_pct >= 70:
|
| system_health = "degraded"
|
| else:
|
| system_health = "unhealthy"
|
|
|
| return {
|
| "total_apis": total,
|
| "online": status_counts["online"],
|
| "degraded": status_counts["degraded"],
|
| "offline": status_counts["offline"],
|
| "avg_response_time_ms": round(avg_response_time, 2),
|
| "last_update": datetime.utcnow().isoformat(),
|
| "system_health": system_health
|
| }
|
|
|
| except Exception as e:
|
| logger.error(f"Error getting system status: {e}", exc_info=True)
|
| raise HTTPException(status_code=500, detail=f"Failed to get system status: {str(e)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
| @router.get("/categories")
|
| async def get_categories():
|
| """
|
| Get statistics for all provider categories
|
|
|
| Returns:
|
| List of category statistics with provider counts and health metrics
|
| """
|
| try:
|
| categories = config.get_categories()
|
| category_stats = []
|
|
|
| for category in categories:
|
| providers = db_manager.get_all_providers(category=category)
|
|
|
| if not providers:
|
| continue
|
|
|
| total_sources = len(providers)
|
| online_sources = 0
|
| response_times = []
|
| rate_limited_count = 0
|
| last_updated = None
|
|
|
| for provider in providers:
|
|
|
| attempts = db_manager.get_connection_attempts(
|
| provider_id=provider.id,
|
| hours=1,
|
| limit=5
|
| )
|
|
|
| if attempts:
|
| recent = attempts[0]
|
|
|
|
|
| if not last_updated or recent.timestamp > last_updated:
|
| last_updated = recent.timestamp
|
|
|
|
|
| if recent.status == "success" and recent.response_time_ms and recent.response_time_ms < 2000:
|
| online_sources += 1
|
| response_times.append(recent.response_time_ms)
|
|
|
|
|
| if recent.status == "rate_limited":
|
| rate_limited_count += 1
|
|
|
|
|
| online_ratio = round(online_sources / total_sources, 2) if total_sources > 0 else 0
|
| avg_response_time = round(sum(response_times) / len(response_times), 2) if response_times else 0
|
|
|
|
|
| if online_ratio >= 0.9:
|
| status = "healthy"
|
| elif online_ratio >= 0.7:
|
| status = "degraded"
|
| else:
|
| status = "critical"
|
|
|
| category_stats.append({
|
| "name": category,
|
| "total_sources": total_sources,
|
| "online_sources": online_sources,
|
| "online_ratio": online_ratio,
|
| "avg_response_time_ms": avg_response_time,
|
| "rate_limited_count": rate_limited_count,
|
| "last_updated": last_updated.isoformat() if last_updated else None,
|
| "status": status
|
| })
|
|
|
| return category_stats
|
|
|
| except Exception as e:
|
| logger.error(f"Error getting categories: {e}", exc_info=True)
|
| raise HTTPException(status_code=500, detail=f"Failed to get categories: {str(e)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
| @router.get("/providers")
|
| async def get_providers(
|
| category: Optional[str] = Query(None, description="Filter by category"),
|
| status: Optional[str] = Query(None, description="Filter by status (online/degraded/offline)"),
|
| search: Optional[str] = Query(None, description="Search by provider name")
|
| ):
|
| """
|
| Get list of providers with optional filtering
|
|
|
| Args:
|
| category: Filter by provider category
|
| status: Filter by provider status
|
| search: Search by provider name
|
|
|
| Returns:
|
| List of providers with detailed information
|
| """
|
| try:
|
|
|
| providers = db_manager.get_all_providers(category=category)
|
|
|
| result = []
|
|
|
| for provider in providers:
|
|
|
| if search and search.lower() not in provider.name.lower():
|
| continue
|
|
|
|
|
| attempts = db_manager.get_connection_attempts(
|
| provider_id=provider.id,
|
| hours=1,
|
| limit=10
|
| )
|
|
|
|
|
| provider_status = "offline"
|
| response_time_ms = 0
|
| last_fetch = None
|
|
|
| if attempts:
|
| recent = attempts[0]
|
| last_fetch = recent.timestamp
|
|
|
| if recent.status == "success":
|
| if recent.response_time_ms and recent.response_time_ms < 2000:
|
| provider_status = "online"
|
| else:
|
| provider_status = "degraded"
|
| response_time_ms = recent.response_time_ms or 0
|
| elif recent.status == "rate_limited":
|
| provider_status = "degraded"
|
| else:
|
| provider_status = "offline"
|
|
|
|
|
| if status and provider_status != status:
|
| continue
|
|
|
|
|
| rate_limit_status = rate_limiter.get_status(provider.name)
|
| rate_limit = None
|
| if rate_limit_status:
|
| rate_limit = f"{rate_limit_status['current_usage']}/{rate_limit_status['limit_value']} {rate_limit_status['limit_type']}"
|
| elif provider.rate_limit_type and provider.rate_limit_value:
|
| rate_limit = f"0/{provider.rate_limit_value} {provider.rate_limit_type}"
|
|
|
|
|
| schedule_config = db_manager.get_schedule_config(provider.id)
|
|
|
| result.append({
|
| "id": provider.id,
|
| "name": provider.name,
|
| "category": provider.category,
|
| "status": provider_status,
|
| "response_time_ms": response_time_ms,
|
| "rate_limit": rate_limit,
|
| "last_fetch": last_fetch.isoformat() if last_fetch else None,
|
| "has_key": provider.requires_key,
|
| "endpoints": provider.endpoint_url
|
| })
|
|
|
| return result
|
|
|
| except Exception as e:
|
| logger.error(f"Error getting providers: {e}", exc_info=True)
|
| raise HTTPException(status_code=500, detail=f"Failed to get providers: {str(e)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
| @router.get("/logs")
|
| async def get_logs(
|
| from_time: Optional[str] = Query(None, alias="from", description="Start time (ISO format)"),
|
| to_time: Optional[str] = Query(None, alias="to", description="End time (ISO format)"),
|
| provider: Optional[str] = Query(None, description="Filter by provider name"),
|
| status: Optional[str] = Query(None, description="Filter by status"),
|
| page: int = Query(1, ge=1, description="Page number"),
|
| per_page: int = Query(50, ge=1, le=500, description="Items per page")
|
| ):
|
| """
|
| Get connection attempt logs with filtering and pagination
|
|
|
| Args:
|
| from_time: Start time filter
|
| to_time: End time filter
|
| provider: Provider name filter
|
| status: Status filter
|
| page: Page number
|
| per_page: Items per page
|
|
|
| Returns:
|
| Paginated log entries with metadata
|
| """
|
| try:
|
|
|
| if from_time:
|
| from_dt = datetime.fromisoformat(from_time.replace('Z', '+00:00'))
|
| else:
|
| from_dt = datetime.utcnow() - timedelta(hours=24)
|
|
|
| if to_time:
|
| to_dt = datetime.fromisoformat(to_time.replace('Z', '+00:00'))
|
| else:
|
| to_dt = datetime.utcnow()
|
|
|
| hours = (to_dt - from_dt).total_seconds() / 3600
|
|
|
|
|
| provider_id = None
|
| if provider:
|
| prov = db_manager.get_provider(name=provider)
|
| if prov:
|
| provider_id = prov.id
|
|
|
|
|
| all_logs = db_manager.get_connection_attempts(
|
| provider_id=provider_id,
|
| status=status,
|
| hours=int(hours) + 1,
|
| limit=10000
|
| )
|
|
|
|
|
| filtered_logs = [
|
| log for log in all_logs
|
| if from_dt <= log.timestamp <= to_dt
|
| ]
|
|
|
|
|
| total = len(filtered_logs)
|
| total_pages = (total + per_page - 1) // per_page
|
| start_idx = (page - 1) * per_page
|
| end_idx = start_idx + per_page
|
|
|
|
|
| page_logs = filtered_logs[start_idx:end_idx]
|
|
|
|
|
| logs = []
|
| for log in page_logs:
|
|
|
| prov = db_manager.get_provider(provider_id=log.provider_id)
|
| provider_name = prov.name if prov else "Unknown"
|
|
|
| logs.append({
|
| "id": log.id,
|
| "timestamp": log.timestamp.isoformat(),
|
| "provider": provider_name,
|
| "endpoint": log.endpoint,
|
| "status": log.status,
|
| "response_time_ms": log.response_time_ms,
|
| "http_status_code": log.http_status_code,
|
| "error_type": log.error_type,
|
| "error_message": log.error_message,
|
| "retry_count": log.retry_count,
|
| "retry_result": log.retry_result
|
| })
|
|
|
| return {
|
| "logs": logs,
|
| "pagination": {
|
| "page": page,
|
| "per_page": per_page,
|
| "total": total,
|
| "total_pages": total_pages,
|
| "has_next": page < total_pages,
|
| "has_prev": page > 1
|
| }
|
| }
|
|
|
| except Exception as e:
|
| logger.error(f"Error getting logs: {e}", exc_info=True)
|
| raise HTTPException(status_code=500, detail=f"Failed to get logs: {str(e)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
| @router.get("/schedule")
|
| async def get_schedule():
|
| """
|
| Get schedule status for all providers
|
|
|
| Returns:
|
| List of schedule information for each provider
|
| """
|
| try:
|
| configs = db_manager.get_all_schedule_configs(enabled_only=False)
|
|
|
| schedule_list = []
|
|
|
| for config in configs:
|
|
|
| provider = db_manager.get_provider(provider_id=config.provider_id)
|
| if not provider:
|
| continue
|
|
|
|
|
| total_runs = config.on_time_count + config.late_count
|
| on_time_percentage = round((config.on_time_count / total_runs * 100), 1) if total_runs > 0 else 100.0
|
|
|
|
|
| compliance_today = db_manager.get_schedule_compliance(
|
| provider_id=config.provider_id,
|
| hours=24
|
| )
|
|
|
| total_runs_today = len(compliance_today)
|
| successful_runs = sum(1 for c in compliance_today if c.on_time)
|
| skipped_runs = config.skip_count
|
|
|
|
|
| if not config.enabled:
|
| status = "disabled"
|
| elif on_time_percentage >= 95:
|
| status = "on_schedule"
|
| elif on_time_percentage >= 80:
|
| status = "acceptable"
|
| else:
|
| status = "behind_schedule"
|
|
|
| schedule_list.append({
|
| "provider": provider.name,
|
| "category": provider.category,
|
| "schedule": config.schedule_interval,
|
| "last_run": config.last_run.isoformat() if config.last_run else None,
|
| "next_run": config.next_run.isoformat() if config.next_run else None,
|
| "on_time_percentage": on_time_percentage,
|
| "status": status,
|
| "total_runs_today": total_runs_today,
|
| "successful_runs": successful_runs,
|
| "skipped_runs": skipped_runs
|
| })
|
|
|
| return schedule_list
|
|
|
| except Exception as e:
|
| logger.error(f"Error getting schedule: {e}", exc_info=True)
|
| raise HTTPException(status_code=500, detail=f"Failed to get schedule: {str(e)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
| @router.post("/schedule/trigger")
|
| async def trigger_check(request: TriggerCheckRequest):
|
| """
|
| Trigger immediate health check for a provider
|
|
|
| Args:
|
| request: Request containing provider name
|
|
|
| Returns:
|
| Health check result
|
| """
|
| try:
|
|
|
| provider = db_manager.get_provider(name=request.provider)
|
| if not provider:
|
| raise HTTPException(status_code=404, detail=f"Provider not found: {request.provider}")
|
|
|
|
|
| checker = HealthChecker()
|
| result = await checker.check_provider(request.provider)
|
| await checker.close()
|
|
|
| if not result:
|
| raise HTTPException(status_code=500, detail=f"Health check failed for {request.provider}")
|
|
|
| return {
|
| "provider": result.provider_name,
|
| "status": result.status.value,
|
| "response_time_ms": result.response_time,
|
| "timestamp": datetime.fromtimestamp(result.timestamp).isoformat(),
|
| "error_message": result.error_message,
|
| "triggered_at": datetime.utcnow().isoformat()
|
| }
|
|
|
| except HTTPException:
|
| raise
|
| except Exception as e:
|
| logger.error(f"Error triggering check: {e}", exc_info=True)
|
| raise HTTPException(status_code=500, detail=f"Failed to trigger check: {str(e)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
| @router.get("/freshness")
|
| async def get_freshness():
|
| """
|
| Get data freshness information for all providers
|
|
|
| Returns:
|
| List of data freshness metrics
|
| """
|
| try:
|
| providers = db_manager.get_all_providers()
|
| freshness_list = []
|
|
|
| for provider in providers:
|
|
|
| collections = db_manager.get_data_collections(
|
| provider_id=provider.id,
|
| hours=24,
|
| limit=1
|
| )
|
|
|
| if not collections:
|
| continue
|
|
|
| collection = collections[0]
|
|
|
|
|
| now = datetime.utcnow()
|
| fetch_age_minutes = (now - collection.actual_fetch_time).total_seconds() / 60
|
|
|
|
|
| ttl_minutes = 5
|
| if provider.category == "market_data":
|
| ttl_minutes = 1
|
| elif provider.category == "blockchain_explorers":
|
| ttl_minutes = 5
|
| elif provider.category == "news":
|
| ttl_minutes = 15
|
|
|
|
|
| if fetch_age_minutes <= ttl_minutes:
|
| status = "fresh"
|
| elif fetch_age_minutes <= ttl_minutes * 2:
|
| status = "stale"
|
| else:
|
| status = "expired"
|
|
|
| freshness_list.append({
|
| "provider": provider.name,
|
| "category": provider.category,
|
| "fetch_time": collection.actual_fetch_time.isoformat(),
|
| "data_timestamp": collection.data_timestamp.isoformat() if collection.data_timestamp else None,
|
| "staleness_minutes": round(fetch_age_minutes, 2),
|
| "ttl_minutes": ttl_minutes,
|
| "status": status
|
| })
|
|
|
| return freshness_list
|
|
|
| except Exception as e:
|
| logger.error(f"Error getting freshness: {e}", exc_info=True)
|
| raise HTTPException(status_code=500, detail=f"Failed to get freshness: {str(e)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
| @router.get("/failures")
|
| async def get_failures():
|
| """
|
| Get comprehensive failure analysis
|
|
|
| Returns:
|
| Failure analysis with error distribution and recommendations
|
| """
|
| try:
|
|
|
| analysis = db_manager.get_failure_analysis(hours=24)
|
|
|
|
|
| recent_failures = db_manager.get_failure_logs(hours=1, limit=10)
|
|
|
| recent_list = []
|
| for failure in recent_failures:
|
| provider = db_manager.get_provider(provider_id=failure.provider_id)
|
| recent_list.append({
|
| "timestamp": failure.timestamp.isoformat(),
|
| "provider": provider.name if provider else "Unknown",
|
| "error_type": failure.error_type,
|
| "error_message": failure.error_message,
|
| "http_status": failure.http_status,
|
| "retry_attempted": failure.retry_attempted,
|
| "retry_result": failure.retry_result
|
| })
|
|
|
|
|
| remediation_suggestions = []
|
|
|
| error_type_distribution = analysis.get('failures_by_error_type', [])
|
| for error_stat in error_type_distribution:
|
| error_type = error_stat['error_type']
|
| count = error_stat['count']
|
|
|
| if error_type == 'timeout' and count > 5:
|
| remediation_suggestions.append({
|
| "issue": "High timeout rate",
|
| "suggestion": "Increase timeout values or check network connectivity",
|
| "priority": "high"
|
| })
|
| elif error_type == 'rate_limit' and count > 3:
|
| remediation_suggestions.append({
|
| "issue": "Rate limit errors",
|
| "suggestion": "Implement request throttling or add additional API keys",
|
| "priority": "medium"
|
| })
|
| elif error_type == 'auth_error' and count > 0:
|
| remediation_suggestions.append({
|
| "issue": "Authentication failures",
|
| "suggestion": "Verify API keys are valid and not expired",
|
| "priority": "critical"
|
| })
|
|
|
| return {
|
| "error_type_distribution": error_type_distribution,
|
| "top_failing_providers": analysis.get('top_failing_providers', []),
|
| "recent_failures": recent_list,
|
| "remediation_suggestions": remediation_suggestions
|
| }
|
|
|
| except Exception as e:
|
| logger.error(f"Error getting failures: {e}", exc_info=True)
|
| raise HTTPException(status_code=500, detail=f"Failed to get failures: {str(e)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
| @router.get("/rate-limits")
|
| async def get_rate_limits():
|
| """
|
| Get rate limit status for all providers
|
|
|
| Returns:
|
| List of rate limit information
|
| """
|
| try:
|
| statuses = rate_limiter.get_all_statuses()
|
|
|
| rate_limit_list = []
|
|
|
| for provider_name, status_info in statuses.items():
|
| if status_info:
|
| rate_limit_list.append({
|
| "provider": status_info['provider'],
|
| "limit_type": status_info['limit_type'],
|
| "limit_value": status_info['limit_value'],
|
| "current_usage": status_info['current_usage'],
|
| "percentage": status_info['percentage'],
|
| "reset_time": status_info['reset_time'],
|
| "reset_in_seconds": status_info['reset_in_seconds'],
|
| "status": status_info['status']
|
| })
|
|
|
|
|
| providers = db_manager.get_all_providers()
|
| tracked_providers = {rl['provider'] for rl in rate_limit_list}
|
|
|
| for provider in providers:
|
| if provider.name not in tracked_providers and provider.rate_limit_type and provider.rate_limit_value:
|
| rate_limit_list.append({
|
| "provider": provider.name,
|
| "limit_type": provider.rate_limit_type,
|
| "limit_value": provider.rate_limit_value,
|
| "current_usage": 0,
|
| "percentage": 0.0,
|
| "reset_time": (datetime.utcnow() + timedelta(hours=1)).isoformat(),
|
| "reset_in_seconds": 3600,
|
| "status": "ok"
|
| })
|
|
|
| return rate_limit_list
|
|
|
| except Exception as e:
|
| logger.error(f"Error getting rate limits: {e}", exc_info=True)
|
| raise HTTPException(status_code=500, detail=f"Failed to get rate limits: {str(e)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
| @router.get("/config/keys")
|
| async def get_api_keys():
|
| """
|
| Get API key status for all providers
|
|
|
| Returns:
|
| List of API key information (masked)
|
| """
|
| try:
|
| providers = db_manager.get_all_providers()
|
|
|
| keys_list = []
|
|
|
| for provider in providers:
|
| if not provider.requires_key:
|
| continue
|
|
|
|
|
| if provider.api_key_masked:
|
| key_status = "configured"
|
| else:
|
| key_status = "missing"
|
|
|
|
|
| rate_status = rate_limiter.get_status(provider.name)
|
| usage_quota_remaining = None
|
| if rate_status:
|
| percentage_used = rate_status['percentage']
|
| usage_quota_remaining = f"{100 - percentage_used:.1f}%"
|
|
|
| keys_list.append({
|
| "provider": provider.name,
|
| "key_masked": provider.api_key_masked or "***NOT_SET***",
|
| "created_at": provider.created_at.isoformat(),
|
| "expires_at": None,
|
| "status": key_status,
|
| "usage_quota_remaining": usage_quota_remaining
|
| })
|
|
|
| return keys_list
|
|
|
| except Exception as e:
|
| logger.error(f"Error getting API keys: {e}", exc_info=True)
|
| raise HTTPException(status_code=500, detail=f"Failed to get API keys: {str(e)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
| @router.post("/config/keys/test")
|
| async def test_api_key(request: TestKeyRequest):
|
| """
|
| Test an API key by performing a health check
|
|
|
| Args:
|
| request: Request containing provider name
|
|
|
| Returns:
|
| Test result
|
| """
|
| try:
|
|
|
| provider = db_manager.get_provider(name=request.provider)
|
| if not provider:
|
| raise HTTPException(status_code=404, detail=f"Provider not found: {request.provider}")
|
|
|
| if not provider.requires_key:
|
| raise HTTPException(status_code=400, detail=f"Provider {request.provider} does not require an API key")
|
|
|
| if not provider.api_key_masked:
|
| raise HTTPException(status_code=400, detail=f"No API key configured for {request.provider}")
|
|
|
|
|
| checker = HealthChecker()
|
| result = await checker.check_provider(request.provider)
|
| await checker.close()
|
|
|
| if not result:
|
| raise HTTPException(status_code=500, detail=f"Failed to test API key for {request.provider}")
|
|
|
|
|
| key_valid = result.status.value == "online" or result.status.value == "degraded"
|
|
|
|
|
| if result.error_message and ('auth' in result.error_message.lower() or 'key' in result.error_message.lower() or '401' in result.error_message or '403' in result.error_message):
|
| key_valid = False
|
|
|
| return {
|
| "provider": request.provider,
|
| "key_valid": key_valid,
|
| "test_timestamp": datetime.utcnow().isoformat(),
|
| "response_time_ms": result.response_time,
|
| "status_code": result.status_code,
|
| "error_message": result.error_message,
|
| "test_endpoint": result.endpoint_tested
|
| }
|
|
|
| except HTTPException:
|
| raise
|
| except Exception as e:
|
| logger.error(f"Error testing API key: {e}", exc_info=True)
|
| raise HTTPException(status_code=500, detail=f"Failed to test API key: {str(e)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
| @router.get("/charts/health-history")
|
| async def get_health_history(
|
| hours: int = Query(24, ge=1, le=168, description="Hours of history to retrieve")
|
| ):
|
| """
|
| Get health history data for charts
|
|
|
| Args:
|
| hours: Number of hours of history to retrieve
|
|
|
| Returns:
|
| Time series data for health metrics
|
| """
|
| try:
|
|
|
| metrics = db_manager.get_system_metrics(hours=hours)
|
|
|
| if not metrics:
|
| return {
|
| "timestamps": [],
|
| "success_rate": [],
|
| "avg_response_time": []
|
| }
|
|
|
|
|
| metrics.sort(key=lambda x: x.timestamp)
|
|
|
| timestamps = []
|
| success_rates = []
|
| avg_response_times = []
|
|
|
| for metric in metrics:
|
| timestamps.append(metric.timestamp.isoformat())
|
|
|
|
|
| total = metric.online_count + metric.degraded_count + metric.offline_count
|
| success_rate = round((metric.online_count / total * 100), 2) if total > 0 else 0
|
| success_rates.append(success_rate)
|
|
|
| avg_response_times.append(round(metric.avg_response_time_ms, 2))
|
|
|
| return {
|
| "timestamps": timestamps,
|
| "success_rate": success_rates,
|
| "avg_response_time": avg_response_times
|
| }
|
|
|
| except Exception as e:
|
| logger.error(f"Error getting health history: {e}", exc_info=True)
|
| raise HTTPException(status_code=500, detail=f"Failed to get health history: {str(e)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
| @router.get("/charts/compliance")
|
| async def get_compliance_history(
|
| days: int = Query(7, ge=1, le=30, description="Days of history to retrieve")
|
| ):
|
| """
|
| Get schedule compliance history for charts
|
|
|
| Args:
|
| days: Number of days of history to retrieve
|
|
|
| Returns:
|
| Time series data for compliance metrics
|
| """
|
| try:
|
|
|
| configs = db_manager.get_all_schedule_configs(enabled_only=True)
|
|
|
| if not configs:
|
| return {
|
| "dates": [],
|
| "compliance_percentage": []
|
| }
|
|
|
|
|
| end_date = datetime.utcnow().date()
|
| dates = []
|
| compliance_percentages = []
|
|
|
| for day_offset in range(days - 1, -1, -1):
|
| current_date = end_date - timedelta(days=day_offset)
|
| dates.append(current_date.isoformat())
|
|
|
|
|
| day_start = datetime.combine(current_date, datetime.min.time())
|
| day_end = datetime.combine(current_date, datetime.max.time())
|
|
|
| total_checks = 0
|
| on_time_checks = 0
|
|
|
| for config in configs:
|
| compliance_records = db_manager.get_schedule_compliance(
|
| provider_id=config.provider_id,
|
| hours=24
|
| )
|
|
|
|
|
| day_records = [
|
| r for r in compliance_records
|
| if day_start <= r.timestamp <= day_end
|
| ]
|
|
|
| total_checks += len(day_records)
|
| on_time_checks += sum(1 for r in day_records if r.on_time)
|
|
|
|
|
| compliance_pct = round((on_time_checks / total_checks * 100), 2) if total_checks > 0 else 100.0
|
| compliance_percentages.append(compliance_pct)
|
|
|
| return {
|
| "dates": dates,
|
| "compliance_percentage": compliance_percentages
|
| }
|
|
|
| except Exception as e:
|
| logger.error(f"Error getting compliance history: {e}", exc_info=True)
|
| raise HTTPException(status_code=500, detail=f"Failed to get compliance history: {str(e)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
| @router.get("/charts/rate-limit-history")
|
| async def get_rate_limit_history(
|
| hours: int = Query(24, ge=1, le=168, description="Hours of history to retrieve")
|
| ):
|
| """
|
| Get rate limit usage history data for charts
|
|
|
| Args:
|
| hours: Number of hours of history to retrieve
|
|
|
| Returns:
|
| Time series data for rate limit usage by provider
|
| """
|
| try:
|
|
|
| providers = db_manager.get_all_providers()
|
| providers_with_limits = [p for p in providers if p.rate_limit_type and p.rate_limit_value]
|
|
|
| if not providers_with_limits:
|
| return {
|
| "timestamps": [],
|
| "providers": []
|
| }
|
|
|
|
|
| end_time = datetime.utcnow()
|
| start_time = end_time - timedelta(hours=hours)
|
|
|
|
|
| timestamps = []
|
| current_time = start_time
|
| while current_time <= end_time:
|
| timestamps.append(current_time.strftime("%H:%M"))
|
| current_time += timedelta(hours=1)
|
|
|
|
|
| provider_data = []
|
|
|
| for provider in providers_with_limits[:5]:
|
|
|
| rate_limit_records = db_manager.get_rate_limit_usage(
|
| provider_id=provider.id,
|
| hours=hours
|
| )
|
|
|
| if not rate_limit_records:
|
| continue
|
|
|
|
|
| usage_percentages = []
|
| current_time = start_time
|
|
|
| for _ in range(len(timestamps)):
|
| hour_end = current_time + timedelta(hours=1)
|
|
|
|
|
| hour_records = [
|
| r for r in rate_limit_records
|
| if current_time <= r.timestamp < hour_end
|
| ]
|
|
|
| if hour_records:
|
|
|
| avg_percentage = sum(r.percentage for r in hour_records) / len(hour_records)
|
| usage_percentages.append(round(avg_percentage, 2))
|
| else:
|
|
|
| usage_percentages.append(0.0)
|
|
|
| current_time = hour_end
|
|
|
| provider_data.append({
|
| "name": provider.name,
|
| "usage_percentage": usage_percentages
|
| })
|
|
|
| return {
|
| "timestamps": timestamps,
|
| "providers": provider_data
|
| }
|
|
|
| except Exception as e:
|
| logger.error(f"Error getting rate limit history: {e}", exc_info=True)
|
| raise HTTPException(status_code=500, detail=f"Failed to get rate limit history: {str(e)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
| @router.get("/charts/freshness-history")
|
| async def get_freshness_history(
|
| hours: int = Query(24, ge=1, le=168, description="Hours of history to retrieve")
|
| ):
|
| """
|
| Get data freshness (staleness) history for charts
|
|
|
| Args:
|
| hours: Number of hours of history to retrieve
|
|
|
| Returns:
|
| Time series data for data staleness by provider
|
| """
|
| try:
|
|
|
| providers = db_manager.get_all_providers()
|
|
|
| if not providers:
|
| return {
|
| "timestamps": [],
|
| "providers": []
|
| }
|
|
|
|
|
| end_time = datetime.utcnow()
|
| start_time = end_time - timedelta(hours=hours)
|
|
|
|
|
| timestamps = []
|
| current_time = start_time
|
| while current_time <= end_time:
|
| timestamps.append(current_time.strftime("%H:%M"))
|
| current_time += timedelta(hours=1)
|
|
|
|
|
| provider_data = []
|
|
|
| for provider in providers[:5]:
|
|
|
| collections = db_manager.get_data_collections(
|
| provider_id=provider.id,
|
| hours=hours,
|
| limit=1000
|
| )
|
|
|
| if not collections:
|
| continue
|
|
|
|
|
| staleness_values = []
|
| current_time = start_time
|
|
|
| for _ in range(len(timestamps)):
|
| hour_end = current_time + timedelta(hours=1)
|
|
|
|
|
| hour_records = [
|
| c for c in collections
|
| if current_time <= c.actual_fetch_time < hour_end
|
| ]
|
|
|
| if hour_records:
|
|
|
| staleness_list = []
|
| for record in hour_records:
|
| if record.staleness_minutes is not None:
|
| staleness_list.append(record.staleness_minutes)
|
| elif record.data_timestamp and record.actual_fetch_time:
|
|
|
| staleness_seconds = (record.actual_fetch_time - record.data_timestamp).total_seconds()
|
| staleness_minutes = staleness_seconds / 60
|
| staleness_list.append(staleness_minutes)
|
|
|
| if staleness_list:
|
| avg_staleness = sum(staleness_list) / len(staleness_list)
|
| staleness_values.append(round(avg_staleness, 2))
|
| else:
|
| staleness_values.append(0.0)
|
| else:
|
|
|
| staleness_values.append(None)
|
|
|
| current_time = hour_end
|
|
|
|
|
| if any(v is not None and v > 0 for v in staleness_values):
|
| provider_data.append({
|
| "name": provider.name,
|
| "staleness_minutes": staleness_values
|
| })
|
|
|
| return {
|
| "timestamps": timestamps,
|
| "providers": provider_data
|
| }
|
|
|
| except Exception as e:
|
| logger.error(f"Error getting freshness history: {e}", exc_info=True)
|
| raise HTTPException(status_code=500, detail=f"Failed to get freshness history: {str(e)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
| @router.get("/health")
|
| async def api_health():
|
| """
|
| API health check endpoint
|
|
|
| Returns:
|
| API health status
|
| """
|
| try:
|
|
|
| db_health = db_manager.health_check()
|
|
|
| return {
|
| "status": "healthy" if db_health['status'] == 'healthy' else "unhealthy",
|
| "timestamp": datetime.utcnow().isoformat(),
|
| "database": db_health['status'],
|
| "version": "1.0.0"
|
| }
|
| except Exception as e:
|
| logger.error(f"Health check failed: {e}", exc_info=True)
|
| return {
|
| "status": "unhealthy",
|
| "timestamp": datetime.utcnow().isoformat(),
|
| "error": str(e),
|
| "version": "1.0.0"
|
| }
|
|
|
|
|
|
|
|
|
|
|
|
|
| logger.info("API endpoints module loaded successfully")
|
|
|