wickra_exchange_core/
observability.rs1use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
12pub struct Health {
13 pub connected: bool,
15 pub last_message_ms: Option<i64>,
17 pub clock_offset_ms: i64,
19 pub rate_budget_used: u32,
21 pub reconnects: u64,
23}
24
25impl Health {
26 #[must_use]
29 pub fn staleness_ms(&self, now_ms: i64) -> Option<i64> {
30 self.last_message_ms.map(|last| now_ms - last)
31 }
32
33 #[must_use]
36 pub fn is_healthy(&self, now_ms: i64, max_staleness_ms: i64) -> bool {
37 self.connected
38 && self
39 .staleness_ms(now_ms)
40 .is_some_and(|s| s <= max_staleness_ms)
41 }
42}
43
44#[must_use]
48pub fn redact(text: &str, secret: &str) -> String {
49 if secret.is_empty() {
50 return text.to_string();
51 }
52 text.replace(secret, "<redacted>")
53}
54
55#[cfg(test)]
56mod tests {
57 use super::*;
58
59 #[test]
60 fn staleness_and_health() {
61 let mut health = Health {
62 connected: true,
63 last_message_ms: Some(1_000),
64 ..Health::default()
65 };
66 assert_eq!(health.staleness_ms(1_500), Some(500));
67 assert!(health.is_healthy(1_500, 1_000));
68 assert!(!health.is_healthy(5_000, 1_000)); health.connected = false;
71 assert!(!health.is_healthy(1_100, 1_000)); }
73
74 #[test]
75 fn health_with_no_messages_is_never_healthy() {
76 let health = Health {
77 connected: true,
78 last_message_ms: None,
79 ..Health::default()
80 };
81 assert_eq!(health.staleness_ms(1_000), None);
82 assert!(!health.is_healthy(1_000, 10_000));
83 }
84
85 #[test]
86 fn redaction_removes_secrets_and_ignores_empty() {
87 let line = "auth header X-KEY=SECRET123 sent";
88 assert_eq!(
89 redact(line, "SECRET123"),
90 "auth header X-KEY=<redacted> sent"
91 );
92 assert_eq!(
94 redact("aSECRETbSECRETc", "SECRET"),
95 "a<redacted>b<redacted>c"
96 );
97 assert_eq!(redact(line, ""), line);
99 }
100}