use std::time::Duration;
use decdn_common::admin::{AdminRpcClient, BindingStatus};
use decdn_common::config::ResolvedConfig;
use jsonrpsee::http_client::HttpClientBuilder;
use super::{Finding, Report, Severity};
pub(crate) struct LiveInfo {
pub(crate) daemon_running: bool,
pub(crate) cache_bytes: Option<u64>,
}
pub(crate) fn parse_cache_bytes(metrics_body: &str) -> Option<u64> {
metrics_body.lines().find_map(|line| {
let line = line.trim_start();
if line.starts_with('#') {
return None;
}
let rest = line.strip_prefix("decdn_cache_bytes")?;
let after_name = match rest.chars().next() {
Some(c) if c.is_ascii_whitespace() => rest,
Some('{') => {
let close = rest.find('}')?;
rest.get(close + 1..)?
}
_ => return None,
};
let value = after_name.split_whitespace().next()?;
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_precision_loss
)]
value
.parse::<f64>()
.ok()
.map(|v| if v.is_nan() || v < 0.0 { 0 } else { v as u64 })
})
}
pub(crate) async fn probe_live(
report: &mut Report,
cfg: &ResolvedConfig,
args: &decdn_common::cli::DoctorArgs,
global_config: Option<&std::path::Path>,
) -> LiveInfo {
let dur = Duration::from_millis(args.timeout_ms);
let Ok(admin_url) =
crate::commands::node::resolve_admin_url(args.admin_url.as_deref(), global_config)
else {
report.push(Finding {
group: "Live",
id: "live.health",
severity: Severity::Pass,
title: "admin endpoint not resolvable — live checks skipped".into(),
detail: None,
remediation: None,
});
return LiveInfo {
daemon_running: false,
cache_bytes: None,
};
};
let Ok(client) = HttpClientBuilder::default()
.request_timeout(dur)
.build(&admin_url)
else {
report.push(Finding {
group: "Live",
id: "live.health",
severity: Severity::Pass,
title: "admin client could not be built — live checks skipped".into(),
detail: None,
remediation: None,
});
return LiveInfo {
daemon_running: false,
cache_bytes: None,
};
};
let Ok(health) = client.health().await else {
report.push(Finding {
group: "Live",
id: "live.health",
severity: Severity::Pass,
title: "no running daemon detected (offline diagnosis)".into(),
detail: None,
remediation: None,
});
return LiveInfo {
daemon_running: false,
cache_bytes: None,
};
};
let mismatch = matches!(health.binding, BindingStatus::Mismatch);
report.push(Finding {
group: "Live",
id: "live.health",
severity: if mismatch { Severity::Fail } else { Severity::Pass },
title: if mismatch {
"daemon key/binding mismatch".into()
} else {
"daemon healthy".into()
},
detail: Some(format!(
"binding={:?} registry_active={}",
health.binding, health.registry_active
)),
remediation: if mismatch {
Some(
"the running key does not match the bound identity; check node.secret / registration"
.into(),
)
} else {
None
},
});
if !health.registry_active {
report.push(Finding {
group: "Live",
id: "live.registry",
severity: Severity::Warn,
title: "node is not active in the on-chain registry".into(),
detail: None,
remediation: Some("run `decdn node register` / check bond & capacity".into()),
});
}
let cache_bytes = scrape_cache_bytes(cfg, dur).await;
LiveInfo {
daemon_running: true,
cache_bytes,
}
}
async fn scrape_cache_bytes(cfg: &ResolvedConfig, dur: Duration) -> Option<u64> {
let url = format!(
"http://{}:{}/metrics",
cfg.observability.metrics_bind, cfg.observability.metrics_port
);
let client = reqwest::Client::builder().timeout(dur).build().ok()?;
let body = client.get(&url).send().await.ok()?.text().await.ok()?;
parse_cache_bytes(&body)
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] mod tests;