use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct Health {
pub connected: bool,
pub last_message_ms: Option<i64>,
pub clock_offset_ms: i64,
pub rate_budget_used: u32,
pub reconnects: u64,
}
impl Health {
#[must_use]
pub fn staleness_ms(&self, now_ms: i64) -> Option<i64> {
self.last_message_ms.map(|last| now_ms - last)
}
#[must_use]
pub fn is_healthy(&self, now_ms: i64, max_staleness_ms: i64) -> bool {
self.connected
&& self
.staleness_ms(now_ms)
.is_some_and(|s| s <= max_staleness_ms)
}
}
#[must_use]
pub fn redact(text: &str, secret: &str) -> String {
if secret.is_empty() {
return text.to_string();
}
text.replace(secret, "<redacted>")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn staleness_and_health() {
let mut health = Health {
connected: true,
last_message_ms: Some(1_000),
..Health::default()
};
assert_eq!(health.staleness_ms(1_500), Some(500));
assert!(health.is_healthy(1_500, 1_000));
assert!(!health.is_healthy(5_000, 1_000));
health.connected = false;
assert!(!health.is_healthy(1_100, 1_000)); }
#[test]
fn health_with_no_messages_is_never_healthy() {
let health = Health {
connected: true,
last_message_ms: None,
..Health::default()
};
assert_eq!(health.staleness_ms(1_000), None);
assert!(!health.is_healthy(1_000, 10_000));
}
#[test]
fn redaction_removes_secrets_and_ignores_empty() {
let line = "auth header X-KEY=SECRET123 sent";
assert_eq!(
redact(line, "SECRET123"),
"auth header X-KEY=<redacted> sent"
);
assert_eq!(
redact("aSECRETbSECRETc", "SECRET"),
"a<redacted>b<redacted>c"
);
assert_eq!(redact(line, ""), line);
}
}