diff options
| author | Your Name <you@example.com> | 2026-05-26 18:01:30 +0530 |
|---|---|---|
| committer | Your Name <you@example.com> | 2026-05-26 18:01:30 +0530 |
| commit | e435f7d7b4ad4e4b1d3c21c35df5f41ffd642376 (patch) | |
| tree | ccb0587d8aff2d0513f2cef359349b6e0b1b947f | |
| parent | 8816a192c95cf539b65975469a2d61aed46f0414 (diff) | |
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
| -rw-r--r-- | Cargo.toml | 1 | ||||
| -rw-r--r-- | src/config.rs | 6 | ||||
| -rw-r--r-- | src/http_health.rs | 40 | ||||
| -rw-r--r-- | src/main.rs | 34 |
4 files changed, 78 insertions, 3 deletions
| @@ -22,3 +22,4 @@ git2 = "0.20" | |||
| 22 | hex = "0.4" | 22 | hex = "0.4" |
| 23 | clap = { version = "4", features = ["derive", "env"] } | 23 | clap = { version = "4", features = ["derive", "env"] } |
| 24 | dirs = "6" | 24 | dirs = "6" |
| 25 | axum = "0.8" | ||
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 { | |||
| 33 | pub mirror_dir: PathBuf, | 33 | pub mirror_dir: PathBuf, |
| 34 | #[serde(default = "default_database")] | 34 | #[serde(default = "default_database")] |
| 35 | pub database: PathBuf, | 35 | pub database: PathBuf, |
| 36 | #[serde(default = "default_health_port")] | ||
| 37 | pub health_port: u16, | ||
| 38 | } | ||
| 39 | |||
| 40 | fn default_health_port() -> u16 { | ||
| 41 | 7335 | ||
| 36 | } | 42 | } |
| 37 | 43 | ||
| 38 | fn default_mirror_dir() -> PathBuf { | 44 | 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 @@ | |||
| 1 | use axum::extract::State; | ||
| 2 | use axum::response::Json; | ||
| 3 | use axum::routing::get; | ||
| 4 | use axum::Router; | ||
| 5 | use serde_json::{json, Value}; | ||
| 6 | use std::sync::Arc; | ||
| 7 | use std::time::Instant; | ||
| 8 | use tokio::sync::watch; | ||
| 9 | |||
| 10 | pub struct HealthState { | ||
| 11 | pub started_at: Instant, | ||
| 12 | pub cycle_count: watch::Receiver<u64>, | ||
| 13 | pub last_cycle_ok: watch::Receiver<bool>, | ||
| 14 | pub db_path: String, | ||
| 15 | } | ||
| 16 | |||
| 17 | pub async fn start_health_server(port: u16, state: Arc<HealthState>) -> anyhow::Result<()> { | ||
| 18 | let app = Router::new() | ||
| 19 | .route("/health", get(health_handler)) | ||
| 20 | .route("/api/mirror-health", get(health_handler)) | ||
| 21 | .with_state(state); | ||
| 22 | |||
| 23 | let listener = tokio::net::TcpListener::bind(format!("127.0.0.1:{}", port)).await?; | ||
| 24 | tracing::info!(port, "health server listening"); | ||
| 25 | axum::serve(listener, app).await?; | ||
| 26 | Ok(()) | ||
| 27 | } | ||
| 28 | |||
| 29 | async fn health_handler(State(state): State<Arc<HealthState>>) -> Json<Value> { | ||
| 30 | let uptime = state.started_at.elapsed(); | ||
| 31 | let cycle_count = *state.cycle_count.borrow(); | ||
| 32 | let last_ok = *state.last_cycle_ok.borrow(); | ||
| 33 | |||
| 34 | Json(json!({ | ||
| 35 | "status": if last_ok || cycle_count == 0 { "ok" } else { "degraded" }, | ||
| 36 | "uptime_secs": uptime.as_secs(), | ||
| 37 | "cycle_count": cycle_count, | ||
| 38 | "last_cycle_ok": last_ok, | ||
| 39 | })) | ||
| 40 | } | ||
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; | |||
| 3 | mod discovery; | 3 | mod discovery; |
| 4 | mod git_mirror; | 4 | mod git_mirror; |
| 5 | mod health; | 5 | mod health; |
| 6 | mod http_health; | ||
| 6 | mod nostr_mirror; | 7 | mod nostr_mirror; |
| 7 | mod signing; | 8 | mod signing; |
| 8 | 9 | ||
| @@ -70,6 +71,24 @@ async fn run_daemon(config: config::ResolvedConfig, db: db::MirrorDb) -> Result< | |||
| 70 | let db = Arc::new(db); | 71 | let db = Arc::new(db); |
| 71 | let config = Arc::new(config); | 72 | let config = Arc::new(config); |
| 72 | 73 | ||
| 74 | let (cycle_count_tx, cycle_count_rx) = tokio::sync::watch::channel(0u64); | ||
| 75 | let (last_cycle_ok_tx, last_cycle_ok_rx) = tokio::sync::watch::channel(true); | ||
| 76 | |||
| 77 | let health_state = Arc::new(http_health::HealthState { | ||
| 78 | started_at: std::time::Instant::now(), | ||
| 79 | cycle_count: cycle_count_rx, | ||
| 80 | last_cycle_ok: last_cycle_ok_rx, | ||
| 81 | db_path: config.storage.database.display().to_string(), | ||
| 82 | }); | ||
| 83 | |||
| 84 | let health_port = config.storage.health_port; | ||
| 85 | let health_state_clone = health_state.clone(); | ||
| 86 | tokio::spawn(async move { | ||
| 87 | if let Err(e) = http_health::start_health_server(health_port, health_state_clone).await { | ||
| 88 | tracing::error!(error = %e, "health server failed"); | ||
| 89 | } | ||
| 90 | }); | ||
| 91 | |||
| 73 | let servers = health::verify_all_servers(&config.servers.known).await; | 92 | let servers = health::verify_all_servers(&config.servers.known).await; |
| 74 | let healthy: Vec<_> = servers | 93 | let healthy: Vec<_> = servers |
| 75 | .values() | 94 | .values() |
| @@ -101,19 +120,28 @@ async fn run_daemon(config: config::ResolvedConfig, db: db::MirrorDb) -> Result< | |||
| 101 | config.discovery.poll_interval_secs | 120 | config.discovery.poll_interval_secs |
| 102 | ); | 121 | ); |
| 103 | 122 | ||
| 123 | let mut cycle_count: u64 = 0; | ||
| 124 | |||
| 104 | loop { | 125 | loop { |
| 105 | tokio::select! { | 126 | tokio::select! { |
| 106 | _ = interval.tick() => { | 127 | _ = interval.tick() => { |
| 107 | if let Err(e) = mirror_cycle( | 128 | let result = mirror_cycle( |
| 108 | &config, | 129 | &config, |
| 109 | &db, | 130 | &db, |
| 110 | &nostr_client, | 131 | &nostr_client, |
| 111 | &mirror, | 132 | &mirror, |
| 112 | &nostr_mirror, | 133 | &nostr_mirror, |
| 113 | &healthy, | 134 | &healthy, |
| 114 | ).await { | 135 | ).await; |
| 115 | tracing::error!(error = %e, "mirror cycle failed"); | 136 | |
| 137 | match &result { | ||
| 138 | Ok(()) => tracing::info!("mirror cycle complete"), | ||
| 139 | Err(e) => tracing::error!(error = %e, "mirror cycle failed"), | ||
| 116 | } | 140 | } |
| 141 | |||
| 142 | cycle_count += 1; | ||
| 143 | let _ = cycle_count_tx.send(cycle_count); | ||
| 144 | let _ = last_cycle_ok_tx.send(result.is_ok()); | ||
| 117 | } | 145 | } |
| 118 | _ = tokio::signal::ctrl_c() => { | 146 | _ = tokio::signal::ctrl_c() => { |
| 119 | tracing::info!("shutting down"); | 147 | tracing::info!("shutting down"); |