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

@@ -0,0 +1,128 @@
use super::{Result, OrchestratorError};
use crate::models::{ServiceConfig, ManagedApp, AppStatus};
use crate::systemd::{ServiceGenerator, SystemCtl};
use crate::logger::get_logger;
use dashmap::DashMap;
use std::sync::Arc;
pub struct AppManager {
apps: Arc<DashMap<String, ServiceConfig>>,
}
impl AppManager {
pub fn new() -> Self {
AppManager {
apps: Arc::new(DashMap::new()),
}
}
pub fn register_app(&self, config: ServiceConfig) -> Result<()> {
let logger = get_logger();
// Validar configuración
config.validate()
.map_err(|e| OrchestratorError::ValidationError(e))?;
// Verificar si ya existe
if self.apps.contains_key(&config.app_name) {
logger.warning("AppManager", "Aplicación ya registrada", Some(&config.app_name));
return Err(OrchestratorError::AppAlreadyExists(config.app_name.clone()));
}
// Verificar si el servicio ya existe en systemd
if SystemCtl::is_service_exists(&config.service_name()) {
logger.warning("AppManager", "Servicio systemd ya existe", Some(&config.service_name()));
return Err(OrchestratorError::AppAlreadyExists(
format!("El servicio {} ya existe en systemd", config.service_name())
));
}
logger.info("AppManager", &format!("Registrando aplicación: {}", config.app_name));
// Generar archivo de servicio
let service_content = ServiceGenerator::create_service(&config)?;
ServiceGenerator::write_service_file(&config, &service_content)?;
// Recargar daemon de systemd
SystemCtl::daemon_reload()?;
// Habilitar el servicio
SystemCtl::enable(&config.service_name())?;
// Guardar en memoria
self.apps.insert(config.app_name.clone(), config.clone());
logger.info("AppManager", &format!("Aplicación {} registrada exitosamente", config.app_name));
Ok(())
}
pub fn unregister_app(&self, app_name: &str) -> Result<()> {
let logger = get_logger();
logger.info("AppManager", &format!("Desregistrando aplicación: {}", app_name));
// Obtener configuración
let config = self.apps.get(app_name)
.ok_or_else(|| OrchestratorError::AppNotFound(app_name.to_string()))?;
let service_name = config.service_name();
drop(config); // Liberar el lock
// Detener el servicio si está corriendo
let _ = SystemCtl::stop(&service_name);
// Deshabilitar el servicio
let _ = SystemCtl::disable(&service_name);
// Eliminar archivo de servicio
ServiceGenerator::delete_service_file(&service_name)?;
// Recargar daemon
SystemCtl::daemon_reload()?;
// Eliminar de memoria
self.apps.remove(app_name);
logger.info("AppManager", &format!("Aplicación {} desregistrada exitosamente", app_name));
Ok(())
}
pub fn list_apps(&self) -> Vec<String> {
self.apps.iter()
.map(|entry| entry.key().clone())
.collect()
}
pub fn get_app(&self, app_name: &str) -> Option<ServiceConfig> {
self.apps.get(app_name).map(|entry| entry.clone())
}
pub fn app_exists(&self, app_name: &str) -> bool {
self.apps.contains_key(app_name)
}
pub fn get_app_status(&self, app_name: &str) -> Option<ManagedApp> {
let config = self.get_app(app_name)?;
let systemd_status = SystemCtl::status(&config.service_name());
// Por ahora retornamos información básica
// El monitor.rs se encargará de enriquecer con PID, CPU, RAM
Some(ManagedApp {
name: app_name.to_string(),
status: AppStatus::reconcile(false, &systemd_status),
pid: None,
cpu_usage: 0.0,
memory_usage: 0,
systemd_status,
last_updated: chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string(),
})
}
}
impl Default for AppManager {
fn default() -> Self {
Self::new()
}
}

View File

