Files
SIAX-MONITOR/src/api/websocket.rs
pablinux b0489739cf 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
2026-01-13 08:24:13 -05:00

189 lines
5.3 KiB
Rust

use axum::{
extract::{
ws::{Message, WebSocket, WebSocketUpgrade},
Path, State,
},
response::IntoResponse,
};
use futures::{sink::SinkExt, stream::StreamExt};
use std::process::Stdio;
use std::sync::Arc;
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::process::Command as TokioCommand;
use crate::logger::get_logger;
use dashmap::DashMap;
pub struct WebSocketManager {
active_connections: Arc<DashMap<String, usize>>,
max_connections_per_app: usize,
}
impl WebSocketManager {
pub fn new() -> Self {
WebSocketManager {
active_connections: Arc::new(DashMap::new()),
max_connections_per_app: 5,
}
}
fn can_connect(&self, app_name: &str) -> bool {
let count = self.active_connections
.get(app_name)
.map(|v| *v)
.unwrap_or(0);
count < self.max_connections_per_app
}
fn increment_connection(&self, app_name: &str) {
self.active_connections
.entry(app_name.to_string())
.and_modify(|c| *c += 1)
.or_insert(1);
}
fn decrement_connection(&self, app_name: &str) {
self.active_connections
.entry(app_name.to_string())
.and_modify(|c| {
if *c > 0 {
*c -= 1;
}
});
}
}
impl Default for WebSocketManager {
fn default() -> Self {
Self::new()
}
}
pub async fn logs_websocket_handler(
ws: WebSocketUpgrade,
Path(app_name): Path<String>,
State(ws_manager): State<Arc<WebSocketManager>>,
) -> impl IntoResponse {
let logger = get_logger();
// Verificar límite de conexiones
if !ws_manager.can_connect(&app_name) {
logger.warning(
"WebSocket",
"Límite de conexiones excedido",
Some(&app_name)
);
return ws.on_upgrade(move |socket| async move {
let mut socket = socket;
let _ = socket.send(Message::Text(
"Error: Límite de conexiones simultáneas excedido".to_string()
)).await;
let _ = socket.close().await;
});
}
logger.info("WebSocket", &format!("Nueva conexión para logs de: {}", app_name));
ws_manager.increment_connection(&app_name);
ws.on_upgrade(move |socket| handle_logs_socket(socket, app_name, ws_manager))
}
async fn handle_logs_socket(
socket: WebSocket,
app_name: String,
ws_manager: Arc<WebSocketManager>,
) {
let logger = get_logger();
let service_name = format!("{}.service", app_name);
// Iniciar journalctl
let mut child = match TokioCommand::new("journalctl")
.arg("-u")
.arg(&service_name)
.arg("-f")
.arg("--output=json")
.arg("-n")
.arg("50") // Últimas 50 líneas
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
{
Ok(child) => child,
Err(e) => {
logger.error("WebSocket", "Error iniciando journalctl", Some(&e.to_string()));
ws_manager.decrement_connection(&app_name);
return;
}
};
let stdout = child.stdout.take().unwrap();
let reader = BufReader::new(stdout);
let mut lines = reader.lines();
let (mut sender, mut receiver) = socket.split();
// Enviar mensaje de bienvenida
let welcome = format!("📡 Conectado a logs de: {}", app_name);
let _ = sender.send(Message::Text(welcome)).await;
// Task para enviar logs
let send_task = tokio::spawn(async move {
while let Ok(Some(line)) = lines.next_line().await {
// Parsear JSON de journalctl
if let Ok(json) = serde_json::from_str::<serde_json::Value>(&line) {
let message = json.get("MESSAGE")
.and_then(|m| m.as_str())
.unwrap_or(&line);
let timestamp = json.get("__REALTIME_TIMESTAMP")
.and_then(|t| t.as_str())
.unwrap_or("");
let formatted = if !timestamp.is_empty() {
format!("[{}] {}", timestamp, message)
} else {
message.to_string()
};
if sender.send(Message::Text(formatted)).await.is_err() {
break;
}
} else {
// Si no es JSON, enviar la línea tal cual
if sender.send(Message::Text(line)).await.is_err() {
break;
}
}
}
});
// Task para recibir mensajes del cliente (para detectar desconexión)
let receive_task = tokio::spawn(async move {
while let Some(msg) = receiver.next().await {
if let Ok(msg) = msg {
match msg {
Message::Close(_) => break,
Message::Ping(_) => {
// Los pings se manejan automáticamente
}
_ => {}
}
} else {
break;
}
}
});
// Esperar a que termine alguna de las dos tasks
tokio::select! {
_ = send_task => {},
_ = receive_task => {},
}
// Cleanup
let _ = child.kill().await;
ws_manager.decrement_connection(&app_name);
logger.info("WebSocket", &format!("Conexión cerrada para: {}", app_name));
}