feat: Sistema de monitoreo base con logging y configuración dinámica

- Implementado monitor de procesos Node.js con detección automática
- Sistema de logging con niveles (Info, Warning, Error, Critical)
- ConfigManager para gestión dinámica de apps monitoreadas
- Interfaz web básica con escaneo de procesos
- Integración con API central para reportar estados
- User-Agent tracking para identificación de agentes
- Persistencia de configuración en JSON
- Logs almacenados en archivo con rotación
- Sistema modular: monitor, interface, logger, config

Estructura:
- src/main.rs: Orquestador principal
- src/monitor.rs: Monitoreo de procesos y envío a API
- src/interface.rs: Servidor web Axum con endpoints
- src/logger.rs: Sistema de logging a archivo y consola
- src/config.rs: Gestión de configuración persistente
- web/: Templates HTML para interfaz web
- config/: Configuración de apps monitoreadas
- logs/: Archivos de log del sistema

Features implementadas:
 Detección automática de procesos Node.js
 Monitoreo de CPU y RAM por proceso
 Reportes periódicos a API central (cada 60s)
 Interfaz web en puerto 8080
 Logs estructurados con timestamps
 Configuración dinámica sin reinicio
 Script de despliegue automatizado

Próximos pasos:
- Integración con systemd para control de procesos
- Dashboard mejorado con cards de apps
- Logs en tiempo real vía WebSocket
- Start/Stop/Restart de aplicaciones
This commit is contained in:
2026-01-11 23:14:09 -05:00
parent bc1953fce1
commit 3595e55a1e
20 changed files with 3465 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/target

10
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,10 @@
# Default ignored files
/shelf/
/workspace.xml
# Ignored default folder with query files
/queries/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
# Editor-based HTTP Client requests
/httpRequests/

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/siax_monitor.iml" filepath="$PROJECT_DIR$/.idea/siax_monitor.iml" />
</modules>
</component>
</project>

11
.idea/siax_monitor.iml generated Normal file
View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="EMPTY_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

1897
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

13
Cargo.toml Normal file
View File

