Add health endpoint for agent monitoring
Added /health HTTP endpoint on port 8080 that returns:
- Agent status and version
- Uptime in seconds
- Pending metrics count
- Last error (if any)
Implementation:
- Uses lightweight tiny_http server in background thread
- Non-blocking health checks
- Returns JSON for easy integration with monitoring tools
- Ready for Kubernetes liveness/readiness probes
Example response:
{
"status": "healthy",
"version": "0.1.0",
"uptime_seconds": 3600,
"config_last_fetch": "2026-01-14T23:00:00Z",
"metrics_pending": 0,
"last_error": null
}
This commit is contained in:
parent
824f4388eb
commit
f7ac5f48e8
4 changed files with 167 additions and 0 deletions
31
Cargo.lock
generated
31
Cargo.lock
generated
|
|
@ -85,6 +85,12 @@ version = "1.0.100"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ascii"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "base64"
|
name = "base64"
|
||||||
version = "0.22.1"
|
version = "0.22.1"
|
||||||
|
|
@ -119,6 +125,12 @@ version = "1.0.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chunked_transfer"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6e4de3bc4ea267985becf712dc6d9eed8b04c953b3fcfb339ebc87acd9804901"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap"
|
name = "clap"
|
||||||
version = "4.5.54"
|
version = "4.5.54"
|
||||||
|
|
@ -309,6 +321,12 @@ version = "0.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "httpdate"
|
||||||
|
version = "1.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icu_collections"
|
name = "icu_collections"
|
||||||
version = "2.1.1"
|
version = "2.1.1"
|
||||||
|
|
@ -852,6 +870,18 @@ dependencies = [
|
||||||
"windows-sys 0.61.2",
|
"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]]
|
[[package]]
|
||||||
name = "tinystr"
|
name = "tinystr"
|
||||||
version = "0.8.2"
|
version = "0.8.2"
|
||||||
|
|
@ -896,6 +926,7 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"snmp",
|
"snmp",
|
||||||
|
"tiny_http",
|
||||||
"tokio",
|
"tokio",
|
||||||
"ureq",
|
"ureq",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ log = { version = "0.4", features = ["std"] }
|
||||||
clap = { version = "4.0", features = ["derive", "env"] }
|
clap = { version = "4.0", features = ["derive", "env"] }
|
||||||
prost = "0.13"
|
prost = "0.13"
|
||||||
prost-types = "0.13"
|
prost-types = "0.13"
|
||||||
|
tiny_http = "0.12"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
prost-build = "0.13"
|
prost-build = "0.13"
|
||||||
|
|
|
||||||
130
src/health.rs
Normal file
130
src/health.rs
Normal file
|
|
@ -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<String>,
|
||||||
|
pub metrics_pending: usize,
|
||||||
|
pub last_error: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct HealthServer {
|
||||||
|
start_time: Timestamp,
|
||||||
|
storage: Storage,
|
||||||
|
last_error: Arc<Mutex<Option<String>>>,
|
||||||
|
config_last_fetch: Arc<Mutex<Option<Timestamp>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
mod api_client;
|
mod api_client;
|
||||||
mod buffer;
|
mod buffer;
|
||||||
mod config;
|
mod config;
|
||||||
|
mod health;
|
||||||
mod metrics;
|
mod metrics;
|
||||||
mod poller;
|
mod poller;
|
||||||
mod proto;
|
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();
|
let snmp_client = snmp::SnmpClient::new();
|
||||||
|
|
||||||
// Create and run scheduler
|
// Create and run scheduler
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue