use anyhow::Result;
#[derive(Debug, serde::Serialize)]
pub struct DiagReport {
pub daemon_running: bool,
pub peer_id: Option<String>,
pub connected_peers: Option<usize>,
pub storage_blocks: Option<u64>,
pub storage_bytes: Option<u64>,
pub memory_bytes: Option<u64>,
pub avg_inference_ms: Option<f64>,
pub cache_hit_rate: Option<f64>,
pub tensorlogic_rules: Option<usize>,
pub tensorlogic_facts: Option<usize>,
pub hnsw_vectors: Option<u64>,
pub uptime_secs: Option<u64>,
}
impl DiagReport {
pub fn offline() -> Self {
Self {
daemon_running: false,
peer_id: None,
connected_peers: None,
storage_blocks: None,
storage_bytes: None,
memory_bytes: None,
avg_inference_ms: None,
cache_hit_rate: None,
tensorlogic_rules: None,
tensorlogic_facts: None,
hnsw_vectors: None,
uptime_secs: None,
}
}
}
fn fmt_bytes(bytes: u64) -> String {
const KB: u64 = 1_024;
const MB: u64 = KB * 1_024;
const GB: u64 = MB * 1_024;
if bytes >= GB {
format!("{:.1} GB", bytes as f64 / GB as f64)
} else if bytes >= MB {
format!("{:.1} MB", bytes as f64 / MB as f64)
} else if bytes >= KB {
format!("{:.1} KB", bytes as f64 / KB as f64)
} else {
format!("{bytes} B")
}
}
fn fmt_uptime(secs: u64) -> String {
let h = secs / 3600;
let m = (secs % 3600) / 60;
let s = secs % 60;
if h > 0 {
format!("{h}h {m}m")
} else if m > 0 {
format!("{m}m {s}s")
} else {
format!("{s}s")
}
}
fn fmt_count(n: u64) -> String {
let s = n.to_string();
let mut out = String::with_capacity(s.len() + s.len() / 3);
for (i, ch) in s.chars().rev().enumerate() {
if i > 0 && i % 3 == 0 {
out.push(',');
}
out.push(ch);
}
out.chars().rev().collect()
}
fn print_text_report(report: &DiagReport) {
println!("=== IPFRS Node Diagnostics ===");
let daemon_str = if report.daemon_running {
"running"
} else {
"not running"
};
println!("{:<20} {}", "Daemon:", daemon_str);
if let Some(ref pid) = report.peer_id {
println!("{:<20} {}", "Peer ID:", pid);
}
if let Some(peers) = report.connected_peers {
println!("{:<20} {}", "Connected peers:", peers);
}
if let Some(blocks) = report.storage_blocks {
println!("{:<20} {}", "Storage blocks:", fmt_count(blocks));
}
if let Some(bytes) = report.storage_bytes {
println!("{:<20} {}", "Storage bytes:", fmt_bytes(bytes));
}
if let Some(mem) = report.memory_bytes {
println!("{:<20} {}", "Memory:", fmt_bytes(mem));
}
if let Some(ms) = report.avg_inference_ms {
println!("{:<20} {:.1} ms", "Avg inference:", ms);
}
if let Some(rate) = report.cache_hit_rate {
println!("{:<20} {:.1}%", "Cache hit rate:", rate * 100.0);
}
if let (Some(rules), Some(facts)) = (report.tensorlogic_rules, report.tensorlogic_facts) {
println!("{:<20} {} rules, {} facts", "TensorLogic:", rules, facts);
}
if let Some(vecs) = report.hnsw_vectors {
println!("{:<20} {}", "HNSW vectors:", fmt_count(vecs));
}
if let Some(secs) = report.uptime_secs {
println!("{:<20} {}", "Uptime:", fmt_uptime(secs));
}
}
pub async fn handle_diag(json_output: bool) -> Result<()> {
let report = match try_collect_diagnostics().await {
Ok(r) => r,
Err(_) => {
if !json_output {
eprintln!("Daemon not running");
}
DiagReport::offline()
}
};
if json_output {
let json = serde_json::to_string_pretty(&report)?;
println!("{json}");
} else {
print_text_report(&report);
}
Ok(())
}
async fn try_collect_diagnostics() -> Result<DiagReport> {
use ipfrs::{Node, NodeConfig};
let mut node = Node::new(NodeConfig::default())?;
node.start().await?;
let diag = node.diagnostics().await?;
let peer_id = node.peer_id().ok();
node.stop().await?;
let storage_blocks = Some(diag.storage.total_blocks);
let storage_bytes = Some(diag.storage.total_bytes);
let memory_bytes = Some(diag.resources.memory_bytes);
let uptime_secs = Some(diag.uptime.as_secs());
let (avg_inference_ms, tensorlogic_rules, tensorlogic_facts) =
if let Some(ref tl) = diag.tensorlogic {
(tl.avg_inference_ms, Some(tl.num_rules), Some(tl.num_facts))
} else {
(None, None, None)
};
let (cache_hit_rate, hnsw_vectors) = if let Some(ref sem) = diag.semantic {
(sem.cache_hit_rate, Some(sem.num_vectors as u64))
} else {
(None, None)
};
let connected_peers = diag.network.as_ref().map(|n| n.connected_peers);
Ok(DiagReport {
daemon_running: true,
peer_id,
connected_peers,
storage_blocks,
storage_bytes,
memory_bytes,
avg_inference_ms,
cache_hit_rate,
tensorlogic_rules,
tensorlogic_facts,
hnsw_vectors,
uptime_secs,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_offline_report_all_none() {
let r = DiagReport::offline();
assert!(!r.daemon_running);
assert!(r.peer_id.is_none());
assert!(r.connected_peers.is_none());
assert!(r.storage_blocks.is_none());
assert!(r.storage_bytes.is_none());
assert!(r.memory_bytes.is_none());
assert!(r.avg_inference_ms.is_none());
assert!(r.cache_hit_rate.is_none());
assert!(r.tensorlogic_rules.is_none());
assert!(r.tensorlogic_facts.is_none());
assert!(r.hnsw_vectors.is_none());
assert!(r.uptime_secs.is_none());
}
#[test]
fn test_diag_report_json_serialization() {
let r = DiagReport {
daemon_running: true,
peer_id: Some("12D3KooWTest".to_owned()),
connected_peers: Some(3),
storage_blocks: Some(1_247),
storage_bytes: Some(44_369_510),
memory_bytes: Some(134_700_032),
avg_inference_ms: Some(12.3),
cache_hit_rate: Some(0.874),
tensorlogic_rules: Some(42),
tensorlogic_facts: Some(156),
hnsw_vectors: Some(10_000),
uptime_secs: Some(12_240),
};
let json = serde_json::to_string(&r).expect("serialization failed");
let back: serde_json::Value = serde_json::from_str(&json).expect("deserialization failed");
assert_eq!(back["daemon_running"], true);
assert_eq!(back["peer_id"], "12D3KooWTest");
assert_eq!(back["connected_peers"], 3);
assert_eq!(back["storage_blocks"], 1_247);
assert_eq!(back["hnsw_vectors"], 10_000);
assert_eq!(back["tensorlogic_rules"], 42);
assert_eq!(back["tensorlogic_facts"], 156);
}
#[test]
fn test_diag_report_debug() {
let r = DiagReport::offline();
let dbg = format!("{r:?}");
assert!(dbg.contains("daemon_running: false"));
}
#[test]
fn test_fmt_bytes() {
assert_eq!(fmt_bytes(512), "512 B");
assert_eq!(fmt_bytes(1_536), "1.5 KB");
assert_eq!(fmt_bytes(44_369_510), "42.3 MB");
assert_eq!(fmt_bytes(2_147_483_648), "2.0 GB");
}
#[test]
fn test_fmt_uptime() {
assert_eq!(fmt_uptime(30), "30s");
assert_eq!(fmt_uptime(90), "1m 30s");
assert_eq!(fmt_uptime(12_240), "3h 24m");
}
#[test]
fn test_fmt_count() {
assert_eq!(fmt_count(0), "0");
assert_eq!(fmt_count(1_247), "1,247");
assert_eq!(fmt_count(10_000_000), "10,000,000");
}
}