Files
SIAX-MONITOR/src/discovery.rs
pablinux 246b5c8342 feat: Mejorar logging del discovery y agregar endpoint /api/monitored
- Agregar logs detallados en discovery.rs:
  * Mostrar cuántos archivos se escanean
  * Mostrar cuántos servicios siax-app-* se encuentran
  * Mostrar cuántos se parsean exitosamente
  * Logs tanto en logger como en stdout para debugging

- Agregar endpoint GET /api/monitored:
  * Retorna el contenido completo de monitored_apps.json
  * Permite verificar qué apps están siendo monitoreadas
  * Útil para debugging y diagnóstico

- Mejorar mensajes de error con emojis para mejor visibilidad
- Logs en cada paso del proceso de sincronización
2026-01-18 03:40:19 -05:00

322 lines
11 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/// Módulo para descubrir servicios systemd existentes
use std::fs;
use std::path::Path;
use crate::logger::get_logger;
use crate::config::{get_config_manager, MonitoredApp};
const SYSTEMD_DIR: &str = "/etc/systemd/system";
const SERVICE_PREFIX: &str = "siax-app-";
/// Descubre servicios systemd existentes con prefijo siax-app-*
pub fn discover_services() -> Vec<DiscoveredService> {
let logger = get_logger();
logger.info("Discovery", &format!("🔍 Escaneando servicios systemd en: {}", SYSTEMD_DIR));
println!("🔍 Discovery: Buscando servicios en {}", SYSTEMD_DIR);
let mut services = Vec::new();
// Leer directorio de systemd
let entries = match fs::read_dir(SYSTEMD_DIR) {
Ok(entries) => {
logger.info("Discovery", &format!("✅ Directorio {} accesible", SYSTEMD_DIR));
println!("✅ Discovery: Directorio {} accesible", SYSTEMD_DIR);
entries
},
Err(e) => {
logger.error("Discovery", &format!("❌ No se pudo leer directorio {}", SYSTEMD_DIR), Some(&e.to_string()));
println!("❌ Discovery: ERROR - No se pudo leer {}: {}", SYSTEMD_DIR, e);
return services;
}
};
// Buscar archivos siax-app-*.service
let mut total_files = 0;
let mut siax_files = 0;
for entry in entries.flatten() {
total_files += 1;
let path = entry.path();
if let Some(filename) = path.file_name() {
let filename_str = filename.to_string_lossy();
// Verificar que sea un archivo .service con nuestro prefijo
if filename_str.starts_with(SERVICE_PREFIX) && filename_str.ends_with(".service") {
siax_files += 1;
logger.info("Discovery", &format!("✅ Encontrado: {}", filename_str));
println!("✅ Discovery: Servicio detectado: {}", filename_str);
// Extraer nombre de la app
let app_name = extract_app_name(&filename_str);
// Leer configuración del servicio
if let Some(service) = parse_service_file(&path, &app_name) {
services.push(service);
} else {
logger.warning("Discovery", &format!("⚠️ No se pudo parsear: {}", filename_str), None);
println!("⚠️ Discovery: No se pudo parsear {}", filename_str);
}
}
}
}
logger.info("Discovery", &format!("📊 Escaneados {} archivos, {} con prefijo '{}', {} parseados exitosamente",
total_files, siax_files, SERVICE_PREFIX, services.len()));
println!("📊 Discovery: Archivos totales: {}, siax-app-*: {}, parseados: {}",
total_files, siax_files, services.len());
services
}
/// Extrae el nombre de la app desde el nombre del archivo
/// Ejemplo: "siax-app-IDEAS.service" -> "app_IDEAS"
fn extract_app_name(filename: &str) -> String {
// Remover "siax-app-" del inicio y ".service" del final
filename
.trim_start_matches(SERVICE_PREFIX)
.trim_end_matches(".service")
.to_string()
}
/// Servicio descubierto en systemd
#[derive(Debug, Clone)]
pub struct DiscoveredService {
pub app_name: String,
pub service_file: String,
pub working_directory: Option<String>,
pub user: Option<String>,
pub exec_start: Option<String>,
pub port: Option<i32>,
pub node_env: String,
pub entry_point: Option<String>,
pub node_bin: Option<String>,
}
/// Parsea un archivo .service para extraer configuración completa
fn parse_service_file(path: &Path, app_name: &str) -> Option<DiscoveredService> {
let logger = get_logger();
let content = match fs::read_to_string(path) {
Ok(content) => content,
Err(e) => {
logger.error("Discovery", &format!("Error leyendo {}", path.display()), Some(&e.to_string()));
return None;
}
};
let mut service = DiscoveredService {
app_name: app_name.to_string(),
service_file: path.to_string_lossy().to_string(),
working_directory: None,
user: None,
exec_start: None,
port: None,
node_env: String::from("production"),
entry_point: None,
node_bin: None,
};
// Parsear líneas del archivo
for line in content.lines() {
let line = line.trim();
// WorkingDirectory
if line.starts_with("WorkingDirectory=") {
service.working_directory = Some(line.trim_start_matches("WorkingDirectory=").to_string());
}
// User
if line.starts_with("User=") {
service.user = Some(line.trim_start_matches("User=").to_string());
}
// ExecStart
if line.starts_with("ExecStart=") {
let exec_start = line.trim_start_matches("ExecStart=").to_string();
// Extraer node_bin y entry_point del ExecStart
// Ejemplo: /home/user/.nvm/versions/node/v24.12.0/bin/node server.js
let parts: Vec<&str> = exec_start.split_whitespace().collect();
if !parts.is_empty() {
service.node_bin = Some(parts[0].to_string());
// Buscar el archivo .js como entry_point
for part in &parts[1..] {
if part.ends_with(".js") {
service.entry_point = Some(part.to_string());
break;
}
}
}
service.exec_start = Some(exec_start);
}
// Environment con PORT
if line.starts_with("Environment=") && line.contains("PORT") {
if let Some(port) = extract_port_from_env(line) {
service.port = Some(port);
}
}
// Environment con NODE_ENV
if line.starts_with("Environment=") && line.contains("NODE_ENV") {
if let Some(env) = extract_env_value(line, "NODE_ENV") {
service.node_env = env;
}
}
}
logger.info("Discovery", &format!(" App: {}, User: {:?}, WorkDir: {:?}, Env: {}, EntryPoint: {:?}",
service.app_name,
service.user,
service.working_directory,
service.node_env,
service.entry_point
));
Some(service)
}
/// Extrae el puerto de una línea Environment
/// Ejemplo: Environment="PORT=3000" -> Some(3000)
fn extract_port_from_env(line: &str) -> Option<i32> {
// Buscar PORT=número
if let Some(start) = line.find("PORT=") {
let after_port = &line[start + 5..];
// Extraer números
let port_str: String = after_port.chars()
.take_while(|c| c.is_numeric())
.collect();
port_str.parse::<i32>().ok()
} else {
None
}
}
/// Extrae un valor de variable de entorno de una línea Environment
/// Ejemplo: Environment="NODE_ENV=production" -> Some("production")
fn extract_env_value(line: &str, var_name: &str) -> Option<String> {
let pattern = format!("{}=", var_name);
if let Some(start) = line.find(&pattern) {
let after_var = &line[start + pattern.len()..];
// Extraer hasta espacios, comillas o fin de línea
let value: String = after_var.chars()
.take_while(|c| !c.is_whitespace() && *c != '"')
.collect();
if !value.is_empty() {
Some(value)
} else {
None
}
} else {
None
}
}
/// Sincroniza los servicios descubiertos con monitored_apps.json
pub fn sync_discovered_services(services: Vec<DiscoveredService>) {
let logger = get_logger();
let config_manager = get_config_manager();
logger.info("Discovery", &format!("🔄 Sincronizando {} servicios descubiertos...", services.len()));
println!("🔄 Discovery: Sincronizando {} servicios con monitored_apps.json", services.len());
let mut added_count = 0;
let mut skipped_count = 0;
for service in services {
// Intentar detectar el puerto si no se encontró en Environment
let port = service.port.unwrap_or_else(|| {
detect_port_from_name(&service.app_name)
});
// Verificar si ya existe en la configuración
let existing_apps = config_manager.get_apps();
let already_exists = existing_apps.iter().any(|app| app.name == service.app_name);
if already_exists {
logger.info("Discovery", &format!("⏭️ {} ya existe en configuración", service.app_name));
println!("⏭️ Discovery: {} ya existe, omitiendo", service.app_name);
skipped_count += 1;
continue;
}
// Crear MonitoredApp con información completa
let service_name = format!("siax-app-{}.service", service.app_name);
let registered_at = chrono::Local::now().to_rfc3339();
let app = MonitoredApp {
name: service.app_name.clone(),
service_name,
path: service.working_directory.unwrap_or_default(),
port,
entry_point: service.entry_point.unwrap_or_default(),
node_bin: service.node_bin.unwrap_or_default(),
mode: service.node_env,
service_file_path: service.service_file.clone(),
registered_at,
systemd_service: None,
created_at: None,
};
// Agregar a monitored_apps.json
logger.info("Discovery", &format!(" Agregando {} (puerto: {}, entry: {})",
app.name, app.port, app.entry_point));
match config_manager.add_app_full(app) {
Ok(_) => {
logger.info("Discovery", &format!("{} agregado exitosamente", service.app_name));
println!("✅ Discovery: {} agregado a monitored_apps.json", service.app_name);
added_count += 1;
}
Err(e) => {
logger.error("Discovery", &format!("Error agregando {}", service.app_name), Some(&e));
println!("❌ Discovery: Error agregando {}: {}", service.app_name, e);
}
}
}
logger.info("Discovery", &format!("📊 Resumen: {} agregadas, {} ya existían", added_count, skipped_count));
println!("📊 Discovery: Resumen final - {} apps nuevas, {} existentes", added_count, skipped_count);
}
/// Intenta detectar el puerto desde el nombre de la app
/// Esto es un fallback simple si no se encuentra en el .service
fn detect_port_from_name(app_name: &str) -> i32 {
// Algunos puertos conocidos por nombre
match app_name.to_lowercase().as_str() {
name if name.contains("tareas") => 3000,
name if name.contains("fidelizacion") => 3001,
name if name.contains("ideas") => 2000,
_ => 8080, // Puerto por defecto genérico
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_app_name() {
assert_eq!(extract_app_name("siax-app-IDEAS.service"), "IDEAS");
assert_eq!(extract_app_name("siax-app-TAREAS.service"), "TAREAS");
assert_eq!(extract_app_name("siax-app-fidelizacion.service"), "fidelizacion");
}
#[test]
fn test_extract_port_from_env() {
assert_eq!(extract_port_from_env("Environment=PORT=3000"), Some(3000));
assert_eq!(extract_port_from_env("Environment=\"PORT=8080\""), Some(8080));
assert_eq!(extract_port_from_env("Environment=NODE_ENV=production"), None);
}
#[test]
fn test_detect_port_from_name() {
assert_eq!(detect_port_from_name("app_tareas"), 3000);
assert_eq!(detect_port_from_name("IDEAS"), 2000);
assert_eq!(detect_port_from_name("unknown_app"), 8080);
}
}