synops-health: sjekk status for alle tjenester i stacken (oppgave 28.7)
CLI-verktøy som sjekker PG, Caddy, Maskinrommet, LiteLLM, Whisper, LiveKit og Authentik parallelt. Output: JSON med status per tjeneste + samlet helsetilstand (healthy/degraded). Exit-kode 0 = alt oppe. Gjenbruker samme mønster som maskinrommet/src/health.rs men som frittstående verktøy — kan brukes av admin-dashboard, overvåking, og direkte fra CLI.
This commit is contained in:
parent
8f12b08c25
commit
d3ecb5b279
4 changed files with 217 additions and 2 deletions
3
tasks.md
3
tasks.md
|
|
@ -381,8 +381,7 @@ modell som brukes til hva.
|
||||||
- [x] 28.4 `synops-notify`: send varsel via epost (synops-mail), WebSocket-push, eller begge. Input: `--to <node_id> --message <tekst> [--channel email|ws|both]`. Brukes av orkestreringer og vaktmesteren.
|
- [x] 28.4 `synops-notify`: send varsel via epost (synops-mail), WebSocket-push, eller begge. Input: `--to <node_id> --message <tekst> [--channel email|ws|both]`. Brukes av orkestreringer og vaktmesteren.
|
||||||
- [x] 28.5 `synops-validate`: sjekk at en node matcher forventet skjema for sin node_kind. Input: `--node-id <uuid>`. Output: liste av avvik. Brukes av valideringsfasen og som pre-commit sjekk.
|
- [x] 28.5 `synops-validate`: sjekk at en node matcher forventet skjema for sin node_kind. Input: `--node-id <uuid>`. Output: liste av avvik. Brukes av valideringsfasen og som pre-commit sjekk.
|
||||||
- [x] 28.6 `synops-backup`: PG-dump + CAS-filiste + metadata-snapshot. Input: `[--full | --incremental]`. Output: backup-sti. Erstatter cron-scriptet fra 12.2.
|
- [x] 28.6 `synops-backup`: PG-dump + CAS-filiste + metadata-snapshot. Input: `[--full | --incremental]`. Output: backup-sti. Erstatter cron-scriptet fra 12.2.
|
||||||
- [~] 28.7 `synops-health`: sjekk status for alle tjenester (PG, Caddy, vaktmesteren, LiteLLM, Whisper, LiveKit). Output: JSON med status per tjeneste. Brukes av admin-dashboard og overvåking.
|
- [x] 28.7 `synops-health`: sjekk status for alle tjenester (PG, Caddy, vaktmesteren, LiteLLM, Whisper, LiveKit). Output: JSON med status per tjeneste. Brukes av admin-dashboard og overvåking.
|
||||||
> Påbegynt: 2026-03-18T20:50
|
|
||||||
|
|
||||||
## Fase 29: Universell input — alle modaliteter blir noder
|
## Fase 29: Universell input — alle modaliteter blir noder
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ eller maskinrommet-API. Ligger i PATH via symlink eller direkte kall.
|
||||||
| `synops-notify` | Send varsel via epost, WebSocket-push, eller begge | Ferdig |
|
| `synops-notify` | Send varsel via epost, WebSocket-push, eller begge | Ferdig |
|
||||||
| `synops-validate` | Valider at en node matcher forventet skjema for sin node_kind | Ferdig |
|
| `synops-validate` | Valider at en node matcher forventet skjema for sin node_kind | Ferdig |
|
||||||
| `synops-backup` | PG-dump + CAS-filiste + metadata-snapshot (`--full` / `--incremental`) | Ferdig |
|
| `synops-backup` | PG-dump + CAS-filiste + metadata-snapshot (`--full` / `--incremental`) | Ferdig |
|
||||||
|
| `synops-health` | Sjekk status for alle tjenester (PG, Caddy, Maskinrommet, LiteLLM, Whisper, LiveKit, Authentik) | Ferdig |
|
||||||
|
|
||||||
## Delt bibliotek
|
## Delt bibliotek
|
||||||
|
|
||||||
|
|
|
||||||
20
tools/synops-health/Cargo.toml
Normal file
20
tools/synops-health/Cargo.toml
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
[package]
|
||||||
|
name = "synops-health"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "synops-health"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
clap = { version = "4", features = ["derive"] }
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
sqlx = { version = "0.8", features = ["runtime-tokio", "tls-rustls", "postgres", "uuid", "chrono", "json"] }
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
|
tracing = "0.1"
|
||||||
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
|
reqwest = { version = "0.12", features = ["json"] }
|
||||||
|
synops-common = { path = "../synops-common" }
|
||||||
195
tools/synops-health/src/main.rs
Normal file
195
tools/synops-health/src/main.rs
Normal file
|
|
@ -0,0 +1,195 @@
|
||||||
|
// synops-health — Sjekk status for alle tjenester i Synops-stacken.
|
||||||
|
//
|
||||||
|
// Sjekker: PostgreSQL, Caddy, Maskinrommet, LiteLLM, Whisper, LiveKit, Authentik.
|
||||||
|
// Output: JSON med status per tjeneste + samlet status.
|
||||||
|
// Exit-kode: 0 = alle oppe, 1 = noe nede/degradert, 2 = kjørefeil.
|
||||||
|
//
|
||||||
|
// Brukes av admin-dashboard og overvåking.
|
||||||
|
//
|
||||||
|
// Miljøvariabler (alle valgfrie — har fornuftige defaults):
|
||||||
|
// DATABASE_URL — PostgreSQL-tilkobling
|
||||||
|
// AI_GATEWAY_URL — LiteLLM base-URL (default: http://localhost:4000)
|
||||||
|
// WHISPER_URL — faster-whisper base-URL (default: http://localhost:8000)
|
||||||
|
// LIVEKIT_URL — LiveKit base-URL (default: http://localhost:7880)
|
||||||
|
//
|
||||||
|
// Ref: docs/infra/observerbarhet.md, maskinrommet/src/health.rs
|
||||||
|
|
||||||
|
use clap::Parser;
|
||||||
|
use serde::Serialize;
|
||||||
|
use std::process;
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
/// Sjekk status for alle Synops-tjenester.
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[command(name = "synops-health", about = "Sjekk tjenestestatus for Synops-stacken")]
|
||||||
|
struct Cli {
|
||||||
|
/// Timeout per tjeneste i sekunder
|
||||||
|
#[arg(long, default_value = "5")]
|
||||||
|
timeout: u64,
|
||||||
|
|
||||||
|
/// Kjør kun stille — bare exit-kode, ingen output
|
||||||
|
#[arg(long)]
|
||||||
|
quiet: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct HealthReport {
|
||||||
|
timestamp: String,
|
||||||
|
overall_status: String, // "healthy", "degraded", "critical"
|
||||||
|
services: Vec<ServiceStatus>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Clone)]
|
||||||
|
struct ServiceStatus {
|
||||||
|
name: String,
|
||||||
|
status: String, // "up", "down", "degraded"
|
||||||
|
latency_ms: Option<u64>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
details: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
synops_common::logging::init("synops_health");
|
||||||
|
|
||||||
|
let cli = Cli::parse();
|
||||||
|
let timeout = std::time::Duration::from_secs(cli.timeout);
|
||||||
|
|
||||||
|
let http = reqwest::Client::builder()
|
||||||
|
.timeout(timeout)
|
||||||
|
.danger_accept_invalid_certs(true) // Lokale tjenester kan ha self-signed
|
||||||
|
.build()
|
||||||
|
.expect("Kunne ikke opprette HTTP-klient");
|
||||||
|
|
||||||
|
// Kjør alle sjekker parallelt
|
||||||
|
let (pg, caddy, maskinrommet, litellm, whisper, livekit, authentik) = tokio::join!(
|
||||||
|
check_pg(timeout),
|
||||||
|
check_http(&http, "Caddy", "http://localhost:2019/config/"),
|
||||||
|
check_http(&http, "Maskinrommet", "http://localhost:3100/health"),
|
||||||
|
check_litellm(&http),
|
||||||
|
check_whisper(&http),
|
||||||
|
check_livekit(&http),
|
||||||
|
check_http(&http, "Authentik", "https://auth.sidelinja.org/-/health/ready/"),
|
||||||
|
);
|
||||||
|
|
||||||
|
let services = vec![pg, caddy, maskinrommet, litellm, whisper, livekit, authentik];
|
||||||
|
|
||||||
|
let overall_status = if services.iter().all(|s| s.status == "up") {
|
||||||
|
"healthy"
|
||||||
|
} else if services.iter().any(|s| s.status == "down") {
|
||||||
|
"degraded"
|
||||||
|
} else {
|
||||||
|
"degraded"
|
||||||
|
}
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let report = HealthReport {
|
||||||
|
timestamp: chrono::Utc::now().to_rfc3339(),
|
||||||
|
overall_status: overall_status.clone(),
|
||||||
|
services,
|
||||||
|
};
|
||||||
|
|
||||||
|
if !cli.quiet {
|
||||||
|
println!(
|
||||||
|
"{}",
|
||||||
|
serde_json::to_string_pretty(&report).expect("JSON-serialisering feilet")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let exit_code = if overall_status == "healthy" { 0 } else { 1 };
|
||||||
|
process::exit(exit_code);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Tjeneste-sjekker
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/// Sjekk PostgreSQL med direkte SQL-spørring.
|
||||||
|
async fn check_pg(timeout: std::time::Duration) -> ServiceStatus {
|
||||||
|
let name = "PostgreSQL".to_string();
|
||||||
|
|
||||||
|
// Prøv å koble til med timeout
|
||||||
|
let connect_result = tokio::time::timeout(timeout, synops_common::db::connect()).await;
|
||||||
|
|
||||||
|
match connect_result {
|
||||||
|
Ok(Ok(db)) => {
|
||||||
|
let start = Instant::now();
|
||||||
|
match sqlx::query_scalar::<_, i32>("SELECT 1").fetch_one(&db).await {
|
||||||
|
Ok(_) => ServiceStatus {
|
||||||
|
name,
|
||||||
|
status: "up".to_string(),
|
||||||
|
latency_ms: Some(start.elapsed().as_millis() as u64),
|
||||||
|
details: None,
|
||||||
|
},
|
||||||
|
Err(e) => ServiceStatus {
|
||||||
|
name,
|
||||||
|
status: "down".to_string(),
|
||||||
|
latency_ms: None,
|
||||||
|
details: Some(format!("{e}")),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Err(e)) => ServiceStatus {
|
||||||
|
name,
|
||||||
|
status: "down".to_string(),
|
||||||
|
latency_ms: None,
|
||||||
|
details: Some(format!("Tilkobling feilet: {e}")),
|
||||||
|
},
|
||||||
|
Err(_) => ServiceStatus {
|
||||||
|
name,
|
||||||
|
status: "down".to_string(),
|
||||||
|
latency_ms: None,
|
||||||
|
details: Some("Timeout".to_string()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sjekk en HTTP-tjeneste. Aksepterer 401/403 som "up" (auth kreves men tjenesten kjører).
|
||||||
|
async fn check_http(client: &reqwest::Client, name: &str, url: &str) -> ServiceStatus {
|
||||||
|
let start = Instant::now();
|
||||||
|
match client.get(url).send().await {
|
||||||
|
Ok(resp) => {
|
||||||
|
let latency = start.elapsed().as_millis() as u64;
|
||||||
|
let code = resp.status();
|
||||||
|
if code.is_success() || code.as_u16() == 401 || code.as_u16() == 403 {
|
||||||
|
ServiceStatus {
|
||||||
|
name: name.to_string(),
|
||||||
|
status: "up".to_string(),
|
||||||
|
latency_ms: Some(latency),
|
||||||
|
details: None,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ServiceStatus {
|
||||||
|
name: name.to_string(),
|
||||||
|
status: "degraded".to_string(),
|
||||||
|
latency_ms: Some(latency),
|
||||||
|
details: Some(format!("HTTP {code}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => ServiceStatus {
|
||||||
|
name: name.to_string(),
|
||||||
|
status: "down".to_string(),
|
||||||
|
latency_ms: None,
|
||||||
|
details: Some(format!("{e}")),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn check_litellm(client: &reqwest::Client) -> ServiceStatus {
|
||||||
|
let url = std::env::var("AI_GATEWAY_URL")
|
||||||
|
.unwrap_or_else(|_| "http://localhost:4000".to_string());
|
||||||
|
check_http(client, "LiteLLM", &format!("{url}/health")).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn check_whisper(client: &reqwest::Client) -> ServiceStatus {
|
||||||
|
let url = std::env::var("WHISPER_URL")
|
||||||
|
.unwrap_or_else(|_| "http://localhost:8000".to_string());
|
||||||
|
check_http(client, "Whisper", &format!("{url}/health")).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn check_livekit(client: &reqwest::Client) -> ServiceStatus {
|
||||||
|
let url = std::env::var("LIVEKIT_URL")
|
||||||
|
.unwrap_or_else(|_| "http://localhost:7880".to_string());
|
||||||
|
check_http(client, "LiveKit", &url).await
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue