use axum::{ Router, extract::{Query, State}, response::{IntoResponse, Response}, routing::get, http::StatusCode, }; use serde::{Deserialize, Serialize}; use std::process::Command; use tower_http::cors::{CorsLayer, Any}; use tower_http::trace::TraceLayer; struct AppError(anyhow::Error); impl IntoResponse for AppError { fn into_response(self) -> Response { let body = serde_json::json!({ "error": self.0.to_string(), }); (StatusCode::INTERNAL_SERVER_ERROR, axum::Json(body)).into_response() } } impl> From for AppError { fn from(err: E) -> Self { AppError(err.into()) } } #[derive(Clone)] struct AppState { start_time: chrono::DateTime, } #[derive(Serialize)] struct HealthResponse { status: String, version: String, } async fn health() -> axum::Json { axum::Json(HealthResponse { status: "ok".to_string(), version: "0.1.0".to_string(), }) } #[derive(Serialize)] struct SystemInfo { hostname: String, uptime: String, cpu: String, memory: String, timestamp: String, } async fn system_info() -> Result, AppError> { let hostname = String::from_utf8_lossy( &Command::new("hostname").output()?.stdout ).trim().to_string(); let uptime = String::from_utf8_lossy( &Command::new("uptime").arg("-p").output()?.stdout ).trim().to_string(); let cpu = String::from_utf8_lossy( &Command::new("sh") .args(["-c", "lscpu | grep 'Model name' | cut -d: -f2 | xargs"]) .output()?.stdout ).trim().to_string(); let memory = String::from_utf8_lossy( &Command::new("free").arg("-h").args(["--si"]).output()?.stdout ).trim().to_string(); Ok(axum::Json(SystemInfo { hostname, uptime, cpu, memory, timestamp: chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string(), })) } #[derive(Deserialize)] struct LogQuery { lines: Option, } #[derive(Serialize)] struct LogResponse { lines: u32, logs: Vec, } async fn get_logs(Query(params): Query) -> Result, AppError> { let lines = params.lines.unwrap_or(50).clamp(1, 500); let output = Command::new("journalctl") .args(["-n", &lines.to_string(), "--no-pager", "-o", "cat"]) .output() .map_err(|e| AppError(anyhow::anyhow!("Error ejecutando journalctl: {}", e)))?; if !output.status.success() { return Err(AppError(anyhow::anyhow!( "journalctl falló: {}", String::from_utf8_lossy(&output.stderr) ))); } let logs: Vec = String::from_utf8_lossy(&output.stdout) .lines() .map(|l| l.to_string()) .collect(); Ok(axum::Json(LogResponse { lines, logs })) } async fn get_disk(State(state): State) -> Result, AppError> { let output = Command::new("df") .args(["-h", "--type=ext4", "--type=btrfs", "--type=xfs", "--exclude-type=tmpfs", "--exclude-type=devtmpfs"]) .output()?; let stdout = String::from_utf8_lossy(&output.stdout); let mut disks = Vec::new(); for line in stdout.lines().skip(1) { let parts: Vec<&str> = line.split_whitespace().collect(); if parts.len() >= 6 { disks.push(serde_json::json!({ "filesystem": parts[0], "size": parts[1], "used": parts[2], "avail": parts[3], "use_percent": parts[4], "mounted_on": parts[5], })); } } let uptime_secs = (chrono::Local::now() - state.start_time).num_seconds(); Ok(axum::Json(serde_json::json!({ "disks": disks, "server_uptime_seconds": uptime_secs, "timestamp": chrono::Local::now().to_rfc3339(), }))) } #[tokio::main] async fn main() { tracing_subscriber::fmt() .with_env_filter( tracing_subscriber::EnvFilter::try_from_default_env() .unwrap_or_else(|_| "crustaceo_api=debug,tower_http=debug".into()), ) .init(); let state = AppState { start_time: chrono::Local::now(), }; let app = Router::new() .route("/health", get(health)) .route("/info", get(system_info)) .route("/logs", get(get_logs)) .route("/disk", get(get_disk)) .layer(CorsLayer::new().allow_origin(Any).allow_methods(Any).allow_headers(Any)) .layer(TraceLayer::new_for_http()) .with_state(state); let listener = tokio::net::TcpListener::bind("0.0.0.0:3000") .await .expect("No se pudo enlazar al puerto 3000"); println!("🦀 crustaceo-api escuchando en http://0.0.0.0:3000"); axum::serve(listener, app).await.unwrap(); }