from fastapi import APIRouter, HTTPException import logging from datetime import datetime, timedelta from database.connection import db_connection from models.sync_models import HealthCheck, EstadoSincronizacion import time router = APIRouter(prefix="/api", tags=["health"]) logger = logging.getLogger(__name__) # Variable global para tracking del tiempo de inicio start_time = time.time() def get_uptime() -> str: """Calcula el tiempo de actividad del servicio""" uptime_seconds = time.time() - start_time if uptime_seconds < 60: return f"{int(uptime_seconds)} segundos" elif uptime_seconds < 3600: minutes = int(uptime_seconds / 60) seconds = int(uptime_seconds % 60) return f"{minutes}m {seconds}s" elif uptime_seconds < 86400: hours = int(uptime_seconds / 3600) minutes = int((uptime_seconds % 3600) / 60) return f"{hours}h {minutes}m" else: days = int(uptime_seconds / 86400) hours = int((uptime_seconds % 86400) / 3600) return f"{days}d {hours}h" @router.get("/health", response_model=HealthCheck, summary="Verificar salud del sistema", description="Retorna el estado general de salud del sistema de monitoreo") async def health_check(): """ Endpoint de health check que proporciona información sobre: - Estado general del sistema - Conectividad con la base de datos - Estadísticas generales de nodos - Tiempo de actividad del servicio - Versión de la API """ try: logger.info("Ejecutando health check del sistema") # Probar conexión a ambas bases de datos main_db_status = "healthy" backup_db_status = "healthy" try: main_db_test = db_connection.test_connection("main") if not main_db_test.get("connection_successful", False): main_db_status = "unhealthy" except Exception as e: logger.error(f"Error en conexión a BD principal durante health check: {str(e)}") main_db_status = "unhealthy" try: backup_db_test = db_connection.test_connection("backup") if not backup_db_test.get("connection_successful", False): backup_db_status = "unhealthy" except Exception as e: logger.error(f"Error en conexión a BD backup durante health check: {str(e)}") backup_db_status = "unhealthy" # Estado general de BD (ambas deben estar healthy) db_status = "healthy" if main_db_status == "healthy" and backup_db_status == "healthy" else "unhealthy" # Obtener estadísticas de nodos si la BD está disponible total_nodes = 0 nodes_healthy = 0 nodes_attention = 0 nodes_error = 0 if db_status == "healthy": try: # Consultar estadísticas de nodos por estado stats_query = """ SELECT CASE WHEN Activo = 1 THEN 'Actualizada' ELSE 'Atención' END as EstadoSincronizacion, COUNT(DISTINCT NodoSubNodo) as NodeCount FROM BasesdeDatos GROUP BY Activo """ results = db_connection.execute_query(stats_query, server_type="main") status_mapping = { EstadoSincronizacion.ACTUALIZADA.value: 'healthy', EstadoSincronizacion.ATENCION.value: 'attention', EstadoSincronizacion.ERROR.value: 'error' } for row in results: estado = row.get('EstadoSincronizacion', '') count = row.get('NodeCount', 0) total_nodes += count if estado == EstadoSincronizacion.ACTUALIZADA.value: nodes_healthy += count elif estado == EstadoSincronizacion.ATENCION.value: nodes_attention += count elif estado == EstadoSincronizacion.ERROR.value: nodes_error += count logger.info(f"Estadísticas de nodos - Total: {total_nodes}, " f"Healthy: {nodes_healthy}, Atención: {nodes_attention}, Error: {nodes_error}") except Exception as e: logger.error(f"Error obteniendo estadísticas de nodos: {str(e)}") # Mantener valores por defecto si hay error # Determinar estado general del sistema overall_status = "healthy" if db_status != "healthy": overall_status = "unhealthy" elif nodes_error > 0 or (total_nodes > 0 and nodes_healthy / total_nodes < 0.5): overall_status = "degraded" elif nodes_attention > 0: overall_status = "degraded" # Crear respuesta del health check health_response = HealthCheck( status=overall_status, database_status=db_status, total_nodes=total_nodes, nodes_healthy=nodes_healthy, nodes_attention=nodes_attention, nodes_error=nodes_error, last_check=datetime.now(), uptime=get_uptime(), version="1.0.0" ) logger.info(f"Health check completado - Estado: {overall_status}") return health_response except Exception as e: logger.error(f"Error crítico durante health check: {str(e)}") # Retornar estado de error crítico return HealthCheck( status="unhealthy", database_status="error", total_nodes=0, nodes_healthy=0, nodes_attention=0, nodes_error=0, last_check=datetime.now(), uptime=get_uptime(), version="1.0.0" ) @router.get("/ping", summary="Ping simple", description="Endpoint simple para verificar que la API está respondiendo") async def ping(): """ Endpoint simple de ping para verificación básica de disponibilidad. Útil para load balancers y monitoreo básico. """ return { "message": "pong", "timestamp": datetime.now().isoformat(), "status": "ok" }