@@ -0,0 +1,13 @@
[package]
name = "siax_monitor"
version = "0.1.0"
edition = "2021"
[dependencies]
tokio = { version = "1", features = ["full"] }
axum = "0.7"
reqwest = { version = "0.11", features = ["json"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
sysinfo = "0.30"
chrono = "0.4"

View File

@@ -0,0 +1,12 @@
{
"apps": [
{
"name": "app_tareas",
"port": 3000
},
{
"name": "fidelizacion",
"port": 3001
}
]
}

65
desplegar_agent.sh Executable file
View File

@@ -0,0 +1,65 @@
#!/bin/bash
# --- CONFIGURACIÓN GLOBAL ---
BINARY_NAME="siax_monitor" # ← Cambié esto para que coincida con Cargo.toml
TARGET="x86_64-unknown-linux-gnu" # ← Cambié a gnu en lugar de musl
echo "📦 Compilando binario para Linux ($TARGET)..."
cargo build --release --target $TARGET
if [ $? -ne 0 ]; then
echo "❌ Error en la compilación."
exit 1
fi
# --- FUNCIÓN MAESTRA DE DESPLIEGUE ---
# Parámetros: IP, USUARIO, RUTA_DESTINO
deploy_to_server() {
local IP=$1
local USER=$2
local DEST_PATH=$3
echo "------------------------------------------------"
echo "📡 Desplegando en: $USER@$IP:$DEST_PATH"
# 1. Crear directorio y asegurar permisos
ssh $USER@$IP "mkdir -p $DEST_PATH"
# 2. Subir binario
scp target/$TARGET/release/$BINARY_NAME $USER@$IP:$DEST_PATH/
# 3. Hacer ejecutable
ssh $USER@$IP "chmod +x $DEST_PATH/$BINARY_NAME"
# 4. Configurar Systemd
echo "⚙️ Configurando servicio Systemd para $USER..."
ssh $USER@$IP "sudo bash -c 'cat <<EOF > /etc/systemd/system/siax_monitor.service
[Unit]
Description=SIAX Monitor Agent - $IP
After=network.target
[Service]
Type=simple
User=$USER
WorkingDirectory=$DEST_PATH
ExecStart=$DEST_PATH/$BINARY_NAME
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
EOF'"
# 5. Reiniciar servicio
ssh $USER@$IP "sudo systemctl daemon-reload && sudo systemctl enable siax_monitor && sudo systemctl restart siax_monitor"
echo "✅ Servidor $IP configurado correctamente."
}
# --- LISTA PERSONALIZADA DE SERVIDORES ---
#deploy_to_server "192.168.1.140" "root" "/root/app"
deploy_to_server "192.168.10.145" "root" "/root/app"
deploy_to_server "192.168.10.150" "pablinux" "/home/pablinux/app"
deploy_to_server "192.168.10.160" "user_apps" "/home/user_apps/apps"
echo "------------------------------------------------"
echo "🎉 ¡Despliegue masivo completado!"

4
logs/errors.log Normal file
View File

@@ -0,0 +1,4 @@
[2026-01-11 22:01:17] [INFO] [Interface] Logs limpiados por el usuario
[2026-01-11 22:08:35] [INFO] [Sistema] Iniciando SIAX Agent
[2026-01-11 22:08:35] [INFO] [Sistema] Sistema SIAX completamente operativo
[2026-01-11 22:08:35] [INFO] [Monitor] Vigilando procesos para siax-intel [SIAX-Agent/0.1.0 (linux/x86_64; Rust-Monitor)]

View File

@@ -0,0 +1,129 @@
use serde::{Serialize, Deserialize};
use std::fs::{self, create_dir_all};
use std::path::Path;
use std::sync::{Arc, RwLock, OnceLock};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MonitoredApp {
pub name: String,
pub port: i32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AppConfig {
pub apps: Vec<MonitoredApp>,
}
impl Default for AppConfig {
fn default() -> Self {
AppConfig {
apps: vec![
MonitoredApp { name: "app_tareas".to_string(), port: 3000 },
MonitoredApp { name: "fidelizacion".to_string(), port: 3001 },
]
}
}
}
pub struct ConfigManager {
config_path: String,
config: Arc<RwLock<AppConfig>>,
}
impl ConfigManager {
pub fn new(config_path: &str) -> Self {
// Crear directorio config si no existe
if let Some(parent) = Path::new(config_path).parent() {
let _ = create_dir_all(parent);
}
// Cargar o crear configuración
let config = Self::load_config(config_path);
ConfigManager {
config_path: config_path.to_string(),
config: Arc::new(RwLock::new(config)),
}
}
fn load_config(path: &str) -> AppConfig {
match fs::read_to_string(path) {
Ok(content) => {
match serde_json::from_str(&content) {
Ok(config) => {
println!("✅ Configuración cargada desde: {}", path);
config
}
Err(e) => {
eprintln!("⚠️ Error parseando config: {}. Usando default.", e);
AppConfig::default()
}
}
}
Err(_) => {
println!(" Archivo de config no encontrado. Creando uno nuevo...");
let default_config = AppConfig::default();
let _ = Self::save_config_to_file(path, &default_config);
default_config
}
}
}
fn save_config_to_file(path: &str, config: &AppConfig) -> std::io::Result<()> {
let json = serde_json::to_string_pretty(config)?;
fs::write(path, json)?;
println!("💾 Configuración guardada en: {}", path);
Ok(())
}
pub fn get_apps(&self) -> Vec<MonitoredApp> {
let config = self.config.read().unwrap();
config.apps.clone()
}
pub fn add_app(&self, name: String, port: i32) -> 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));
}
config.apps.push(MonitoredApp { name, port });
// Guardar en disco
match Self::save_config_to_file(&self.config_path, &config) {
Ok(_) => Ok(()),
Err(e) => Err(format!("Error al guardar configuración: {}", e))
}
}
pub fn remove_app(&self, name: &str) -> Result<(), String> {
let mut config = self.config.write().unwrap();
let original_len = config.apps.len();
config.apps.retain(|app| app.name != name);
if config.apps.len() == original_len {
return Err(format!("La app '{}' no se encontró", name));
}
// Guardar en disco
match Self::save_config_to_file(&self.config_path, &config) {
Ok(_) => Ok(()),
Err(e) => Err(format!("Error al guardar configuración: {}", e))
}
}
pub fn get_arc(&self) -> Arc<RwLock<AppConfig>> {
Arc::clone(&self.config)
}
}
// Singleton global del ConfigManager
static CONFIG_MANAGER: OnceLock<ConfigManager> = OnceLock::new();
// ⚠️ IMPORTANTE: Esta función DEBE ser pública
pub fn get_config_manager() -> &'static ConfigManager {
CONFIG_MANAGER.get_or_init(|| ConfigManager::new("config/monitored_apps.json"))
}

View File

