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
361 lines
12 KiB
Rust
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)
|
|
})))
|
|
}
|
|
}
|
|
}
|