1use anyhow::Result;
8
9#[derive(Debug, serde::Serialize)]
15pub struct DiagReport {
16 pub daemon_running: bool,
18 pub peer_id: Option<String>,
20 pub connected_peers: Option<usize>,
22 pub storage_blocks: Option<u64>,
24 pub storage_bytes: Option<u64>,
26 pub memory_bytes: Option<u64>,
28 pub avg_inference_ms: Option<f64>,
30 pub cache_hit_rate: Option<f64>,
32 pub tensorlogic_rules: Option<usize>,
34 pub tensorlogic_facts: Option<usize>,
36 pub hnsw_vectors: Option<u64>,
38 pub uptime_secs: Option<u64>,
40}
41
42impl DiagReport {
43 pub fn offline() -> Self {
47 Self {
48 daemon_running: false,
49 peer_id: None,
50 connected_peers: None,
51 storage_blocks: None,
52 storage_bytes: None,
53 memory_bytes: None,
54 avg_inference_ms: None,
55 cache_hit_rate: None,
56 tensorlogic_rules: None,
57 tensorlogic_facts: None,
58 hnsw_vectors: None,
59 uptime_secs: None,
60 }
61 }
62}
63
64fn fmt_bytes(bytes: u64) -> String {
70 const KB: u64 = 1_024;
71 const MB: u64 = KB * 1_024;
72 const GB: u64 = MB * 1_024;
73
74 if bytes >= GB {
75 format!("{:.1} GB", bytes as f64 / GB as f64)
76 } else if bytes >= MB {
77 format!("{:.1} MB", bytes as f64 / MB as f64)
78 } else if bytes >= KB {
79 format!("{:.1} KB", bytes as f64 / KB as f64)
80 } else {
81 format!("{bytes} B")
82 }
83}
84
85fn fmt_uptime(secs: u64) -> String {
87 let h = secs / 3600;
88 let m = (secs % 3600) / 60;
89 let s = secs % 60;
90 if h > 0 {
91 format!("{h}h {m}m")
92 } else if m > 0 {
93 format!("{m}m {s}s")
94 } else {
95 format!("{s}s")
96 }
97}
98
99fn fmt_count(n: u64) -> String {
101 let s = n.to_string();
102 let mut out = String::with_capacity(s.len() + s.len() / 3);
103 for (i, ch) in s.chars().rev().enumerate() {
104 if i > 0 && i % 3 == 0 {
105 out.push(',');
106 }
107 out.push(ch);
108 }
109 out.chars().rev().collect()
110}
111
112fn print_text_report(report: &DiagReport) {
114 println!("=== IPFRS Node Diagnostics ===");
115
116 let daemon_str = if report.daemon_running {
117 "running"
118 } else {
119 "not running"
120 };
121 println!("{:<20} {}", "Daemon:", daemon_str);
122
123 if let Some(ref pid) = report.peer_id {
124 println!("{:<20} {}", "Peer ID:", pid);
125 }
126
127 if let Some(peers) = report.connected_peers {
128 println!("{:<20} {}", "Connected peers:", peers);
129 }
130
131 if let Some(blocks) = report.storage_blocks {
132 println!("{:<20} {}", "Storage blocks:", fmt_count(blocks));
133 }
134
135 if let Some(bytes) = report.storage_bytes {
136 println!("{:<20} {}", "Storage bytes:", fmt_bytes(bytes));
137 }
138
139 if let Some(mem) = report.memory_bytes {
140 println!("{:<20} {}", "Memory:", fmt_bytes(mem));
141 }
142
143 if let Some(ms) = report.avg_inference_ms {
144 println!("{:<20} {:.1} ms", "Avg inference:", ms);
145 }
146
147 if let Some(rate) = report.cache_hit_rate {
148 println!("{:<20} {:.1}%", "Cache hit rate:", rate * 100.0);
149 }
150
151 if let (Some(rules), Some(facts)) = (report.tensorlogic_rules, report.tensorlogic_facts) {
152 println!("{:<20} {} rules, {} facts", "TensorLogic:", rules, facts);
153 }
154
155 if let Some(vecs) = report.hnsw_vectors {
156 println!("{:<20} {}", "HNSW vectors:", fmt_count(vecs));
157 }
158
159 if let Some(secs) = report.uptime_secs {
160 println!("{:<20} {}", "Uptime:", fmt_uptime(secs));
161 }
162}
163
164pub async fn handle_diag(json_output: bool) -> Result<()> {
179 let report = match try_collect_diagnostics().await {
181 Ok(r) => r,
182 Err(_) => {
183 if !json_output {
184 eprintln!("Daemon not running");
185 }
186 DiagReport::offline()
187 }
188 };
189
190 if json_output {
191 let json = serde_json::to_string_pretty(&report)?;
192 println!("{json}");
193 } else {
194 print_text_report(&report);
195 }
196
197 Ok(())
198}
199
200async fn try_collect_diagnostics() -> Result<DiagReport> {
202 use ipfrs::{Node, NodeConfig};
203
204 let mut node = Node::new(NodeConfig::default())?;
205 node.start().await?;
206
207 let diag = node.diagnostics().await?;
208 let peer_id = node.peer_id().ok();
209
210 node.stop().await?;
211
212 let storage_blocks = Some(diag.storage.total_blocks);
213 let storage_bytes = Some(diag.storage.total_bytes);
214 let memory_bytes = Some(diag.resources.memory_bytes);
215 let uptime_secs = Some(diag.uptime.as_secs());
216
217 let (avg_inference_ms, tensorlogic_rules, tensorlogic_facts) =
218 if let Some(ref tl) = diag.tensorlogic {
219 (tl.avg_inference_ms, Some(tl.num_rules), Some(tl.num_facts))
220 } else {
221 (None, None, None)
222 };
223
224 let (cache_hit_rate, hnsw_vectors) = if let Some(ref sem) = diag.semantic {
225 (sem.cache_hit_rate, Some(sem.num_vectors as u64))
226 } else {
227 (None, None)
228 };
229
230 let connected_peers = diag.network.as_ref().map(|n| n.connected_peers);
231
232 Ok(DiagReport {
233 daemon_running: true,
234 peer_id,
235 connected_peers,
236 storage_blocks,
237 storage_bytes,
238 memory_bytes,
239 avg_inference_ms,
240 cache_hit_rate,
241 tensorlogic_rules,
242 tensorlogic_facts,
243 hnsw_vectors,
244 uptime_secs,
245 })
246}
247
248#[cfg(test)]
253mod tests {
254 use super::*;
255
256 #[test]
257 fn test_offline_report_all_none() {
258 let r = DiagReport::offline();
259 assert!(!r.daemon_running);
260 assert!(r.peer_id.is_none());
261 assert!(r.connected_peers.is_none());
262 assert!(r.storage_blocks.is_none());
263 assert!(r.storage_bytes.is_none());
264 assert!(r.memory_bytes.is_none());
265 assert!(r.avg_inference_ms.is_none());
266 assert!(r.cache_hit_rate.is_none());
267 assert!(r.tensorlogic_rules.is_none());
268 assert!(r.tensorlogic_facts.is_none());
269 assert!(r.hnsw_vectors.is_none());
270 assert!(r.uptime_secs.is_none());
271 }
272
273 #[test]
274 fn test_diag_report_json_serialization() {
275 let r = DiagReport {
276 daemon_running: true,
277 peer_id: Some("12D3KooWTest".to_owned()),
278 connected_peers: Some(3),
279 storage_blocks: Some(1_247),
280 storage_bytes: Some(44_369_510),
281 memory_bytes: Some(134_700_032),
282 avg_inference_ms: Some(12.3),
283 cache_hit_rate: Some(0.874),
284 tensorlogic_rules: Some(42),
285 tensorlogic_facts: Some(156),
286 hnsw_vectors: Some(10_000),
287 uptime_secs: Some(12_240),
288 };
289
290 let json = serde_json::to_string(&r).expect("serialization failed");
291 let back: serde_json::Value = serde_json::from_str(&json).expect("deserialization failed");
292
293 assert_eq!(back["daemon_running"], true);
294 assert_eq!(back["peer_id"], "12D3KooWTest");
295 assert_eq!(back["connected_peers"], 3);
296 assert_eq!(back["storage_blocks"], 1_247);
297 assert_eq!(back["hnsw_vectors"], 10_000);
298 assert_eq!(back["tensorlogic_rules"], 42);
299 assert_eq!(back["tensorlogic_facts"], 156);
300 }
301
302 #[test]
303 fn test_diag_report_debug() {
304 let r = DiagReport::offline();
305 let dbg = format!("{r:?}");
306 assert!(dbg.contains("daemon_running: false"));
307 }
308
309 #[test]
310 fn test_fmt_bytes() {
311 assert_eq!(fmt_bytes(512), "512 B");
312 assert_eq!(fmt_bytes(1_536), "1.5 KB");
313 assert_eq!(fmt_bytes(44_369_510), "42.3 MB");
314 assert_eq!(fmt_bytes(2_147_483_648), "2.0 GB");
315 }
316
317 #[test]
318 fn test_fmt_uptime() {
319 assert_eq!(fmt_uptime(30), "30s");
320 assert_eq!(fmt_uptime(90), "1m 30s");
321 assert_eq!(fmt_uptime(12_240), "3h 24m");
322 }
323
324 #[test]
325 fn test_fmt_count() {
326 assert_eq!(fmt_count(0), "0");
327 assert_eq!(fmt_count(1_247), "1,247");
328 assert_eq!(fmt_count(10_000_000), "10,000,000");
329 }
330}