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

@@ -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;