@@ -0,0 +1,126 @@
use super::{Result, OrchestratorError};
use crate::systemd::SystemCtl;
use crate::logger::get_logger;
use dashmap::DashMap;
use std::sync::Arc;
use std::time::{Duration, Instant};
pub struct LifecycleManager {
rate_limiter: Arc<DashMap<String, Instant>>,
rate_limit_duration: Duration,
}
impl LifecycleManager {
pub fn new() -> Self {
LifecycleManager {
rate_limiter: Arc::new(DashMap::new()),
rate_limit_duration: Duration::from_secs(1),
}
}
pub fn start_app(&self, app_name: &str) -> Result<()> {
let logger = get_logger();
// Verificar rate limit
self.check_rate_limit(app_name)?;
logger.info("Lifecycle", &format!("Iniciando aplicación: {}", app_name));
let service_name = format!("{}.service", app_name);
SystemCtl::start(&service_name)?;
// Actualizar rate limiter
self.update_rate_limiter(app_name);
logger.info("Lifecycle", &format!("Aplicación {} iniciada", app_name));
Ok(())
}
pub fn stop_app(&self, app_name: &str) -> Result<()> {
let logger = get_logger();
// Verificar rate limit
self.check_rate_limit(app_name)?;
logger.info("Lifecycle", &format!("Deteniendo aplicación: {}", app_name));
let service_name = format!("{}.service", app_name);
SystemCtl::stop(&service_name)?;
// Actualizar rate limiter
self.update_rate_limiter(app_name);
logger.info("Lifecycle", &format!("Aplicación {} detenida", app_name));
Ok(())
}
pub fn restart_app(&self, app_name: &str) -> Result<()> {
let logger = get_logger();
// Verificar rate limit
self.check_rate_limit(app_name)?;
logger.info("Lifecycle", &format!("Reiniciando aplicación: {}", app_name));
let service_name = format!("{}.service", app_name);
SystemCtl::restart(&service_name)?;
// Actualizar rate limiter
self.update_rate_limiter(app_name);
logger.info("Lifecycle", &format!("Aplicación {} reiniciada", app_name));
Ok(())
}
fn check_rate_limit(&self, app_name: &str) -> Result<()> {
if let Some(last_action) = self.rate_limiter.get(app_name) {
let elapsed = last_action.elapsed();
if elapsed < self.rate_limit_duration {
let logger = get_logger();
logger.warning(
"Lifecycle",
"Rate limit excedido",
Some(&format!("App: {}, Espera: {:?}", app_name, self.rate_limit_duration - elapsed))
);
return Err(OrchestratorError::RateLimitExceeded(app_name.to_string()));
}
}
Ok(())
}
fn update_rate_limiter(&self, app_name: &str) {
self.rate_limiter.insert(app_name.to_string(), Instant::now());
}
pub fn recover_inconsistent_state(&self, app_name: &str, expected_running: bool) -> Result<()> {
let logger = get_logger();
logger.warning(
"Lifecycle",
"Intentando recuperar estado inconsistente",
Some(app_name)
);
let service_name = format!("{}.service", app_name);
if expected_running {
// Se espera que esté corriendo pero no está
logger.info("Lifecycle", &format!("Intentando reiniciar {}", app_name));
SystemCtl::start(&service_name)?;
} else {
// Se espera que esté detenido pero está corriendo
logger.info("Lifecycle", &format!("Intentando detener {}", app_name));
SystemCtl::stop(&service_name)?;
}
Ok(())
}
}
impl Default for LifecycleManager {
fn default() -> Self {
Self::new()
}
}

27
src/orchestrator/mod.rs Normal file
View File

@@ -0,0 +1,27 @@
pub mod app_manager;
pub mod lifecycle;
pub use app_manager::*;
pub use lifecycle::*;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum OrchestratorError {
#[error("Error de systemd: {0}")]
SystemdError(#[from] crate::systemd::SystemdError),
#[error("Aplicación ya existe: {0}")]
AppAlreadyExists(String),
#[error("Aplicación no encontrada: {0}")]
AppNotFound(String),
#[error("Rate limit excedido para: {0}")]
RateLimitExceeded(String),
#[error("Error de validación: {0}")]
ValidationError(String),
}
pub type Result<T> = std::result::Result<T, OrchestratorError>;