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:
129
src/config.rs
129
src/config.rs
@@ -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"))
|
||||
}
|
||||
260
src/interface.rs
260
src/interface.rs
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
237
src/logger.rs
237
src/logger.rs
@@ -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
39
src/main.rs
Normal 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);
|
||||
}
|
||||
160
src/monitor.rs
160
src/monitor.rs
@@ -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())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user