Last active 2 weeks ago Unlisted

Código completo del servidor HTTP crustaceo-api con Axum 0.8

main.rs Raw
1use axum::{
2 Router,
3 extract::{Query, State},
4 response::{IntoResponse, Response},
5 routing::get,
6 http::StatusCode,
7};
8use serde::{Deserialize, Serialize};
9use std::process::Command;
10use tower_http::cors::{CorsLayer, Any};
11use tower_http::trace::TraceLayer;
12
13struct AppError(anyhow::Error);
14
15impl IntoResponse for AppError {
16 fn into_response(self) -> Response {
17 let body = serde_json::json!({
18 "error": self.0.to_string(),
19 });
20 (StatusCode::INTERNAL_SERVER_ERROR, axum::Json(body)).into_response()
21 }
22}
23
24impl<E: Into<anyhow::Error>> From<E> for AppError {
25 fn from(err: E) -> Self {
26 AppError(err.into())
27 }
28}
29
30#[derive(Clone)]
31struct AppState {
32 start_time: chrono::DateTime<chrono::Local>,
33}
34
35#[derive(Serialize)]
36struct HealthResponse {
37 status: String,
38 version: String,
39}
40
41async fn health() -> axum::Json<HealthResponse> {
42 axum::Json(HealthResponse {
43 status: "ok".to_string(),
44 version: "0.1.0".to_string(),
45 })
46}
47
48#[derive(Serialize)]
49struct SystemInfo {
50 hostname: String,
51 uptime: String,
52 cpu: String,
53 memory: String,
54 timestamp: String,
55}
56
57async fn system_info() -> Result<axum::Json<SystemInfo>, AppError> {
58 let hostname = String::from_utf8_lossy(
59 &Command::new("hostname").output()?.stdout
60 ).trim().to_string();
61 let uptime = String::from_utf8_lossy(
62 &Command::new("uptime").arg("-p").output()?.stdout
63 ).trim().to_string();
64 let cpu = String::from_utf8_lossy(
65 &Command::new("sh")
66 .args(["-c", "lscpu | grep 'Model name' | cut -d: -f2 | xargs"])
67 .output()?.stdout
68 ).trim().to_string();
69 let memory = String::from_utf8_lossy(
70 &Command::new("free").arg("-h").args(["--si"]).output()?.stdout
71 ).trim().to_string();
72 Ok(axum::Json(SystemInfo {
73 hostname, uptime, cpu, memory,
74 timestamp: chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string(),
75 }))
76}
77
78#[derive(Deserialize)]
79struct LogQuery {
80 lines: Option<u32>,
81}
82
83#[derive(Serialize)]
84struct LogResponse {
85 lines: u32,
86 logs: Vec<String>,
87}
88
89async fn get_logs(Query(params): Query<LogQuery>) -> Result<axum::Json<LogResponse>, AppError> {
90 let lines = params.lines.unwrap_or(50).clamp(1, 500);
91 let output = Command::new("journalctl")
92 .args(["-n", &lines.to_string(), "--no-pager", "-o", "cat"])
93 .output()
94 .map_err(|e| AppError(anyhow::anyhow!("Error ejecutando journalctl: {}", e)))?;
95 if !output.status.success() {
96 return Err(AppError(anyhow::anyhow!(
97 "journalctl falló: {}",
98 String::from_utf8_lossy(&output.stderr)
99 )));
100 }
101 let logs: Vec<String> = String::from_utf8_lossy(&output.stdout)
102 .lines()
103 .map(|l| l.to_string())
104 .collect();
105 Ok(axum::Json(LogResponse { lines, logs }))
106}
107
108async fn get_disk(State(state): State<AppState>) -> Result<axum::Json<serde_json::Value>, AppError> {
109 let output = Command::new("df")
110 .args(["-h", "--type=ext4", "--type=btrfs", "--type=xfs",
111 "--exclude-type=tmpfs", "--exclude-type=devtmpfs"])
112 .output()?;
113 let stdout = String::from_utf8_lossy(&output.stdout);
114 let mut disks = Vec::new();
115 for line in stdout.lines().skip(1) {
116 let parts: Vec<&str> = line.split_whitespace().collect();
117 if parts.len() >= 6 {
118 disks.push(serde_json::json!({
119 "filesystem": parts[0],
120 "size": parts[1],
121 "used": parts[2],
122 "avail": parts[3],
123 "use_percent": parts[4],
124 "mounted_on": parts[5],
125 }));
126 }
127 }
128 let uptime_secs = (chrono::Local::now() - state.start_time).num_seconds();
129 Ok(axum::Json(serde_json::json!({
130 "disks": disks,
131 "server_uptime_seconds": uptime_secs,
132 "timestamp": chrono::Local::now().to_rfc3339(),
133 })))
134}
135
136#[tokio::main]
137async fn main() {
138 tracing_subscriber::fmt()
139 .with_env_filter(
140 tracing_subscriber::EnvFilter::try_from_default_env()
141 .unwrap_or_else(|_| "crustaceo_api=debug,tower_http=debug".into()),
142 )
143 .init();
144 let state = AppState {
145 start_time: chrono::Local::now(),
146 };
147 let app = Router::new()
148 .route("/health", get(health))
149 .route("/info", get(system_info))
150 .route("/logs", get(get_logs))
151 .route("/disk", get(get_disk))
152 .layer(CorsLayer::new().allow_origin(Any).allow_methods(Any).allow_headers(Any))
153 .layer(TraceLayer::new_for_http())
154 .with_state(state);
155 let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
156 .await
157 .expect("No se pudo enlazar al puerto 3000");
158 println!("🦀 crustaceo-api escuchando en http://0.0.0.0:3000");
159 axum::serve(listener, app).await.unwrap();
160}
161