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