confium_coordinator/coordinator/
diagnostics.rs1use chrono::{DateTime, Duration, Utc};
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct DiagnosticsReport {
9 pub version: String,
11 pub started_at: DateTime<Utc>,
13 pub generated_at: DateTime<Utc>,
15 pub uptime_seconds: i64,
17 pub active_sessions: usize,
19 pub total_sessions_created: u64,
21 pub total_sessions_completed: u64,
23 pub total_sessions_expired: u64,
25 pub registered_signers: usize,
27 pub aggregations_attempted: u64,
29 pub aggregations_failed: u64,
31 pub success_rate: f64,
33 pub memory_usage_bytes: u64,
35 pub warnings: Vec<String>,
37}
38
39impl DiagnosticsReport {
40 #[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 pub fn to_json(&self) -> Result<String, serde_json::Error> {
104 serde_json::to_string_pretty(self)
105 }
106
107 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 (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}