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