Skip to main content

confium_coordinator/coordinator/
diagnostics.rs

1//! Coordinator diagnostics — self-health report generation.
2
3use chrono::{DateTime, Duration, Utc};
4use serde::{Deserialize, Serialize};
5
6/// A comprehensive diagnostics report.
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct DiagnosticsReport {
9    /// Build version.
10    pub version: String,
11    /// When the coordinator started.
12    pub started_at: DateTime<Utc>,
13    /// When this report was generated.
14    pub generated_at: DateTime<Utc>,
15    /// Uptime in seconds.
16    pub uptime_seconds: i64,
17    /// Active session count.
18    pub active_sessions: usize,
19    /// Total sessions created.
20    pub total_sessions_created: u64,
21    /// Total sessions completed.
22    pub total_sessions_completed: u64,
23    /// Total sessions expired.
24    pub total_sessions_expired: u64,
25    /// Registered signer count.
26    pub registered_signers: usize,
27    /// Total aggregations attempted.
28    pub aggregations_attempted: u64,
29    /// Total aggregations failed.
30    pub aggregations_failed: u64,
31    /// Aggregation success rate (0.0–1.0).
32    pub success_rate: f64,
33    /// Memory usage estimate (bytes).
34    pub memory_usage_bytes: u64,
35    /// Any warnings.
36    pub warnings: Vec<String>,
37}
38
39impl DiagnosticsReport {
40    /// Generate a report from the current coordinator state.
41    #[allow(clippy::too_many_arguments)]
42    pub fn generate(
43        version: &str,
44        started_at: DateTime<Utc>,
45        active_sessions: usize,
46        total_created: u64,
47        total_completed: u64,
48        total_expired: u64,
49        registered_signers: usize,
50        aggregations_attempted: u64,
51        aggregations_failed: u64,
52    ) -> Self {
53        let now = Utc::now();
54        let uptime = now - started_at;
55        let success_rate = if aggregations_attempted > 0 {
56            1.0 - (aggregations_failed as f64 / aggregations_attempted as f64)
57        } else {
58            1.0
59        };
60
61        let mut warnings = Vec::new();
62        if success_rate < 0.95 {
63            warnings.push(format!(
64                "Success rate {:.1}% is below 95%",
65                success_rate * 100.0
66            ));
67        }
68        if active_sessions > 50 {
69            warnings.push(format!("{active_sessions} active sessions (high)"));
70        }
71        if registered_signers == 0 && total_created > 0 {
72            warnings.push("No registered signers but sessions exist".into());
73        }
74        if uptime > Duration::zero() && total_created > 0 {
75            let sessions_per_hour = total_created as f64 / (uptime.num_seconds() as f64 / 3600.0);
76            if sessions_per_hour > 1000.0 {
77                warnings.push(format!(
78                    "{:.0} sessions/hour (high load)",
79                    sessions_per_hour
80                ));
81            }
82        }
83
84        Self {
85            version: version.into(),
86            started_at,
87            generated_at: now,
88            uptime_seconds: uptime.num_seconds(),
89            active_sessions,
90            total_sessions_created: total_created,
91            total_sessions_completed: total_completed,
92            total_sessions_expired: total_expired,
93            registered_signers,
94            aggregations_attempted,
95            aggregations_failed,
96            success_rate,
97            memory_usage_bytes: estimate_memory_usage(active_sessions, registered_signers),
98            warnings,
99        }
100    }
101
102    /// Serialize to JSON.
103    pub fn to_json(&self) -> Result<String, serde_json::Error> {
104        serde_json::to_string_pretty(self)
105    }
106
107    /// Is the coordinator healthy (no warnings)?
108    pub fn is_healthy(&self) -> bool {
109        self.warnings.is_empty() && self.success_rate >= 0.95
110    }
111}
112
113fn estimate_memory_usage(active_sessions: usize, registered_signers: usize) -> u64 {
114    // Rough estimate: each session ~4KB, each signer connection ~2KB
115    (active_sessions as u64 * 4096) + (registered_signers as u64 * 2048) + 1_000_000
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    fn make_report(
123        active: usize,
124        created: u64,
125        completed: u64,
126        signers: usize,
127        agg_attempted: u64,
128        agg_failed: u64,
129    ) -> DiagnosticsReport {
130        DiagnosticsReport::generate(
131            "0.3.0",
132            Utc::now() - Duration::minutes(10),
133            active,
134            created,
135            completed,
136            0,
137            signers,
138            agg_attempted,
139            agg_failed,
140        )
141    }
142
143    #[test]
144    fn healthy_report_has_no_warnings() {
145        let report = make_report(5, 10, 9, 3, 10, 0);
146        assert!(report.warnings.is_empty());
147        assert!(report.is_healthy());
148    }
149
150    #[test]
151    fn low_success_rate_warns() {
152        let report = make_report(5, 10, 5, 3, 10, 2);
153        assert!(report.warnings.iter().any(|w| w.contains("Success rate")));
154    }
155
156    #[test]
157    fn many_sessions_warns() {
158        let report = make_report(60, 100, 40, 5, 100, 5);
159        assert!(
160            report
161                .warnings
162                .iter()
163                .any(|w| w.contains("active sessions"))
164        );
165    }
166
167    #[test]
168    fn no_signers_with_sessions_warns() {
169        let report = make_report(5, 10, 5, 0, 10, 0);
170        assert!(
171            report
172                .warnings
173                .iter()
174                .any(|w| w.contains("No registered signers"))
175        );
176    }
177
178    #[test]
179    fn uptime_positive() {
180        let report = make_report(0, 0, 0, 0, 0, 0);
181        assert!(report.uptime_seconds > 0);
182    }
183
184    #[test]
185    fn success_rate_zero_when_no_aggregations() {
186        let report = make_report(0, 0, 0, 0, 0, 0);
187        assert_eq!(report.success_rate, 1.0);
188    }
189
190    #[test]
191    fn success_rate_computed() {
192        let report = make_report(0, 0, 0, 0, 100, 10);
193        assert!((report.success_rate - 0.9).abs() < 0.001);
194    }
195
196    #[test]
197    fn json_serialization_works() {
198        let report = make_report(1, 1, 1, 1, 1, 0);
199        let json = report.to_json().unwrap();
200        assert!(json.contains("version"));
201        assert!(json.contains("uptime_seconds"));
202    }
203
204    #[test]
205    fn memory_usage_estimated() {
206        let report = make_report(10, 0, 0, 5, 0, 0);
207        assert!(report.memory_usage_bytes > 1_000_000);
208    }
209
210    #[test]
211    fn version_included() {
212        let report = make_report(0, 0, 0, 0, 0, 0);
213        assert_eq!(report.version, "0.3.0");
214    }
215}