From e435f7d7b4ad4e4b1d3c21c35df5f41ffd642376 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 26 May 2026 18:01:30 +0530 Subject: Add HTTP health endpoint on /health and /api/mirror-health - New axum-based health server on port 7335 (configurable via health_port) - Reports status, uptime, cycle_count, last_cycle_ok as JSON - Status is 'ok' on startup and after successful cycles, 'degraded' after failures - Config: storage.health_port defaults to 7335 - Spawned alongside daemon loop, independent of mirror cycles --- src/config.rs | 6 ++++++ src/http_health.rs | 40 ++++++++++++++++++++++++++++++++++++++++ src/main.rs | 34 +++++++++++++++++++++++++++++++--- 3 files changed, 77 insertions(+), 3 deletions(-) create mode 100644 src/http_health.rs (limited to 'src') diff --git a/src/config.rs b/src/config.rs index ceff44d..037deb2 100644 --- a/src/config.rs +++ b/src/config.rs @@ -33,6 +33,12 @@ pub struct StorageConfig { pub mirror_dir: PathBuf, #[serde(default = "default_database")] pub database: PathBuf, + #[serde(default = "default_health_port")] + pub health_port: u16, +} + +fn default_health_port() -> u16 { + 7335 } fn default_mirror_dir() -> PathBuf { diff --git a/src/http_health.rs b/src/http_health.rs new file mode 100644 index 0000000..0cdfeb5 --- /dev/null +++ b/src/http_health.rs @@ -0,0 +1,40 @@ +use axum::extract::State; +use axum::response::Json; +use axum::routing::get; +use axum::Router; +use serde_json::{json, Value}; +use std::sync::Arc; +use std::time::Instant; +use tokio::sync::watch; + +pub struct HealthState { + pub started_at: Instant, + pub cycle_count: watch::Receiver, + pub last_cycle_ok: watch::Receiver, + pub db_path: String, +} + +pub async fn start_health_server(port: u16, state: Arc) -> anyhow::Result<()> { + let app = Router::new() + .route("/health", get(health_handler)) + .route("/api/mirror-health", get(health_handler)) + .with_state(state); + + let listener = tokio::net::TcpListener::bind(format!("127.0.0.1:{}", port)).await?; + tracing::info!(port, "health server listening"); + axum::serve(listener, app).await?; + Ok(()) +} + +async fn health_handler(State(state): State>) -> Json { + let uptime = state.started_at.elapsed(); + let cycle_count = *state.cycle_count.borrow(); + let last_ok = *state.last_cycle_ok.borrow(); + + Json(json!({ + "status": if last_ok || cycle_count == 0 { "ok" } else { "degraded" }, + "uptime_secs": uptime.as_secs(), + "cycle_count": cycle_count, + "last_cycle_ok": last_ok, + })) +} diff --git a/src/main.rs b/src/main.rs index b709d44..494342c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ mod db; mod discovery; mod git_mirror; mod health; +mod http_health; mod nostr_mirror; mod signing; @@ -70,6 +71,24 @@ async fn run_daemon(config: config::ResolvedConfig, db: db::MirrorDb) -> Result< let db = Arc::new(db); let config = Arc::new(config); + let (cycle_count_tx, cycle_count_rx) = tokio::sync::watch::channel(0u64); + let (last_cycle_ok_tx, last_cycle_ok_rx) = tokio::sync::watch::channel(true); + + let health_state = Arc::new(http_health::HealthState { + started_at: std::time::Instant::now(), + cycle_count: cycle_count_rx, + last_cycle_ok: last_cycle_ok_rx, + db_path: config.storage.database.display().to_string(), + }); + + let health_port = config.storage.health_port; + let health_state_clone = health_state.clone(); + tokio::spawn(async move { + if let Err(e) = http_health::start_health_server(health_port, health_state_clone).await { + tracing::error!(error = %e, "health server failed"); + } + }); + let servers = health::verify_all_servers(&config.servers.known).await; let healthy: Vec<_> = servers .values() @@ -101,19 +120,28 @@ async fn run_daemon(config: config::ResolvedConfig, db: db::MirrorDb) -> Result< config.discovery.poll_interval_secs ); + let mut cycle_count: u64 = 0; + loop { tokio::select! { _ = interval.tick() => { - if let Err(e) = mirror_cycle( + let result = mirror_cycle( &config, &db, &nostr_client, &mirror, &nostr_mirror, &healthy, - ).await { - tracing::error!(error = %e, "mirror cycle failed"); + ).await; + + match &result { + Ok(()) => tracing::info!("mirror cycle complete"), + Err(e) => tracing::error!(error = %e, "mirror cycle failed"), } + + cycle_count += 1; + let _ = cycle_count_tx.send(cycle_count); + let _ = last_cycle_ok_tx.send(result.is_ok()); } _ = tokio::signal::ctrl_c() => { tracing::info!("shutting down"); -- cgit v1.2.3