feat: Implementación completa Fase 4 - Sistema de monitoreo con API REST y WebSocket

 Nuevas funcionalidades:
- API REST unificada en puerto 8080 (eliminado CORS)
- WebSocket para logs en tiempo real desde journalctl
- Integración completa con systemd para gestión de servicios
- Escaneo automático de procesos Node.js y Python
- Rate limiting (1 operación/segundo por app)
- Interface web moderna con Tailwind CSS (tema oscuro)
- Documentación API estilo Swagger completamente en español

🎨 Interface Web (todas las páginas en español):
- Dashboard con estadísticas en tiempo real
- Visor de escaneo de procesos con filtros
- Formulario de registro de aplicaciones con variables de entorno
- Visor de logs en tiempo real con WebSocket y sidebar
- Página de selección de apps detectadas
- Documentación completa de API REST

🏗️ Arquitectura:
- Módulo models: ServiceConfig, ManagedApp, AppStatus
- Módulo systemd: wrapper de systemctl, generador de .service, parser
- Módulo orchestrator: AppManager, LifecycleManager con validaciones
- Módulo api: handlers REST, WebSocket manager, DTOs
- Servidor unificado en puerto 8080 (Web + API + WS)

🔧 Mejoras técnicas:
- Eliminación de CORS mediante servidor unificado
- Separación clara frontend/backend con carga dinámica
- Thread-safe con Arc<DashMap> para estado compartido
- Reconciliación de estados: sysinfo vs systemd
- Validaciones de paths, usuarios y configuraciones
- Manejo robusto de errores con thiserror

📝 Documentación:
- README.md actualizado con arquitectura completa
- EJEMPLOS.md con casos de uso detallados
- ESTADO_PROYECTO.md con progreso de Fase 4
- API docs interactiva en /api-docs
- Script de despliegue mejorado con health checks

🚀 Producción:
- Deployment script con validaciones
- Health checks y rollback capability
- Configuración de sudoers para systemctl
- Hardening de seguridad en servicios systemd
This commit is contained in:
2026-01-13 08:24:13 -05:00
parent 3595e55a1e
commit b0489739cf
33 changed files with 6893 additions and 1261 deletions

View File

@@ -4,6 +4,8 @@ use reqwest::header::{HeaderMap, HeaderValue, USER_AGENT};
use std::time::Duration;
use crate::logger::get_logger;
use crate::config::get_config_manager;
use crate::systemd::SystemCtl;
use crate::models::{AppStatus, ServiceStatus};
// User-Agent dinámico
fn generate_user_agent() -> String {
@@ -24,6 +26,9 @@ struct AppStatusUpdate {
memory_usage: String,
cpu_usage: String,
last_check: String,
systemd_status: String,
#[serde(skip_serializing_if = "Option::is_none")]
discrepancy: Option<String>,
}
pub async fn run_monitoring(server_name: String, api_key: String, cloud_url: String) {
@@ -47,7 +52,16 @@ pub async fn run_monitoring(server_name: String, api_key: String, cloud_url: Str
}
for app in apps_to_monitor {
let data = collect_metrics(&sys, &app.name, app.port, &server_name);
let data = collect_metrics_with_systemd(&sys, &app.name, app.port, &server_name);
// Reportar discrepancias
if let Some(ref disc) = data.discrepancy {
logger.warning(
"Monitor",
&format!("Discrepancia detectada en {}", app.name),
Some(disc)
);
}
match send_to_cloud(data, &api_key, &cloud_url, &user_agent).await {
Ok(_) => {},
@@ -66,41 +80,63 @@ pub async fn run_monitoring(server_name: String, api_key: String, cloud_url: Str
}
}
fn collect_metrics(sys: &System, name: &str, port: i32, server: &str) -> AppStatusUpdate {
fn collect_metrics_with_systemd(sys: &System, name: &str, port: i32, server: &str) -> AppStatusUpdate {
let mut pid_encontrado = 0;
let mut cpu = 0.0;
let mut mem = 0.0;
let mut status = "stopped".to_string();
let mut process_detected = false;
// 1. Detección por proceso (método original)
for (pid, process) in sys.processes() {
let process_name = process.name();
if process_name.contains("node") {
// Soportar node y python
if process_name.contains("node") || process_name.contains("python") {
if let Some(cwd) = process.cwd() {
let cwd_str = cwd.to_string_lossy();
if cwd_str.contains(name) {
pid_encontrado = pid.as_u32() as i32;
cpu = process.cpu_usage();
mem = process.memory() as f64 / 1024.0 / 1024.0;
status = "running".to_string();
process_detected = true;
break;
}
}
}
}
// 2. Consultar systemd
let service_name = format!("{}.service", name);
let systemd_status = SystemCtl::status(&service_name);
// 3. Reconciliar estados
let final_status = AppStatus::reconcile(process_detected, &systemd_status);
// 4. Detectar discrepancias
let discrepancy = match (&final_status, process_detected, &systemd_status) {
(AppStatus::Crashed, false, ServiceStatus::Active) => {
Some(format!("Systemd reporta activo pero proceso no detectado"))
}
(AppStatus::Zombie, true, ServiceStatus::Inactive) => {
Some(format!("Proceso detectado pero systemd reporta inactivo"))
}
_ => None,
};
let now = chrono::Local::now();
let timestamp = now.format("%Y-%m-%d %H:%M:%S").to_string();
AppStatusUpdate {
app_name: name.to_string(),
server: server.to_string(),
status,
status: final_status.as_str().to_string(),
port,
pid: pid_encontrado,
pid: if pid_encontrado > 0 { pid_encontrado } else { 0 },
memory_usage: format!("{:.2}MB", mem),
cpu_usage: format!("{:.2}%", cpu),
last_check: timestamp,
systemd_status: systemd_status.as_str().to_string(),
discrepancy,
}
}
@@ -157,4 +193,4 @@ async fn send_to_cloud(
eprintln!("⚠️ Error HTTP {}: {}", status, error_text);
Err(format!("HTTP {}: {}", status, error_text).into())
}
}
}