upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYour Name <you@example.com>2026-05-26 18:01:30 +0530
committerYour Name <you@example.com>2026-05-26 18:01:30 +0530
commite435f7d7b4ad4e4b1d3c21c35df5f41ffd642376 (patch)
treeccb0587d8aff2d0513f2cef359349b6e0b1b947f
parent8816a192c95cf539b65975469a2d61aed46f0414 (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.toml1
-rw-r--r--src/config.rs6
-rw-r--r--src/http_health.rs40
-rw-r--r--src/main.rs34
4 files changed, 78 insertions, 3 deletions
diff --git a/Cargo.toml b/Cargo.toml
index a093dbb..2c6be46 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -22,3 +22,4 @@ git2 = "0.20"
22hex = "0.4" 22hex = "0.4"
23clap = { version = "4", features = ["derive", "env"] } 23clap = { version = "4", features = ["derive", "env"] }
24dirs = "6" 24dirs = "6"
25axum = "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
40fn default_health_port() -> u16 {
41 7335
36} 42}
37 43
38fn default_mirror_dir() -> PathBuf { 44fn 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 @@
1use axum::extract::State;
2use axum::response::Json;
3use axum::routing::get;
4use axum::Router;
5use serde_json::{json, Value};
6use std::sync::Arc;
7use std::time::Instant;
8use tokio::sync::watch;
9
10pub 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
17pub 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
29async 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;
3mod discovery; 3mod discovery;
4mod git_mirror; 4mod git_mirror;
5mod health; 5mod health;
6mod http_health;
6mod nostr_mirror; 7mod nostr_mirror;
7mod signing; 8mod 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");