Files
nano_metrics/agent/src/metrics/smart.rs
T
Gilles Soulier 017d7bb1bb fix(smart v0.1.8): NVMe — contrôleur correct + flag -a pour attributs complets
- /sys/block expose nvme0n1 (namespace), mais smartctl a besoin du contrôleur
  nvme0 → déduplication via HashSet pour éviter les doublons nvme0n1/nvme0
- smartctl -j → smartctl -a -j pour inclure nvme_smart_health_information_log
  (sans -a, le log de santé NVMe n'est pas dans la sortie JSON)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 07:07:45 +02:00

126 lines
3.5 KiB
Rust

use serde::Deserialize;
#[derive(Deserialize)]
struct SmartJson {
smart_status: SmartStatus,
temperature: Option<SmartTemp>,
ata_smart_attributes: Option<SmartAttrs>,
nvme_smart_health_information_log: Option<NvmeHealth>,
}
#[derive(Deserialize)]
struct SmartStatus { passed: bool }
#[derive(Deserialize)]
struct SmartTemp { current: i64 }
#[derive(Deserialize)]
struct SmartAttrs { table: Vec<SmartAttr> }
#[derive(Deserialize)]
struct SmartAttr {
id: u8,
raw: SmartAttrRaw,
value: Option<i64>,
}
#[derive(Deserialize)]
struct SmartAttrRaw { value: i64 }
#[derive(Deserialize)]
struct NvmeHealth {
percentage_used: Option<i64>,
temperature: Option<i64>,
}
pub fn is_available() -> bool {
std::process::Command::new("which")
.arg("smartctl")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
pub fn parse_json(json: &str) -> Result<crate::payload::SmartMetrics, serde_json::Error> {
let s: SmartJson = serde_json::from_str(json)?;
let temperature = s.temperature.as_ref().map(|t| t.current)
.or_else(|| s.nvme_smart_health_information_log.as_ref()?.temperature);
let mut reallocated = None;
let mut power_hours = None;
let mut wear = None;
if let Some(attrs) = &s.ata_smart_attributes {
for attr in &attrs.table {
match attr.id {
5 => reallocated = Some(attr.raw.value),
9 => power_hours = Some(attr.raw.value),
177 => wear = attr.value,
_ => {}
}
}
}
if wear.is_none() {
if let Some(nvme) = &s.nvme_smart_health_information_log {
if let Some(pct) = nvme.percentage_used {
wear = Some(100 - pct);
}
}
}
Ok(crate::payload::SmartMetrics {
device: String::new(),
passed: s.smart_status.passed,
temperature,
reallocated_sectors: reallocated,
power_on_hours: power_hours,
wear_level: wear,
})
}
pub fn collect() -> Option<Vec<crate::payload::SmartMetrics>> {
if !is_available() {
return None;
}
let mut devs: Vec<String> = std::fs::read_dir("/sys/block")
.into_iter()
.flatten()
.flatten()
.map(|e| e.file_name().into_string().unwrap_or_default())
.filter_map(|n| {
if n.starts_with("sd") {
Some(format!("/dev/{}", n))
} else if n.starts_with("nvme") && !n.contains('n') {
// /sys/block contient nvme0n1 (namespace) — on utilise le contrôleur nvme0
Some(format!("/dev/{}", n))
} else if n.starts_with("nvme") && n.contains('n') {
// Déduit le contrôleur depuis nvme0n1 → nvme0
let ctrl = n.split('n').next()?;
Some(format!("/dev/{}", ctrl))
} else {
None
}
})
.collect::<std::collections::HashSet<_>>()
.into_iter()
.collect();
devs.sort();
let mut results = Vec::new();
for dev in &devs {
let Ok(output) = std::process::Command::new("smartctl")
.args(["-a", "-j", dev])
.output() else { continue };
let json = String::from_utf8_lossy(&output.stdout);
if let Ok(metrics) = parse_json(&json) {
results.push(crate::payload::SmartMetrics {
device: dev.trim_start_matches("/dev/").to_string(),
..metrics
});
}
}
if results.is_empty() { None } else { Some(results) }
}