Files
SIAX-MONITOR/src/api/handlers.rs
pablinux fbc89e9bf0 feat: Agregar sistema de tabs en logs.html con visualización de errores del sistema
Backend (handlers.rs + main.rs):
- Nuevo endpoint GET /api/logs/errors
- Lee logs/errors.log y retorna últimas 500 líneas
- Parsea y formatea logs con niveles (INFO, WARN, ERROR)

Frontend (logs.html):
- Sistema de tabs con 2 pestañas:
  * Tab 1: "Logs de App" - logs en tiempo real vía WebSocket (journalctl)
  * Tab 2: "Errores del Sistema" - logs del archivo errors.log
- Carga apps desde /api/apps (ya usaba el JSON correctamente)
- Colorización por nivel de log:
  * ERROR = rojo
  * WARN = amarillo
  * INFO = azul
- Auto-scroll en ambos tabs
- Diseño consistente con el resto de la UI

Ahora logs.html muestra:
 Logs de aplicaciones individuales (systemd/journalctl)
 Logs de errores del sistema SIAX Monitor (logs/errors.log)
 Navegación por tabs
 Lista de apps desde monitored_apps.json
2026-01-18 04:07:37 -05:00

361 lines
12 KiB
Rust

use axum::{
extract::{Path, State},
http::StatusCode,
response::Json,
};
use std::sync::Arc;
use sysinfo::System;
use crate::orchestrator::{AppManager, LifecycleManager};
use crate::models::{ServiceConfig, RestartPolicy, AppType};
use super::dto::*;
pub struct ApiState {
pub app_manager: Arc<AppManager>,
pub lifecycle_manager: Arc<LifecycleManager>,
}
pub async fn register_app_handler(
State(state): State<Arc<ApiState>>,
Json(payload): Json<RegisterAppRequest>,
) -> Result<Json<ApiResponse<OperationResponse>>, StatusCode> {
// Parsear tipo de aplicación
let app_type = match payload.app_type.to_lowercase().as_str() {
"nodejs" | "node" => AppType::NodeJs,
"python" | "py" => AppType::Python,
_ => return Ok(Json(ApiResponse::error(
"Tipo de aplicación inválido. Use 'nodejs' o 'python'".to_string()
))),
};
// Parsear política de reinicio
let restart_policy = match payload.restart_policy.to_lowercase().as_str() {
"always" => RestartPolicy::Always,
"on-failure" | "onfailure" => RestartPolicy::OnFailure,
"no" | "never" => RestartPolicy::No,
_ => RestartPolicy::Always,
};
let config = ServiceConfig {
app_name: payload.app_name.clone(),
script_path: payload.script_path,
working_directory: payload.working_directory,
user: payload.user,
environment: payload.environment,
restart_policy,
app_type,
description: payload.description,
custom_executable: payload.custom_executable,
use_npm_start: payload.use_npm_start,
};
match state.app_manager.register_app(config) {
Ok(_) => Ok(Json(ApiResponse::success(OperationResponse {
app_name: payload.app_name,
operation: "register".to_string(),
success: true,
message: "Aplicación registrada exitosamente".to_string(),
}))),
Err(e) => Ok(Json(ApiResponse::error(e.to_string()))),
}
}
pub async fn unregister_app_handler(
State(state): State<Arc<ApiState>>,
Path(app_name): Path<String>,
) -> Result<Json<ApiResponse<OperationResponse>>, StatusCode> {
match state.app_manager.unregister_app(&app_name) {
Ok(_) => Ok(Json(ApiResponse::success(OperationResponse {
app_name: app_name.clone(),
operation: "unregister".to_string(),
success: true,
message: "Aplicación eliminada exitosamente".to_string(),
}))),
Err(e) => Ok(Json(ApiResponse::error(e.to_string()))),
}
}
pub async fn start_app_handler(
State(state): State<Arc<ApiState>>,
Path(app_name): Path<String>,
) -> Result<Json<ApiResponse<OperationResponse>>, StatusCode> {
match state.lifecycle_manager.start_app(&app_name) {
Ok(_) => Ok(Json(ApiResponse::success(OperationResponse {
app_name: app_name.clone(),
operation: "start".to_string(),
success: true,
message: "Aplicación iniciada exitosamente".to_string(),
}))),
Err(e) => Ok(Json(ApiResponse::error(e.to_string()))),
}
}
pub async fn stop_app_handler(
State(state): State<Arc<ApiState>>,
Path(app_name): Path<String>,
) -> Result<Json<ApiResponse<OperationResponse>>, StatusCode> {
match state.lifecycle_manager.stop_app(&app_name) {
Ok(_) => Ok(Json(ApiResponse::success(OperationResponse {
app_name: app_name.clone(),
operation: "stop".to_string(),
success: true,
message: "Aplicación detenida exitosamente".to_string(),
}))),
Err(e) => Ok(Json(ApiResponse::error(e.to_string()))),
}
}
pub async fn restart_app_handler(
State(state): State<Arc<ApiState>>,
Path(app_name): Path<String>,
) -> Result<Json<ApiResponse<OperationResponse>>, StatusCode> {
match state.lifecycle_manager.restart_app(&app_name) {
Ok(_) => Ok(Json(ApiResponse::success(OperationResponse {
app_name: app_name.clone(),
operation: "restart".to_string(),
success: true,
message: "Aplicación reiniciada exitosamente".to_string(),
}))),
Err(e) => Ok(Json(ApiResponse::error(e.to_string()))),
}
}
pub async fn get_app_status_handler(
State(_state): State<Arc<ApiState>>,
Path(app_name): Path<String>,
) -> Result<Json<ApiResponse<AppStatusResponse>>, StatusCode> {
use crate::config::get_config_manager;
use crate::systemd::SystemCtl;
use crate::models::{AppStatus, ServiceStatus};
let config_manager = get_config_manager();
let apps = config_manager.get_apps();
// Buscar la app en monitored_apps.json
let app = apps.iter().find(|a| a.name == app_name);
match app {
Some(app) => {
let service_name = format!("siax-app-{}.service", app.name);
let systemd_status = SystemCtl::status(&service_name);
// Obtener métricas del proceso
let mut sys = System::new_all();
sys.refresh_all();
let mut pid = None;
let mut cpu_usage = 0.0;
let mut memory_mb = 0.0;
// Buscar proceso por nombre de app
for (process_pid, process) in sys.processes() {
let cmd = process.cmd().join(" ");
if cmd.contains(&app.name) || cmd.contains(&app.entry_point) {
pid = Some(process_pid.as_u32() as i32);
cpu_usage = process.cpu_usage();
memory_mb = process.memory() as f64 / 1024.0 / 1024.0;
break;
}
}
let status = match systemd_status {
ServiceStatus::Active => "Running",
ServiceStatus::Inactive => "Stopped",
ServiceStatus::Failed => "Failed",
ServiceStatus::Activating => "Starting",
ServiceStatus::Deactivating => "Stopping",
ServiceStatus::Unknown => "Unknown",
};
let response = AppStatusResponse {
name: app.name.clone(),
status: status.to_string(),
pid,
cpu_usage,
memory_usage: format!("{:.2} MB", memory_mb),
systemd_status: systemd_status.as_str().to_string(),
last_updated: chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string(),
};
Ok(Json(ApiResponse::success(response)))
}
None => Ok(Json(ApiResponse::error(
format!("Aplicación '{}' no encontrada", app_name)
))),
}
}
pub async fn list_apps_handler(
State(_state): State<Arc<ApiState>>,
) -> Result<Json<serde_json::Value>, StatusCode> {
use crate::config::get_config_manager;
use crate::systemd::SystemCtl;
// Leer apps desde monitored_apps.json (apps descubiertas + registradas)
let config_manager = get_config_manager();
let monitored_apps = config_manager.get_apps();
// Crear respuesta con información de cada app
let mut apps_with_status = Vec::new();
for app in monitored_apps {
// Verificar estado en systemd
let service_name = format!("siax-app-{}.service", app.name);
let systemd_status = SystemCtl::status(&service_name);
let status = match systemd_status {
crate::models::ServiceStatus::Active => "Running",
crate::models::ServiceStatus::Inactive => "Stopped",
crate::models::ServiceStatus::Failed => "Failed",
crate::models::ServiceStatus::Activating => "Starting",
crate::models::ServiceStatus::Deactivating => "Stopping",
crate::models::ServiceStatus::Unknown => "Unknown",
};
apps_with_status.push(serde_json::json!({
"name": app.name,
"status": status,
"port": app.port,
"service_name": app.service_name,
}));
}
let total = apps_with_status.len();
Ok(Json(serde_json::json!({
"success": true,
"data": {
"apps": apps_with_status,
"total": total
}
})))
}
pub async fn scan_processes_handler() -> Result<Json<ApiResponse<ProcessScanResponse>>, StatusCode> {
let mut sys = System::new_all();
sys.refresh_all();
let mut detected_processes = Vec::new();
for (pid, process) in sys.processes() {
let process_name = process.name().to_lowercase();
let cmd = process.cmd().join(" ").to_lowercase();
let process_type = if process_name.contains("node") || cmd.contains("node") {
Some("nodejs")
} else if process_name.contains("python") || cmd.contains("python") {
Some("python")
} else {
None
};
if let Some(ptype) = process_type {
detected_processes.push(DetectedProcess {
pid: pid.as_u32() as i32,
name: process.name().to_string(),
user: process.user_id().map(|u| u.to_string()),
cpu_usage: process.cpu_usage() as f64,
memory_mb: process.memory() as f64 / 1024.0 / 1024.0,
process_type: ptype.to_string(),
});
}
}
let total = detected_processes.len();
Ok(Json(ApiResponse::success(ProcessScanResponse {
processes: detected_processes,
total,
})))
}
pub async fn health_handler(
State(state): State<Arc<ApiState>>,
) -> Result<Json<ApiResponse<HealthResponse>>, StatusCode> {
use std::path::Path;
let config_path = "config/monitored_apps.json";
let config_exists = Path::new(config_path).exists();
let apps = state.app_manager.list_apps();
let apps_count = apps.len();
let mut systemd_services = Vec::new();
for app_name in &apps {
let service_name = format!("siax-app-{}.service", app_name);
systemd_services.push(service_name);
}
Ok(Json(ApiResponse::success(HealthResponse {
status: "ok".to_string(),
config_loaded: config_exists,
config_path: config_path.to_string(),
apps_count,
systemd_services,
version: env!("CARGO_PKG_VERSION").to_string(),
})))
}
/// Endpoint para ver las apps monitoreadas desde el JSON
pub async fn get_monitored_apps_handler() -> Result<Json<serde_json::Value>, StatusCode> {
use crate::config::get_config_manager;
let config_manager = get_config_manager();
let apps = config_manager.get_apps();
let response = serde_json::json!({
"success": true,
"count": apps.len(),
"apps": apps
});
Ok(Json(response))
}
/// Endpoint para obtener los logs de errores del sistema
pub async fn get_system_error_logs() -> Result<Json<serde_json::Value>, StatusCode> {
use std::fs;
use std::path::Path;
let log_path = "logs/errors.log";
// Verificar si el archivo existe
if !Path::new(log_path).exists() {
return Ok(Json(serde_json::json!({
"success": true,
"logs": [],
"message": "Archivo de logs no encontrado"
})));
}
// Leer el archivo
match fs::read_to_string(log_path) {
Ok(content) => {
// Dividir en líneas y tomar las últimas 500
let lines: Vec<&str> = content.lines().collect();
let total = lines.len();
let recent_lines: Vec<&str> = if lines.len() > 500 {
lines[lines.len() - 500..].to_vec()
} else {
lines
};
Ok(Json(serde_json::json!({
"success": true,
"logs": recent_lines,
"total_lines": total
})))
}
Err(e) => {
Ok(Json(serde_json::json!({
"success": false,
"error": format!("Error leyendo archivo: {}", e)
})))
}
}
}