diff --git a/Cargo.lock b/Cargo.lock index 05ac875..a65165d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -85,6 +85,12 @@ version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +[[package]] +name = "ascii" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" + [[package]] name = "base64" version = "0.22.1" @@ -119,6 +125,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "chunked_transfer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e4de3bc4ea267985becf712dc6d9eed8b04c953b3fcfb339ebc87acd9804901" + [[package]] name = "clap" version = "4.5.54" @@ -309,6 +321,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + [[package]] name = "icu_collections" version = "2.1.1" @@ -852,6 +870,18 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "tiny_http" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389915df6413a2e74fb181895f933386023c71110878cd0825588928e64cdc82" +dependencies = [ + "ascii", + "chunked_transfer", + "httpdate", + "log", +] + [[package]] name = "tinystr" version = "0.8.2" @@ -896,6 +926,7 @@ dependencies = [ "serde", "serde_json", "snmp", + "tiny_http", "tokio", "ureq", ] diff --git a/Cargo.toml b/Cargo.toml index 6c6ef8d..8df4e5e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ log = { version = "0.4", features = ["std"] } clap = { version = "4.0", features = ["derive", "env"] } prost = "0.13" prost-types = "0.13" +tiny_http = "0.12" [build-dependencies] prost-build = "0.13" diff --git a/src/health.rs b/src/health.rs new file mode 100644 index 0000000..b332b2c --- /dev/null +++ b/src/health.rs @@ -0,0 +1,130 @@ +use crate::buffer::Storage; +use crate::metrics::Timestamp; +use log::{error, info}; +use serde::{Deserialize, Serialize}; +use std::sync::{Arc, Mutex}; +use std::thread; +use tiny_http::{Response, Server}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct HealthStatus { + pub status: String, + pub version: String, + pub uptime_seconds: u64, + pub config_last_fetch: Option, + pub metrics_pending: usize, + pub last_error: Option, +} + +pub struct HealthServer { + start_time: Timestamp, + storage: Storage, + last_error: Arc>>, + config_last_fetch: Arc>>, +} + +impl HealthServer { + pub fn new(storage: Storage) -> Self { + Self { + start_time: Timestamp::now(), + storage, + last_error: Arc::new(Mutex::new(None)), + config_last_fetch: Arc::new(Mutex::new(None)), + } + } + + pub fn update_config_fetch_time(&self) { + if let Ok(mut last_fetch) = self.config_last_fetch.lock() { + *last_fetch = Some(Timestamp::now()); + } + } + + pub fn record_error(&self, error: String) { + if let Ok(mut last_error) = self.last_error.lock() { + *last_error = Some(error); + } + } + + pub fn start(self, port: u16) { + thread::spawn(move || { + let addr = format!("0.0.0.0:{}", port); + info!("Starting health endpoint on {}", addr); + + let server = match Server::http(&addr) { + Ok(s) => s, + Err(e) => { + error!("Failed to start health server: {}", e); + return; + } + }; + + for request in server.incoming_requests() { + let path = request.url(); + + if path == "/health" { + let status = self.get_health_status(); + let json = match serde_json::to_string(&status) { + Ok(j) => j, + Err(e) => { + error!("Failed to serialize health status: {}", e); + let _ = request.respond( + Response::from_string("Internal error").with_status_code(500), + ); + continue; + } + }; + + let response = Response::from_string(json) + .with_header( + tiny_http::Header::from_bytes( + &b"Content-Type"[..], + &b"application/json"[..], + ) + .unwrap(), + ) + .with_status_code(200); + + let _ = request.respond(response); + } else { + let _ = + request.respond(Response::from_string("Not found").with_status_code(404)); + } + } + }); + } + + fn get_health_status(&self) -> HealthStatus { + let uptime = self.start_time.elapsed_secs() as u64; + + // Get pending metrics count + let metrics_pending = self + .storage + .get_pending_metrics(1000) + .map(|m| m.len()) + .unwrap_or(0); + + // Get last config fetch time + let config_last_fetch = self + .config_last_fetch + .lock() + .ok() + .and_then(|guard| *guard) + .map(|ts| ts.to_rfc3339()); + + // Get last error + let last_error = self + .last_error + .lock() + .ok() + .and_then(|guard| (*guard).clone()); + + HealthStatus { + status: "healthy".to_string(), + version: env!("CARGO_PKG_VERSION").to_string(), + uptime_seconds: uptime, + config_last_fetch, + metrics_pending, + last_error, + } + } +} diff --git a/src/main.rs b/src/main.rs index ebba3ad..b703099 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ mod api_client; mod buffer; mod config; +mod health; mod metrics; mod poller; mod proto; @@ -100,6 +101,10 @@ async fn main() { } }; + // Start health endpoint server + let health_server = health::HealthServer::new(storage.clone()); + health_server.start(8080); + let snmp_client = snmp::SnmpClient::new(); // Create and run scheduler