feat: metrics
This commit is contained in:
@@ -0,0 +1,351 @@
|
||||
"""
|
||||
Monitoring API Routes for NFOGuard
|
||||
Provides health checks, metrics, and system status endpoints
|
||||
"""
|
||||
from fastapi import APIRouter, Response, HTTPException
|
||||
from typing import Dict, Any, Optional
|
||||
import time
|
||||
|
||||
from monitoring.health import health_checker, HealthStatus
|
||||
from monitoring.metrics import metrics
|
||||
try:
|
||||
from config.validator import get_configuration_summary
|
||||
except ImportError:
|
||||
def get_configuration_summary():
|
||||
return {"status": "Configuration validator not available"}
|
||||
|
||||
|
||||
router = APIRouter(prefix="/api/v1", tags=["monitoring"])
|
||||
|
||||
|
||||
@router.get("/health")
|
||||
async def get_health_status():
|
||||
"""
|
||||
Get comprehensive health status
|
||||
|
||||
Returns detailed health information including:
|
||||
- Overall system health
|
||||
- Individual component health checks
|
||||
- Performance metrics
|
||||
- Error status
|
||||
"""
|
||||
try:
|
||||
health_status = await health_checker.get_full_health_status()
|
||||
|
||||
# Set appropriate HTTP status code
|
||||
if health_status.status == HealthStatus.HEALTHY:
|
||||
status_code = 200
|
||||
elif health_status.status == HealthStatus.DEGRADED:
|
||||
status_code = 200 # Still operational
|
||||
else: # UNHEALTHY
|
||||
status_code = 503 # Service unavailable
|
||||
|
||||
return Response(
|
||||
content=health_status.to_dict(),
|
||||
status_code=status_code,
|
||||
media_type="application/json"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
# Return unhealthy status if health check itself fails
|
||||
return Response(
|
||||
content={
|
||||
"status": "unhealthy",
|
||||
"message": f"Health check failed: {e}",
|
||||
"timestamp": time.time()
|
||||
},
|
||||
status_code=500,
|
||||
media_type="application/json"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/health/ready")
|
||||
async def get_readiness_status():
|
||||
"""
|
||||
Kubernetes readiness probe endpoint
|
||||
|
||||
Returns 200 if service is ready to accept traffic
|
||||
Returns 503 if service is not ready
|
||||
"""
|
||||
try:
|
||||
readiness = await health_checker.get_readiness_status()
|
||||
|
||||
status_code = 200 if readiness["ready"] else 503
|
||||
|
||||
return Response(
|
||||
content=readiness,
|
||||
status_code=status_code,
|
||||
media_type="application/json"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return Response(
|
||||
content={
|
||||
"ready": False,
|
||||
"message": f"Readiness check failed: {e}",
|
||||
"timestamp": time.time()
|
||||
},
|
||||
status_code=503,
|
||||
media_type="application/json"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/health/live")
|
||||
async def get_liveness_status():
|
||||
"""
|
||||
Kubernetes liveness probe endpoint
|
||||
|
||||
Returns 200 if service is alive and responsive
|
||||
Returns 500 if service should be restarted
|
||||
"""
|
||||
try:
|
||||
liveness = await health_checker.get_liveness_status()
|
||||
|
||||
status_code = 200 if liveness["alive"] else 500
|
||||
|
||||
return Response(
|
||||
content=liveness,
|
||||
status_code=status_code,
|
||||
media_type="application/json"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return Response(
|
||||
content={
|
||||
"alive": False,
|
||||
"message": f"Liveness check failed: {e}",
|
||||
"timestamp": time.time()
|
||||
},
|
||||
status_code=500,
|
||||
media_type="application/json"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/metrics")
|
||||
async def get_prometheus_metrics():
|
||||
"""
|
||||
Prometheus-compatible metrics endpoint
|
||||
|
||||
Returns metrics in Prometheus text format for scraping
|
||||
"""
|
||||
try:
|
||||
prometheus_metrics = metrics.get_prometheus_metrics()
|
||||
|
||||
return Response(
|
||||
content=prometheus_metrics,
|
||||
media_type="text/plain; charset=utf-8"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Failed to generate metrics: {e}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/metrics/json")
|
||||
async def get_metrics_json():
|
||||
"""
|
||||
Get all metrics in JSON format
|
||||
|
||||
Returns structured metrics data including:
|
||||
- System metrics (CPU, memory, disk)
|
||||
- Processing metrics (rates, durations)
|
||||
- Error metrics (counts, recent errors)
|
||||
"""
|
||||
try:
|
||||
all_metrics = metrics.get_all_metrics()
|
||||
return all_metrics
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Failed to get metrics: {e}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/status")
|
||||
async def get_system_status():
|
||||
"""
|
||||
Get comprehensive system status
|
||||
|
||||
Returns detailed system information including:
|
||||
- Health status
|
||||
- Configuration summary
|
||||
- Performance metrics
|
||||
- Recent activity
|
||||
"""
|
||||
try:
|
||||
# Get health status
|
||||
health_status = await health_checker.get_full_health_status()
|
||||
|
||||
# Get metrics
|
||||
all_metrics = metrics.get_all_metrics()
|
||||
|
||||
# Get configuration summary
|
||||
config_summary = get_configuration_summary()
|
||||
|
||||
# Combine into comprehensive status
|
||||
status_response = {
|
||||
"overall_status": health_status.status.value,
|
||||
"timestamp": time.time(),
|
||||
"uptime_seconds": health_status.uptime_seconds,
|
||||
"version": health_status.version,
|
||||
"health": health_status.to_dict(),
|
||||
"metrics": all_metrics,
|
||||
"configuration": config_summary,
|
||||
"summary": {
|
||||
"service_healthy": health_status.status in [HealthStatus.HEALTHY, HealthStatus.DEGRADED],
|
||||
"total_webhooks_processed": all_metrics["processing"]["total_webhooks"],
|
||||
"total_nfo_files_created": all_metrics["processing"]["total_nfo_created"],
|
||||
"total_errors": all_metrics["processing"]["total_errors"],
|
||||
"current_processing_rate": all_metrics["processing"]["webhooks_received_per_minute"],
|
||||
"average_processing_time": all_metrics["processing"]["average_processing_time_seconds"]
|
||||
}
|
||||
}
|
||||
|
||||
return status_response
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Failed to get system status: {e}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/status/brief")
|
||||
async def get_brief_status():
|
||||
"""
|
||||
Get brief system status for quick monitoring
|
||||
|
||||
Returns essential status information without detailed metrics
|
||||
"""
|
||||
try:
|
||||
# Get basic health
|
||||
liveness = await health_checker.get_liveness_status()
|
||||
readiness = await health_checker.get_readiness_status()
|
||||
|
||||
# Get basic metrics
|
||||
processing_metrics = metrics.get_processing_metrics()
|
||||
system_metrics = metrics.get_system_metrics()
|
||||
|
||||
return {
|
||||
"status": "healthy" if liveness["alive"] and readiness["ready"] else "unhealthy",
|
||||
"alive": liveness["alive"],
|
||||
"ready": readiness["ready"],
|
||||
"uptime_seconds": liveness["uptime_seconds"],
|
||||
"webhooks_per_minute": processing_metrics["webhooks_received_per_minute"],
|
||||
"active_operations": processing_metrics["active_operations"],
|
||||
"total_errors": processing_metrics["total_errors"],
|
||||
"cpu_percent": system_metrics.get("cpu_percent", 0),
|
||||
"memory_percent": system_metrics.get("memory_percent", 0),
|
||||
"timestamp": time.time()
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Failed to get brief status: {e}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/metrics/processing")
|
||||
async def get_processing_metrics():
|
||||
"""
|
||||
Get processing-specific metrics
|
||||
|
||||
Returns metrics focused on NFO processing performance
|
||||
"""
|
||||
try:
|
||||
processing_metrics = metrics.get_processing_metrics()
|
||||
return processing_metrics
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Failed to get processing metrics: {e}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/metrics/errors")
|
||||
async def get_error_metrics():
|
||||
"""
|
||||
Get error-specific metrics
|
||||
|
||||
Returns error counts, types, and recent error information
|
||||
"""
|
||||
try:
|
||||
error_metrics = metrics.get_error_metrics()
|
||||
return error_metrics
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Failed to get error metrics: {e}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/metrics/system")
|
||||
async def get_system_metrics():
|
||||
"""
|
||||
Get system resource metrics
|
||||
|
||||
Returns CPU, memory, disk, and process information
|
||||
"""
|
||||
try:
|
||||
system_metrics = metrics.get_system_metrics()
|
||||
return system_metrics
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Failed to get system metrics: {e}"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/metrics/reset")
|
||||
async def reset_metrics(metric_types: Optional[str] = None):
|
||||
"""
|
||||
Reset specific metric types
|
||||
|
||||
Parameters:
|
||||
- metric_types: Comma-separated list of metric types to reset
|
||||
(counters, histograms, errors, timeseries)
|
||||
If not specified, resets all metrics
|
||||
"""
|
||||
try:
|
||||
reset_types = None
|
||||
if metric_types:
|
||||
reset_types = [t.strip() for t in metric_types.split(",")]
|
||||
|
||||
metrics.reset_metrics(reset_types)
|
||||
|
||||
return {
|
||||
"message": "Metrics reset successfully",
|
||||
"reset_types": reset_types or "all",
|
||||
"timestamp": time.time()
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Failed to reset metrics: {e}"
|
||||
)
|
||||
|
||||
|
||||
# Legacy endpoints for backwards compatibility
|
||||
@router.get("/health-check")
|
||||
async def legacy_health_check():
|
||||
"""Legacy health check endpoint (redirects to /health)"""
|
||||
return await get_brief_status()
|
||||
|
||||
|
||||
@router.get("/ping")
|
||||
async def ping():
|
||||
"""Simple ping endpoint for basic connectivity testing"""
|
||||
return {
|
||||
"message": "pong",
|
||||
"timestamp": time.time(),
|
||||
"service": "nfoguard",
|
||||
"version": "2.0.0"
|
||||
}
|
||||
+5
-1
@@ -867,4 +867,8 @@ def register_routes(app, dependencies: dict):
|
||||
|
||||
@app.get("/debug/tmdb/{imdb_id}")
|
||||
async def _debug_tmdb_lookup(imdb_id: str):
|
||||
return await debug_tmdb_lookup(imdb_id, dependencies)
|
||||
return await debug_tmdb_lookup(imdb_id, dependencies)
|
||||
|
||||
# Include monitoring routes
|
||||
from api.monitoring_routes import router as monitoring_router
|
||||
app.include_router(monitoring_router)
|
||||
Reference in New Issue
Block a user