@@ -0,0 +1,260 @@
use axum::{
routing::{get, post},
response::Html,
Router,
extract::Form,
};
use std::net::SocketAddr;
use sysinfo::System;
use serde::Deserialize;
use crate::logger::{get_logger, LogLevel};
#[derive(Deserialize)]
struct ProcessForm {
app_name: String,
port: String,
}
pub async fn start_web_server(port: u16) {
let app = Router::new()
.route("/", get(index_handler))
.route("/scan", get(scan_processes_handler))
.route("/select", get(select_processes_handler))
.route("/add-process", post(add_process_handler))
.route("/logs", get(logs_handler))
.route("/clear-logs", post(clear_logs_handler));
let addr = SocketAddr::from(([0, 0, 0, 0], port));
println!("🖥️ Interface Web en: http://localhost:{}", port);
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
axum::serve(listener, app).await.unwrap();
}
async fn index_handler() -> Html<String> {
let template = include_str!("../web/index.html");
let html = template.replace("{{SERVER_NAME}}", "siax-intel");
Html(html)
}
async fn scan_processes_handler() -> Html<String> {
let mut sys = System::new_all();
sys.refresh_all();
let template = include_str!("../web/scan.html");
let mut content = String::new();
let mut node_count = 0;
for (pid, process) in sys.processes() {
let process_name = process.name();
if process_name.contains("node") {
node_count += 1;
let cpu = process.cpu_usage();
let mem_mb = process.memory() as f64 / 1024.0 / 1024.0;
let cwd = if let Some(path) = process.cwd() {
path.to_string_lossy().to_string()
} else {
"N/A".to_string()
};
content.push_str(&format!(
r#"
<div class="process">
<div><span class="pid">PID: {}</span> | <span class="name">{}</span></div>
<div><span class="cpu">CPU: {:.2}%</span> | <span class="mem">RAM: {:.2} MB</span></div>
<div class="path">📁 {}</div>
</div>
"#,
pid.as_u32(),
process_name,
cpu,
mem_mb,
cwd
));
}
}
if node_count == 0 {
content = r#"<div class="no-results">⚠️ No se detectaron procesos Node.js en ejecución</div>"#.to_string();
} else {
let summary = format!(r#"<p class="summary">✅ Total: {} proceso(s) Node.js detectado(s)</p>"#, node_count);
content = summary + &content;
}
let html = template.replace("{{CONTENT}}", &content);
Html(html)
}
async fn select_processes_handler() -> Html<String> {
let mut sys = System::new_all();
sys.refresh_all();
let template = include_str!("../web/select.html");
let mut processes_list = String::new();
let mut node_processes = Vec::new();
for (pid, process) in sys.processes() {
let process_name = process.name();
if process_name.contains("node") {
let cwd = if let Some(path) = process.cwd() {
path.to_string_lossy().to_string()
} else {
"N/A".to_string()
};
node_processes.push((pid.as_u32(), process_name.to_string(), cwd));
}
}
if node_processes.is_empty() {
processes_list = r#"<div class="no-results">⚠️ No se detectaron procesos Node.js en ejecución</div>"#.to_string();
} else {
for (pid, name, cwd) in node_processes {
let suggested_name = cwd.split(&['/', '\\'][..])
.filter(|s| !s.is_empty())
.last()
.unwrap_or("app");
processes_list.push_str(&format!(
r#"
<div class="process-item">
<div class="process-info">
<div><span class="pid">PID: {}</span> | {}</div>
<div class="path">📁 {}</div>
</div>
<button class="select-btn" onclick="fillForm('{}', {})">
✅ Seleccionar
</button>
</div>
"#,
pid, name, cwd, suggested_name, pid
));
}
}
let html = template.replace("{{PROCESSES_LIST}}", &processes_list);
Html(html)
}
async fn add_process_handler(Form(form): Form<ProcessForm>) -> Html<String> {
let logger = get_logger();
let template = include_str!("../web/success.html");
let port: i32 = form.port.parse().unwrap_or(0);
logger.info("Interface", &format!("Nuevo proceso agregado: {} en puerto {}", form.app_name, port));
let html = template
.replace("{{APP_NAME}}", &form.app_name)
.replace("{{PORT}}", &port.to_string());
Html(html)
}
async fn logs_handler() -> Html<String> {
let logger = get_logger();
let template = include_str!("../web/logs.html");
let logs = logger.read_logs(Some(100)); // Últimos 100 logs
// Calcular estadísticas
let mut info_count = 0;
let mut warning_count = 0;
let mut error_count = 0;
let mut critical_count = 0;
for log in &logs {
match log.level {
LogLevel::Info => info_count += 1,
LogLevel::Warning => warning_count += 1,
LogLevel::Error => error_count += 1,
LogLevel::Critical => critical_count += 1,
}
}
let stats = format!(
r#"
<div class="stat-item stat-info">
<div class="stat-number" style="color: #3b82f6;">{}</div>
<div class="stat-label">Info</div>
</div>
<div class="stat-item stat-warning">
<div class="stat-number" style="color: #f59e0b;">{}</div>
<div class="stat-label">Warnings</div>
</div>
<div class="stat-item stat-error">
<div class="stat-number" style="color: #ef4444;">{}</div>
<div class="stat-label">Errors</div>
</div>
<div class="stat-item stat-critical">
<div class="stat-number" style="color: #dc2626;">{}</div>
<div class="stat-label">Critical</div>
</div>
"#,
info_count, warning_count, error_count, critical_count
);
let mut logs_html = String::new();
if logs.is_empty() {
logs_html = r#"<div class="no-logs">📭 No hay logs registrados</div>"#.to_string();
} else {
for log in logs {
let level_class = match log.level {
LogLevel::Info => "info",
LogLevel::Warning => "warning",
LogLevel::Error => "error",
LogLevel::Critical => "critical",
};
let details_html = if let Some(details) = &log.details {
format!(r#"<div class="log-details">📝 {}</div>"#, details)
} else {
String::new()
};
logs_html.push_str(&format!(
r#"
<div class="log-entry log-{}" data-level="{}">
<div class="log-header">
<span class="log-module">[{}]</span>
<span class="log-timestamp">{}</span>
</div>
<div class="log-message">{} {}</div>
{}
</div>
"#,
level_class,
level_class,
log.module,
log.timestamp,
log.level.emoji(),
log.message,
details_html
));
}
}
let html = template
.replace("{{STATS}}", &stats)
.replace("{{LOGS}}", &logs_html);
Html(html)
}
async fn clear_logs_handler() -> Html<&'static str> {
let logger = get_logger();
match logger.clear_logs() {
Ok(_) => {
logger.info("Interface", "Logs limpiados por el usuario");
Html("OK")
}
Err(e) => {
logger.error("Interface", "Error al limpiar logs", Some(&e.to_string()));
Html("ERROR")
}
}
}

View File

@@ -0,0 +1,237 @@
use std::fs::{OpenOptions, create_dir_all};
use std::io::Write;
use std::path::Path;
use chrono::Local;
use serde::{Serialize, Deserialize};
use std::sync::OnceLock;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LogEntry {
pub timestamp: String,
pub level: LogLevel,
pub module: String,
pub message: String,
pub details: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum LogLevel {
Info,
Warning,
Error,
Critical,
}
impl LogLevel {
pub fn as_str(&self) -> &str {
match self {
LogLevel::Info => "INFO",
LogLevel::Warning => "WARNING",
LogLevel::Error => "ERROR",
LogLevel::Critical => "CRITICAL",
}
}
pub fn color(&self) -> &str {
match self {
LogLevel::Info => "#3b82f6",
LogLevel::Warning => "#f59e0b",
LogLevel::Error => "#ef4444",
LogLevel::Critical => "#dc2626",
}
}
pub fn emoji(&self) -> &str {
match self {
LogLevel::Info => "",
LogLevel::Warning => "⚠️",
LogLevel::Error => "",
LogLevel::Critical => "🔥",
}
}
}
pub struct Logger {
log_file: String,
}
impl Logger {
pub fn new() -> Self {
let log_file = "logs/errors.log".to_string();
// Crear directorio logs si no existe
if let Some(parent) = Path::new(&log_file).parent() {
let _ = create_dir_all(parent);
}
Logger { log_file }
}
pub fn log(&self, level: LogLevel, module: &str, message: &str, details: Option<&str>) {
let timestamp = Local::now().format("%Y-%m-%d %H:%M:%S").to_string();
let entry = LogEntry {
timestamp: timestamp.clone(),
level: level.clone(),
module: module.to_string(),
message: message.to_string(),
details: details.map(|d| d.to_string()),
};
// Escribir en archivo
if let Err(e) = self.write_to_file(&entry) {
eprintln!("Error escribiendo log: {}", e);
}
// También imprimir en consola
self.print_to_console(&entry);
}
fn write_to_file(&self, entry: &LogEntry) -> std::io::Result<()> {
let mut file = OpenOptions::new()
.create(true)
.append(true)
.open(&self.log_file)?;
let log_line = if let Some(details) = &entry.details {
format!(
"[{}] [{}] [{}] {} | {}\n",
entry.timestamp,
entry.level.as_str(),
entry.module,
entry.message,
details
)
} else {
format!(
"[{}] [{}] [{}] {}\n",
entry.timestamp,
entry.level.as_str(),
entry.module,
entry.message
)
};
file.write_all(log_line.as_bytes())?;
Ok(())
}
fn print_to_console(&self, entry: &LogEntry) {
let emoji = entry.level.emoji();
let level = entry.level.as_str();
if let Some(details) = &entry.details {
println!(
"{} [{}] [{}] {} | {}",
emoji, level, entry.module, entry.message, details
);
} else {
println!(
"{} [{}] [{}] {}",
emoji, level, entry.module, entry.message
);
}
}
pub fn read_logs(&self, limit: Option<usize>) -> Vec<LogEntry> {
use std::fs::File;
use std::io::{BufRead, BufReader};
let mut entries = Vec::new();
if let Ok(file) = File::open(&self.log_file) {
let reader = BufReader::new(file);
for line in reader.lines() {
if let Ok(line_content) = line {
if let Some(entry) = self.parse_log_line(&line_content) {
entries.push(entry);
}
}
}
}
// Invertir para mostrar los más recientes primero
entries.reverse();
// Limitar cantidad si se especifica
if let Some(limit) = limit {
entries.truncate(limit);
}
entries
}
fn parse_log_line(&self, line: &str) -> Option<LogEntry> {
// Formato: [timestamp] [level] [module] message | details
let parts: Vec<&str> = line.split("] [").collect();
if parts.len() < 3 {
return None;
}
let timestamp = parts[0].trim_start_matches('[').to_string();
let level_str = parts[1];
let module = parts[2].to_string();
let level = match level_str {
"INFO" => LogLevel::Info,
"WARNING" => LogLevel::Warning,
"ERROR" => LogLevel::Error,
"CRITICAL" => LogLevel::Critical,
_ => LogLevel::Info,
};
// El resto es el mensaje y detalles
let rest = parts.get(3..)?.join("] [");
let rest = rest.trim_end_matches(']');
let (message, details) = if let Some(idx) = rest.find(" | ") {
let msg = rest[..idx].to_string();
let det = rest[idx + 3..].to_string();
(msg, Some(det))
} else {
(rest.to_string(), None)
};
Some(LogEntry {
timestamp,
level,
module,
message,
details,
})
}
pub fn clear_logs(&self) -> std::io::Result<()> {
use std::fs;
fs::write(&self.log_file, "")?;
Ok(())
}
// Métodos de conveniencia
pub fn info(&self, module: &str, message: &str) {
self.log(LogLevel::Info, module, message, None);
}
pub fn warning(&self, module: &str, message: &str, details: Option<&str>) {
self.log(LogLevel::Warning, module, message, details);
}
pub fn error(&self, module: &str, message: &str, details: Option<&str>) {
self.log(LogLevel::Error, module, message, details);
}
pub fn critical(&self, module: &str, message: &str, details: Option<&str>) {
self.log(LogLevel::Critical, module, message, details);
}
}
// Singleton global del logger
static LOGGER: OnceLock<Logger> = OnceLock::new();
// ⚠️ IMPORTANTE: Esta función DEBE ser pública
pub fn get_logger() -> &'static Logger {
LOGGER.get_or_init(|| Logger::new())
}

39
src/main.rs Normal file
View File

@@ -0,0 +1,39 @@
mod monitor;
mod interface;
mod logger;
mod config;
use logger::get_logger;
use config::get_config_manager;
#[tokio::main]
async fn main() {
// Inicializar logger
let logger = get_logger();
logger.info("Sistema", "Iniciando SIAX Agent");
// Inicializar config manager
let config_manager = get_config_manager();
let apps = config_manager.get_apps();
println!("📋 Apps a monitorear: {:?}", apps);
let server_name = "siax-intel".to_string();
let api_key = "ak_VVeNzGxK2mCq8s7YpFtHjL3b9dR4TuZ6".to_string();
let cloud_url = "https://api.siax-system.net/api/apps_servcs/apps".to_string();
// 1. Iniciamos el Monitor
let monitor_handle = tokio::spawn(async move {
monitor::run_monitoring(server_name, api_key, cloud_url).await;
});
// 2. Iniciamos la Interface Web
let web_handle = tokio::spawn(async move {
interface::start_web_server(8080).await;
});
println!("✅ Sistema SIAX operativo. Monitor en segundo plano e Interface en puerto 8080.");
logger.info("Sistema", "Sistema SIAX completamente operativo");
// Esperamos a ambos
let _ = tokio::join!(monitor_handle, web_handle);
}

View File

@@ -0,0 +1,160 @@
use sysinfo::System;
use serde::Serialize;
use reqwest::header::{HeaderMap, HeaderValue, USER_AGENT};
use std::time::Duration;
use crate::logger::get_logger;
use crate::config::get_config_manager;
// User-Agent dinámico
fn generate_user_agent() -> String {
let version = env!("CARGO_PKG_VERSION");
let os = std::env::consts::OS;
let arch = std::env::consts::ARCH;
format!("SIAX-Agent/{} ({}/{}; Rust-Monitor)", version, os, arch)
}
#[derive(Serialize, Debug)]
struct AppStatusUpdate {
app_name: String,
server: String,
status: String,
port: i32,
pid: i32,
memory_usage: String,
cpu_usage: String,
last_check: String,
}
pub async fn run_monitoring(server_name: String, api_key: String, cloud_url: String) {
let logger = get_logger();
let config_manager = get_config_manager();
let mut sys = System::new_all();
let user_agent = generate_user_agent();
logger.info("Monitor", &format!("Vigilando procesos para {} [{}]", server_name, user_agent));
println!("🚀 Monitor: Vigilando procesos para {}", server_name);
println!("📡 User-Agent: {}", user_agent);
loop {
sys.refresh_all();
// ✨ LEER APPS DESDE CONFIG (dinámico)
let apps_to_monitor = config_manager.get_apps();
if apps_to_monitor.is_empty() {
logger.warning("Monitor", "No hay apps configuradas para monitorear", None);
}
for app in apps_to_monitor {
let data = collect_metrics(&sys, &app.name, app.port, &server_name);
match send_to_cloud(data, &api_key, &cloud_url, &user_agent).await {
Ok(_) => {},
Err(e) => {
logger.error(
"Monitor",
&format!("Error enviando {}", app.name),
Some(&e.to_string())
);
eprintln!("❌ Error enviando {}: {}", app.name, e);
}
}
}
tokio::time::sleep(Duration::from_secs(60)).await;
}
}
fn collect_metrics(sys: &System, name: &str, port: i32, server: &str) -> AppStatusUpdate {
let mut pid_encontrado = 0;
let mut cpu = 0.0;
let mut mem = 0.0;
let mut status = "stopped".to_string();
for (pid, process) in sys.processes() {
let process_name = process.name();
if process_name.contains("node") {
if let Some(cwd) = process.cwd() {
let cwd_str = cwd.to_string_lossy();
if cwd_str.contains(name) {
pid_encontrado = pid.as_u32() as i32;
cpu = process.cpu_usage();
mem = process.memory() as f64 / 1024.0 / 1024.0;
status = "running".to_string();
break;
}
}
}
}
let now = chrono::Local::now();
let timestamp = now.format("%Y-%m-%d %H:%M:%S").to_string();
AppStatusUpdate {
app_name: name.to_string(),
server: server.to_string(),
status,
port,
pid: pid_encontrado,
memory_usage: format!("{:.2}MB", mem),
cpu_usage: format!("{:.2}%", cpu),
last_check: timestamp,
}
}
async fn send_to_cloud(
data: AppStatusUpdate,
api_key: &str,
cloud_url: &str,
user_agent: &str
) -> Result<(), Box<dyn std::error::Error>> {
let logger = get_logger();
let client = reqwest::Client::new();
let mut headers = HeaderMap::new();
headers.insert(
"x-api-key",
HeaderValue::from_str(api_key)?
);
headers.insert(
"Content-Type",
HeaderValue::from_static("application/json")
);
headers.insert(
USER_AGENT,
HeaderValue::from_str(user_agent)?
);
let response = client
.post(cloud_url)
.headers(headers)
.json(&data)
.timeout(Duration::from_secs(10))
.send()
.await?;
if response.status().is_success() {
println!("📤 {} -> {} (PID: {}, CPU: {}, RAM: {})",
data.app_name,
data.status,
data.pid,
data.cpu_usage,
data.memory_usage
);
Ok(())
} else {
let status = response.status();
let error_text = response.text().await.unwrap_or_else(|_| "Sin respuesta".to_string());
logger.error(
"Monitor",
&format!("Error enviando datos de {}", data.app_name),
Some(&format!("HTTP {}: {}", status, error_text))
);
eprintln!("⚠️ Error HTTP {}: {}", status, error_text);
Err(format!("HTTP {}: {}", status, error_text).into())
}
}

78
web/index.html Normal file
View File

@@ -0,0 +1,78 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>SIAX Emergency Panel</title>
<style>
body {
background: #0f172a;
color: white;
font-family: sans-serif;
padding: 40px;
}
h1 {
color: #3b82f6;
}
.status-online {
color: #22c55e;
font-weight: bold;
}
.server-info {
color: #94a3b8;
}
.button-container {
margin-top: 30px;
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.btn {
padding: 12px 24px;
border: none;
color: white;
border-radius: 8px;
cursor: pointer;
font-size: 16px;
text-decoration: none;
display: inline-block;
}
.btn-primary {
background: #3b82f6;
}
.btn-primary:hover {
background: #2563eb;
}
.btn-success {
background: #22c55e;
}
.btn-success:hover {
background: #16a34a;
}
.btn-warning {
background: #f59e0b;
}
.btn-warning:hover {
background: #d97706;
}
</style>
</head>
<body>
<h1>🚨 SIAX EMERGENCY PANEL</h1>
<p>Estado del Agente: <span class="status-online">● ONLINE</span></p>
<p class="server-info">Servidor: {{SERVER_NAME}}</p>
<div class="button-container">
<a href="/scan" class="btn btn-primary">
🔍 Escanear Sistema
</a>
<a href="/select" class="btn btn-success">
⚙️ Gestionar Procesos
</a>
<a href="/logs" class="btn btn-warning">
📋 Ver Logs
</a>
</div>
</body>
</html>

246
web/logs.html Normal file
View File

@@ -0,0 +1,246 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Logs del Sistema - SIAX</title>
<style>
body {
background: #0f172a;
color: white;
font-family: 'Courier New', monospace;
padding: 40px;
margin: 0;
}
h1 {
color: #3b82f6;
font-family: sans-serif;
}
.controls {
margin: 20px 0;
display: flex;
gap: 10px;
align-items: center;
}
.btn {
padding: 10px 20px;
border: none;
color: white;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
text-decoration: none;
display: inline-block;
}
.btn-primary {
background: #3b82f6;
}
.btn-primary:hover {
background: #2563eb;
}
.btn-danger {
background: #ef4444;
}
.btn-danger:hover {
background: #dc2626;
}
.btn-secondary {
background: #64748b;
}
.btn-secondary:hover {
background: #475569;
}
.stats {
background: #1e293b;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
display: flex;
gap: 20px;
}
.stat-item {
flex: 1;
text-align: center;
padding: 15px;
border-radius: 6px;
}
.stat-info {
background: rgba(59, 130, 246, 0.1);
border: 1px solid #3b82f6;
}
.stat-warning {
background: rgba(245, 158, 11, 0.1);
border: 1px solid #f59e0b;
}
.stat-error {
background: rgba(239, 68, 68, 0.1);
border: 1px solid #ef4444;
}
.stat-critical {
background: rgba(220, 38, 38, 0.1);
border: 1px solid #dc2626;
}
.stat-number {
font-size: 32px;
font-weight: bold;
margin-bottom: 5px;
}
.stat-label {
font-size: 12px;
text-transform: uppercase;
opacity: 0.7;
}
.log-entry {
background: #1e293b;
padding: 15px;
margin: 10px 0;
border-radius: 8px;
border-left: 4px solid;
font-size: 13px;
line-height: 1.6;
}
.log-info {
border-left-color: #3b82f6;
}
.log-warning {
border-left-color: #f59e0b;
}
.log-error {
border-left-color: #ef4444;
}
.log-critical {
border-left-color: #dc2626;
background: rgba(220, 38, 38, 0.1);
}
.log-header {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
font-weight: bold;
}
.log-timestamp {
color: #94a3b8;
font-size: 11px;
}
.log-level {
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: bold;
}
.log-module {
color: #60a5fa;
}
.log-message {
margin: 8px 0;
}
.log-details {
background: #0f172a;
padding: 10px;
border-radius: 4px;
margin-top: 8px;
color: #94a3b8;
font-size: 12px;
}
.no-logs {
background: #1e293b;
padding: 40px;
text-align: center;
border-radius: 8px;
color: #94a3b8;
}
.filter-bar {
background: #1e293b;
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.filter-checkbox {
display: flex;
align-items: center;
gap: 5px;
}
.filter-checkbox input {
width: 18px;
height: 18px;
cursor: pointer;
}
.filter-checkbox label {
cursor: pointer;
user-select: none;
}
</style>
</head>
<body>
<h1>📋 Logs del Sistema SIAX</h1>
<div class="controls">
<button onclick="location.reload()" class="btn btn-primary">🔄 Refrescar</button>
<button onclick="clearLogs()" class="btn btn-danger">🗑️ Limpiar Logs</button>
<a href="/" class="btn btn-secondary">← Volver al Panel</a>
</div>
<div class="stats">
{{STATS}}
</div>
<div class="filter-bar">
<span style="color: #60a5fa; font-weight: bold;">Filtrar por nivel:</span>
<div class="filter-checkbox">
<input type="checkbox" id="filter-info" checked onchange="filterLogs()">
<label for="filter-info"> Info</label>
</div>
<div class="filter-checkbox">
<input type="checkbox" id="filter-warning" checked onchange="filterLogs()">
<label for="filter-warning">⚠️ Warning</label>
</div>
<div class="filter-checkbox">
<input type="checkbox" id="filter-error" checked onchange="filterLogs()">
<label for="filter-error">❌ Error</label>
</div>
<div class="filter-checkbox">
<input type="checkbox" id="filter-critical" checked onchange="filterLogs()">
<label for="filter-critical">🔥 Critical</label>
</div>
</div>
<div id="logs-container">
{{LOGS}}
</div>
<script>
function clearLogs() {
if (confirm('¿Estás seguro de que quieres eliminar todos los logs?')) {
fetch('/clear-logs', { method: 'POST' })
.then(() => location.reload())
.catch(err => alert('Error al limpiar logs: ' + err));
}
}
function filterLogs() {
const showInfo = document.getElementById('filter-info').checked;
const showWarning = document.getElementById('filter-warning').checked;
const showError = document.getElementById('filter-error').checked;
const showCritical = document.getElementById('filter-critical').checked;
const logs = document.querySelectorAll('.log-entry');
logs.forEach(log => {
const level = log.dataset.level;
let show = false;
if (level === 'info' && showInfo) show = true;
if (level === 'warning' && showWarning) show = true;
if (level === 'error' && showError) show = true;
if (level === 'critical' && showCritical) show = true;
log.style.display = show ? 'block' : 'none';
});
}
// Auto-refresh cada 30 segundos
setTimeout(() => location.reload(), 30000);
</script>
</body>
</html>

71
web/scan.html Normal file
View File

@@ -0,0 +1,71 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Scan Results - SIAX</title>
<style>
body {
background: #0f172a;
color: white;
font-family: monospace;
padding: 40px;
}
h1 {
color: #3b82f6;
}
.process {
background: #1e293b;
padding: 15px;
margin: 10px 0;
border-radius: 8px;
border-left: 4px solid #3b82f6;
}
.pid {
color: #22c55e;
font-weight: bold;
}
.name {
color: #60a5fa;
}
.cpu {
color: #f59e0b;
}
.mem {
color: #8b5cf6;
}
.path {
color: #94a3b8;
font-size: 12px;
margin-top: 5px;
}
.summary {
color: #22c55e;
font-size: 18px;
margin: 20px 0;
}
.no-results {
background: #7f1d1d;
padding: 20px;
border-radius: 8px;
border-left: 4px solid #ef4444;
}
.back-btn {
display: inline-block;
padding: 10px 20px;
background: #64748b;
color: white;
text-decoration: none;
border-radius: 8px;
margin-top: 20px;
}
.back-btn:hover {
background: #475569;
}
</style>
</head>
<body>
<h1>🔍 Escaneo de Procesos Node.js</h1>
{{CONTENT}}
<a href="/" class="back-btn">← Volver al Panel</a>
</body>
</html>

160
web/select.html Normal file
View File

@@ -0,0 +1,160 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Gestionar Procesos - SIAX</title>
<style>
body {
background: #0f172a;
color: white;
font-family: sans-serif;
padding: 40px;
}
h1 {
color: #3b82f6;
}
h2 {
color: #60a5fa;
margin-top: 40px;
}
.process-item {
background: #1e293b;
padding: 15px;
margin: 10px 0;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: space-between;
}
.process-info {
flex-grow: 1;
}
.pid {
color: #22c55e;
font-weight: bold;
}
.path {
color: #94a3b8;
font-size: 12px;
margin-top: 5px;
}
.select-btn {
padding: 8px 16px;
background: #22c55e;
border: none;
color: white;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
}
.select-btn:hover {
background: #16a34a;
}
.form-section {
background: #1e293b;
padding: 25px;
border-radius: 12px;
margin-top: 20px;
border: 2px solid #3b82f6;
transition: border-color 0.3s;
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
color: #60a5fa;
margin-bottom: 5px;
font-weight: bold;
}
input {
width: 100%;
padding: 10px;
background: #0f172a;
border: 2px solid #475569;
border-radius: 6px;
color: white;
font-size: 14px;
box-sizing: border-box;
}
input:focus {
outline: none;
border-color: #3b82f6;
}
small {
color: #94a3b8;
display: block;
margin-top: 5px;
}
.submit-btn {
padding: 12px 24px;
background: #3b82f6;
border: none;
color: white;
border-radius: 8px;
cursor: pointer;
font-size: 16px;
margin-top: 10px;
}
.submit-btn:hover {
background: #2563eb;
}
.back-btn {
display: inline-block;
padding: 10px 20px;
background: #64748b;
color: white;
text-decoration: none;
border-radius: 8px;
margin-top: 20px;
}
.back-btn:hover {
background: #475569;
}
.no-results {
background: #7f1d1d;
padding: 20px;
border-radius: 8px;
border-left: 4px solid #ef4444;
}
</style>
</head>
<body>
<h1>⚙️ Gestionar Procesos a Monitorear</h1>
<h2>📋 Procesos Node.js Detectados</h2>
{{PROCESSES_LIST}}
<h2> Agregar Proceso Personalizado</h2>
<div class="form-section">
<form method="POST" action="/add-process">
<div class="form-group">
<label for="app_name">Nombre de la Aplicación:</label>
<input type="text" id="app_name" name="app_name" placeholder="Ej: app_tareas, fidelizacion, mi-api" required>
<small>💡 Este nombre se usará para identificar el proceso en el directorio de trabajo</small>
</div>
<div class="form-group">
<label for="port">Puerto:</label>
<input type="number" id="port" name="port" placeholder="Ej: 3000, 3001, 8080" required>
<small>💡 Puerto donde corre la aplicación</small>
</div>
<button type="submit" class="submit-btn">💾 Guardar y Monitorear</button>
</form>
</div>
<a href="/" class="back-btn">← Volver al Panel</a>
<script>
function fillForm(appName, pid) {
document.getElementById('app_name').value = appName;
document.querySelector('.form-section').scrollIntoView({ behavior: 'smooth' });
document.querySelector('.form-section').style.borderColor = '#22c55e';
setTimeout(() => {
document.querySelector('.form-section').style.borderColor = '#3b82f6';
}, 2000);
}
</script>
</body>
</html>

58
web/success.html Normal file
View File

@@ -0,0 +1,58 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="refresh" content="3;url=/select">
<title>Proceso Agregado - SIAX</title>
<style>
body {
background: #0f172a;
color: white;
font-family: sans-serif;
padding: 40px;
text-align: center;
}
.success {
background: #064e3b;
border: 2px solid #22c55e;
padding: 40px;
border-radius: 16px;
max-width: 500px;
margin: 100px auto;
}
h1 {
color: #22c55e;
margin-bottom: 20px;
}
.details {
background: #0f172a;
padding: 20px;
border-radius: 8px;
margin-top: 20px;
text-align: left;
}
.label {
color: #60a5fa;
font-weight: bold;
}
.redirect-msg {
color: #94a3b8;
margin-top: 30px;
font-size: 14px;
}
</style>
</head>
<body>
<div class="success">
<h1>✅ Proceso Agregado Exitosamente</h1>
<p style="color:#94a3b8;">El proceso será monitoreado en el próximo ciclo</p>
<div class="details">
<p><span class="label">Aplicación:</span> {{APP_NAME}}</p>
<p><span class="label">Puerto:</span> {{PORT}}</p>
</div>
<p class="redirect-msg">Redirigiendo en 3 segundos...</p>
</div>
</body>
</html>