use std::time::Duration;
pub const WORKER_SERVICE: &str = "zakuro_worker";
const DEFAULT_S6_URL: &str = "http://127.0.0.1:8080";
const TIMEOUT: Duration = Duration::from_millis(500);
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorkerHealth {
pub status: String,
pub restart_count: Option<i64>,
pub uptime: Option<String>,
}
pub fn parse_details(body: &str) -> Option<WorkerHealth> {
let v: serde_json::Value = serde_json::from_str(body).ok()?;
let status = v.get("status")?.as_str()?.to_string();
if status.is_empty() {
return None;
}
Some(WorkerHealth {
status,
restart_count: v.get("restart_count").and_then(|r| {
r.as_i64()
.or_else(|| r.as_str().and_then(|s| s.parse().ok()))
}),
uptime: v
.get("uptime")
.and_then(|u| u.as_str())
.filter(|s| !s.is_empty())
.map(str::to_string),
})
}
fn s6_base() -> String {
std::env::var("ZAKURO_S6_URL").unwrap_or_else(|_| DEFAULT_S6_URL.to_string())
}
pub fn worker_health() -> Option<WorkerHealth> {
let url = format!(
"{}/api/services/{}/details",
s6_base().trim_end_matches('/'),
WORKER_SERVICE
);
let agent = ureq::Agent::new_with_config(
ureq::Agent::config_builder()
.timeout_connect(Some(TIMEOUT))
.timeout_recv_response(Some(TIMEOUT))
.build(),
);
let body = agent
.get(&url)
.call()
.ok()?
.body_mut()
.read_to_string()
.ok()?;
parse_details(&body)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_a_healthy_service() {
let h = parse_details(
r#"{"name":"zakuro_worker","status":"running","uptime":"120s",
"pid":"52","restart_count":0}"#,
)
.unwrap();
assert_eq!(h.status, "running");
assert_eq!(h.restart_count, Some(0));
assert_eq!(h.uptime.as_deref(), Some("120s"));
}
#[test]
fn parses_a_flapping_service() {
let h = parse_details(r#"{"status":"down","restart_count":17}"#).unwrap();
assert_eq!(h.status, "down");
assert_eq!(h.restart_count, Some(17));
}
#[test]
fn accepts_a_numeric_string_restart_count() {
let h = parse_details(r#"{"status":"running","restart_count":"3"}"#).unwrap();
assert_eq!(h.restart_count, Some(3));
}
#[test]
fn missing_optional_fields_are_none_not_zero() {
let h = parse_details(r#"{"status":"running"}"#).unwrap();
assert_eq!(h.restart_count, None);
assert_eq!(h.uptime, None);
}
#[test]
fn no_status_is_no_health() {
assert!(parse_details(r#"{"uptime":"5s"}"#).is_none());
assert!(parse_details(r#"{"status":""}"#).is_none());
}
#[test]
fn junk_does_not_panic_or_guess() {
for body in ["", "not json", "[]", "null", r#"{"status":42}"#] {
assert!(parse_details(body).is_none(), "body: {body:?}");
}
}
#[test]
fn nothing_but_the_three_fields_is_carried() {
let h = parse_details(
r##"{"status":"running","run_script":"#!/bin/sh\nexec secret",
"stdout":"KEY=hunter2","restart_count":1}"##,
)
.unwrap();
assert_eq!(h.status, "running");
assert_eq!(h.restart_count, Some(1));
assert_eq!(h.uptime, None);
}
}