feat: Mejorar estructura de monitored_apps.json con metadata completa

- Añadir campos al modelo MonitoredApp:
  * service_name: Nombre del servicio systemd
  * path: WorkingDirectory de la aplicación
  * entry_point: Archivo de entrada (server.js, app.js, etc.)
  * node_bin: Ruta completa al binario de node/python
  * mode: Modo de ejecución (production, development, test)
  * service_file_path: Ruta al archivo .service de systemd
  * registered_at: Timestamp de registro (ISO 8601)

- Actualizar discovery.rs para extraer toda la información:
  * Parsear ExecStart para obtener node_bin y entry_point
  * Extraer NODE_ENV para determinar el modo
  * Guardar ruta completa al archivo .service
  * Usar add_app_full() con información completa

- Integrar ConfigManager en AppManager:
  * Guardar automáticamente en monitored_apps.json al registrar apps
  * Eliminar del JSON al desregistrar apps
  * Extraer metadata desde ServiceConfig (puerto, entry_point, mode, etc.)

- Mantener retrocompatibilidad con JSON antiguo mediante campos deprecated
- Todos los nuevos campos usan #[serde(default)] para evitar errores de deserialización
This commit is contained in:
2026-01-18 03:26:42 -05:00
parent ad9b46bdc5
commit 8822e9e6b5
21 changed files with 3246 additions and 642 deletions

View File

@@ -6,14 +6,51 @@ use crate::logger::get_logger;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MonitoredApp {
/// Nombre de la aplicación
pub name: String,
/// Nombre del servicio systemd (ej: siax-app-TAREAS.service)
#[serde(default)]
pub service_name: String,
/// Ruta completa al directorio de la aplicación (WorkingDirectory)
#[serde(default)]
pub path: String,
/// Puerto donde escucha la aplicación
pub port: i32,
/// Archivo de entrada (ej: server.js, app.js)
#[serde(default)]
pub entry_point: String,
/// Ruta completa al binario de node/python
#[serde(default)]
pub node_bin: String,
/// Modo de ejecución (production, development, test)
#[serde(default = "default_mode")]
pub mode: String,
/// Ruta completa al archivo .service de systemd
#[serde(default)]
pub service_file_path: String,
/// Fecha de registro (ISO 8601)
#[serde(default, skip_serializing_if = "String::is_empty", rename = "reg")]
pub registered_at: String,
// DEPRECATED: Mantener por compatibilidad con versiones antiguas
#[serde(skip_serializing_if = "Option::is_none")]
pub systemd_service: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub created_at: Option<String>,
}
fn default_mode() -> String {
"production".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AppConfig {
pub apps: Vec<MonitoredApp>,
@@ -99,23 +136,16 @@ impl ConfigManager {
config.apps.clone()
}
pub fn add_app(&self, name: String, port: i32) -> Result<(), String> {
/// Agrega una app con información completa
pub fn add_app_full(&self, app: MonitoredApp) -> Result<(), String> {
let mut config = self.config.write().unwrap();
// Verificar si ya existe
if config.apps.iter().any(|app| app.name == name) {
return Err(format!("La app '{}' ya está siendo monitoreada", name));
if config.apps.iter().any(|a| a.name == app.name) {
return Err(format!("La app '{}' ya está siendo monitoreada", app.name));
}
let systemd_service = format!("siax-app-{}.service", name);
let created_at = chrono::Local::now().to_rfc3339();
config.apps.push(MonitoredApp {
name,
port,
systemd_service: Some(systemd_service),
created_at: Some(created_at),
});
config.apps.push(app);
// Guardar en disco
match Self::save_config_to_file(&self.config_path, &config) {
@@ -124,6 +154,29 @@ impl ConfigManager {
}
}
/// Método simplificado para compatibilidad (DEPRECATED)
#[deprecated(note = "Usar add_app_full() con MonitoredApp completo")]
pub fn add_app(&self, name: String, port: i32) -> Result<(), String> {
let service_name = format!("siax-app-{}.service", name);
let registered_at = chrono::Local::now().to_rfc3339();
let app = MonitoredApp {
name,
service_name,
path: String::new(),
port,
entry_point: String::new(),
node_bin: String::new(),
mode: "production".to_string(),
service_file_path: String::new(),
registered_at,
systemd_service: None,
created_at: None,
};
self.add_app_full(app)
}
pub fn remove_app(&self, name: &str) -> Result<(), String> {
let mut config = self.config.write().unwrap();

View File

@@ -67,9 +67,12 @@ pub struct DiscoveredService {
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 básica
/// Parsea un archivo .service para extraer configuración completa
fn parse_service_file(path: &Path, app_name: &str) -> Option<DiscoveredService> {
let logger = get_logger();
@@ -88,6 +91,9 @@ fn parse_service_file(path: &Path, app_name: &str) -> Option<DiscoveredService>
user: None,
exec_start: None,
port: None,
node_env: String::from("production"),
entry_point: None,
node_bin: None,
};
// Parsear líneas del archivo
@@ -106,7 +112,24 @@ fn parse_service_file(path: &Path, app_name: &str) -> Option<DiscoveredService>
// ExecStart
if line.starts_with("ExecStart=") {
service.exec_start = Some(line.trim_start_matches("ExecStart=").to_string());
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
@@ -115,12 +138,21 @@ fn parse_service_file(path: &Path, app_name: &str) -> Option<DiscoveredService>
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: {:?}",
logger.info("Discovery", &format!(" App: {}, User: {:?}, WorkDir: {:?}, Env: {}, EntryPoint: {:?}",
service.app_name,
service.user,
service.working_directory
service.working_directory,
service.node_env,
service.entry_point
));
Some(service)
@@ -143,6 +175,27 @@ fn extract_port_from_env(line: &str) -> Option<i32> {
}
}
/// 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();
@@ -156,7 +209,6 @@ pub fn sync_discovered_services(services: Vec<DiscoveredService>) {
for service in services {
// Intentar detectar el puerto si no se encontró en Environment
let port = service.port.unwrap_or_else(|| {
// Intentar extraer del nombre o usar puerto por defecto
detect_port_from_name(&service.app_name)
});
@@ -170,10 +222,29 @@ pub fn sync_discovered_services(services: Vec<DiscoveredService>) {
continue;
}
// Agregar a monitored_apps.json
logger.info("Discovery", &format!(" Agregando {} (puerto: {})", service.app_name, port));
// Crear MonitoredApp con información completa
let service_name = format!("siax-app-{}.service", service.app_name);
let registered_at = chrono::Local::now().to_rfc3339();
match config_manager.add_app(service.app_name.clone(), port) {
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));
added_count += 1;

View File

@@ -2,6 +2,7 @@ use super::{Result, OrchestratorError};
use crate::models::{ServiceConfig, ManagedApp, AppStatus};
use crate::systemd::{ServiceGenerator, SystemCtl};
use crate::logger::get_logger;
use crate::config::{get_config_manager, MonitoredApp};
use dashmap::DashMap;
use std::sync::Arc;
@@ -52,6 +53,49 @@ impl AppManager {
// Guardar en memoria
self.apps.insert(config.app_name.clone(), config.clone());
// Guardar en monitored_apps.json con información completa
let config_manager = get_config_manager();
let service_file_path = format!("/etc/systemd/system/{}", config.service_name());
let registered_at = chrono::Local::now().to_rfc3339();
// Extraer el puerto del environment si existe
let port = config.environment.get("PORT")
.and_then(|p| p.parse::<i32>().ok())
.unwrap_or(8080);
// Determinar el entry_point desde script_path
let entry_point = std::path::Path::new(&config.script_path)
.file_name()
.and_then(|f| f.to_str())
.unwrap_or("server.js")
.to_string();
// Determinar node_bin (será resuelto por el ServiceGenerator)
let node_bin = config.custom_executable.clone().unwrap_or_default();
// Determinar mode desde NODE_ENV
let mode = config.environment.get("NODE_ENV")
.cloned()
.unwrap_or_else(|| "production".to_string());
let monitored_app = MonitoredApp {
name: config.app_name.clone(),
service_name: config.service_name(),
path: config.working_directory.clone(),
port,
entry_point,
node_bin,
mode,
service_file_path,
registered_at,
systemd_service: None,
created_at: None,
};
if let Err(e) = config_manager.add_app_full(monitored_app) {
logger.warning("AppManager", "No se pudo guardar en monitored_apps.json", Some(&e));
}
logger.info("AppManager", &format!("Aplicación {} registrada exitosamente", config.app_name));
Ok(())
@@ -84,6 +128,12 @@ impl AppManager {
// Eliminar de memoria
self.apps.remove(app_name);
// Eliminar de monitored_apps.json
let config_manager = get_config_manager();
if let Err(e) = config_manager.remove_app(app_name) {
logger.warning("AppManager", "No se pudo eliminar de monitored_apps.json", Some(&e));
}
logger.info("AppManager", &format!("Aplicación {} desregistrada exitosamente", app_name));
Ok(())