Add wear percentage display and NVMe temperature collection
All checks were successful
Build and Release / build-and-release (push) Successful in 2m9s
All checks were successful
Build and Release / build-and-release (push) Successful in 2m9s
- Display wear percentage in storage headers for single physical drives - Remove redundant drive type indicators, show wear data instead - Fix wear metric parsing for physical drives (underscore count issue) - Add NVMe temperature parsing support (Temperature: format) - Add raw metrics debugging functionality for troubleshooting - Clean up physical drive display to remove redundant information
This commit is contained in:
parent
9b4191b2c3
commit
41ded0170c
6
Cargo.lock
generated
6
Cargo.lock
generated
@ -279,7 +279,7 @@ checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cm-dashboard"
|
name = "cm-dashboard"
|
||||||
version = "0.1.128"
|
version = "0.1.129"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
@ -301,7 +301,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cm-dashboard-agent"
|
name = "cm-dashboard-agent"
|
||||||
version = "0.1.128"
|
version = "0.1.129"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@ -324,7 +324,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cm-dashboard-shared"
|
name = "cm-dashboard-shared"
|
||||||
version = "0.1.128"
|
version = "0.1.129"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"serde",
|
"serde",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "cm-dashboard-agent"
|
name = "cm-dashboard-agent"
|
||||||
version = "0.1.129"
|
version = "0.1.130"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@ -499,6 +499,17 @@ impl DiskCollector {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// NVMe format: "Temperature:" (capital T)
|
||||||
|
if line.contains("Temperature:") {
|
||||||
|
if let Some(temp_part) = line.split("Temperature:").nth(1) {
|
||||||
|
if let Some(temp_str) = temp_part.split_whitespace().next() {
|
||||||
|
if let Ok(temp) = temp_str.parse::<f32>() {
|
||||||
|
return Some(temp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Legacy format: "temperature:" (lowercase)
|
||||||
if line.contains("temperature:") {
|
if line.contains("temperature:") {
|
||||||
if let Some(temp_part) = line.split("temperature:").nth(1) {
|
if let Some(temp_part) = line.split("temperature:").nth(1) {
|
||||||
if let Some(temp_str) = temp_part.split_whitespace().next() {
|
if let Some(temp_str) = temp_part.split_whitespace().next() {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "cm-dashboard"
|
name = "cm-dashboard"
|
||||||
version = "0.1.129"
|
version = "0.1.130"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@ -20,12 +20,13 @@ pub struct Dashboard {
|
|||||||
tui_app: Option<TuiApp>,
|
tui_app: Option<TuiApp>,
|
||||||
terminal: Option<Terminal<CrosstermBackend<io::Stdout>>>,
|
terminal: Option<Terminal<CrosstermBackend<io::Stdout>>>,
|
||||||
headless: bool,
|
headless: bool,
|
||||||
|
raw_data: bool,
|
||||||
initial_commands_sent: std::collections::HashSet<String>,
|
initial_commands_sent: std::collections::HashSet<String>,
|
||||||
config: DashboardConfig,
|
config: DashboardConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Dashboard {
|
impl Dashboard {
|
||||||
pub async fn new(config_path: Option<String>, headless: bool) -> Result<Self> {
|
pub async fn new(config_path: Option<String>, headless: bool, raw_data: bool) -> Result<Self> {
|
||||||
info!("Initializing dashboard");
|
info!("Initializing dashboard");
|
||||||
|
|
||||||
// Load configuration - try default path if not specified
|
// Load configuration - try default path if not specified
|
||||||
@ -119,6 +120,7 @@ impl Dashboard {
|
|||||||
tui_app,
|
tui_app,
|
||||||
terminal,
|
terminal,
|
||||||
headless,
|
headless,
|
||||||
|
raw_data,
|
||||||
initial_commands_sent: std::collections::HashSet::new(),
|
initial_commands_sent: std::collections::HashSet::new(),
|
||||||
config,
|
config,
|
||||||
})
|
})
|
||||||
@ -204,6 +206,18 @@ impl Dashboard {
|
|||||||
.insert(metric_message.hostname.clone());
|
.insert(metric_message.hostname.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show raw data if requested (before processing)
|
||||||
|
if self.raw_data {
|
||||||
|
println!("RAW METRICS FROM {}: {} metrics", metric_message.hostname, metric_message.metrics.len());
|
||||||
|
for metric in &metric_message.metrics {
|
||||||
|
println!(" {}: {:?} ({:?})", metric.name, metric.value, metric.status);
|
||||||
|
if let Some(desc) = &metric.description {
|
||||||
|
println!(" └─ {}", desc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!("{}", "─".repeat(80));
|
||||||
|
}
|
||||||
|
|
||||||
// Update metric store
|
// Update metric store
|
||||||
self.metric_store
|
self.metric_store
|
||||||
.update_metrics(&metric_message.hostname, metric_message.metrics);
|
.update_metrics(&metric_message.hostname, metric_message.metrics);
|
||||||
|
|||||||
@ -51,6 +51,10 @@ struct Cli {
|
|||||||
/// Run in headless mode (no TUI, just logging)
|
/// Run in headless mode (no TUI, just logging)
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
headless: bool,
|
headless: bool,
|
||||||
|
|
||||||
|
/// Show raw agent data in headless mode
|
||||||
|
#[arg(long)]
|
||||||
|
raw_data: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
@ -86,7 +90,7 @@ async fn main() -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create and run dashboard
|
// Create and run dashboard
|
||||||
let mut dashboard = Dashboard::new(cli.config, cli.headless).await?;
|
let mut dashboard = Dashboard::new(cli.config, cli.headless, cli.raw_data).await?;
|
||||||
|
|
||||||
// Setup graceful shutdown
|
// Setup graceful shutdown
|
||||||
let ctrl_c = async {
|
let ctrl_c = async {
|
||||||
|
|||||||
@ -232,12 +232,16 @@ impl SystemWidget {
|
|||||||
if let MetricValue::Float(temp) = metric.value {
|
if let MetricValue::Float(temp) = metric.value {
|
||||||
drive.temperature = Some(temp);
|
drive.temperature = Some(temp);
|
||||||
drive.status = metric.status.clone();
|
drive.status = metric.status.clone();
|
||||||
|
// For physical drives, if this is the main drive, also update pool health
|
||||||
|
if drive.name == pool.name && pool.health_status == Status::Unknown {
|
||||||
|
pool.health_status = metric.status.clone();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if metric.name.contains("_wear_percent") {
|
} else if metric.name.contains("_wear_percent") {
|
||||||
if let Some(drive_name) = self.extract_drive_name(&metric.name) {
|
if let Some(drive_name) = self.extract_drive_name(&metric.name) {
|
||||||
// Find existing drive or create new one
|
// For physical drives, ensure we create the drive object
|
||||||
let drive_exists = pool.drives.iter().any(|d| d.name == drive_name);
|
let drive_exists = pool.drives.iter().any(|d| d.name == drive_name);
|
||||||
if !drive_exists {
|
if !drive_exists {
|
||||||
pool.drives.push(StorageDrive {
|
pool.drives.push(StorageDrive {
|
||||||
@ -252,6 +256,10 @@ impl SystemWidget {
|
|||||||
if let MetricValue::Float(wear) = metric.value {
|
if let MetricValue::Float(wear) = metric.value {
|
||||||
drive.wear_percent = Some(wear);
|
drive.wear_percent = Some(wear);
|
||||||
drive.status = metric.status.clone();
|
drive.status = metric.status.clone();
|
||||||
|
// For physical drives, if this is the main drive, also update pool health
|
||||||
|
if drive.name == pool.name && pool.health_status == Status::Unknown {
|
||||||
|
pool.health_status = metric.status.clone();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -372,20 +380,23 @@ impl SystemWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle physical drive health metrics: disk_{drive}_health
|
// Handle physical drive metrics: disk_{drive}_health and disk_{drive}_wear_percent
|
||||||
if metric_name.ends_with("_health") && !metric_name.contains("_pool_health") {
|
if (metric_name.ends_with("_health") && !metric_name.contains("_pool_health"))
|
||||||
// Count underscores to distinguish physical drive health (disk_{drive}_health)
|
|| metric_name.ends_with("_wear_percent") {
|
||||||
// from pool drive health (disk_{pool}_{drive}_health)
|
// Count underscores to distinguish physical drive metrics (disk_{drive}_metric)
|
||||||
|
// from pool drive metrics (disk_{pool}_{drive}_metric)
|
||||||
let underscore_count = metric_name.matches('_').count();
|
let underscore_count = metric_name.matches('_').count();
|
||||||
if underscore_count == 2 { // disk_{drive}_health
|
// disk_nvme0n1_wear_percent has 3 underscores: disk_nvme0n1_wear_percent
|
||||||
if let Some(suffix_pos) = metric_name.rfind("_health") {
|
if underscore_count == 3 { // disk_{drive}_wear_percent (where drive has underscores)
|
||||||
|
if let Some(suffix_pos) = metric_name.rfind("_health")
|
||||||
|
.or_else(|| metric_name.rfind("_wear_percent")) {
|
||||||
return Some(metric_name[5..suffix_pos].to_string()); // Skip "disk_"
|
return Some(metric_name[5..suffix_pos].to_string()); // Skip "disk_"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle drive-specific metrics: disk_{pool}_{drive}_{metric}
|
// Handle drive-specific metrics: disk_{pool}_{drive}_{metric}
|
||||||
let drive_suffixes = ["_temperature", "_wear_percent", "_health"];
|
let drive_suffixes = ["_temperature", "_health"];
|
||||||
for suffix in drive_suffixes {
|
for suffix in drive_suffixes {
|
||||||
if let Some(suffix_pos) = metric_name.rfind(suffix) {
|
if let Some(suffix_pos) = metric_name.rfind(suffix) {
|
||||||
// Extract pool name by finding the second-to-last underscore
|
// Extract pool name by finding the second-to-last underscore
|
||||||
@ -428,9 +439,9 @@ impl SystemWidget {
|
|||||||
|
|
||||||
/// Extract drive name from disk metric name
|
/// Extract drive name from disk metric name
|
||||||
fn extract_drive_name(&self, metric_name: &str) -> Option<String> {
|
fn extract_drive_name(&self, metric_name: &str) -> Option<String> {
|
||||||
// Pattern: disk_{pool_name}_{drive_name}_{metric_type}
|
// Pattern: disk_{pool_name}_{drive_name}_{metric_type} OR disk_{drive_name}_{metric_type}
|
||||||
// Now using actual device names like: disk_srv_media_sdb_temperature
|
// Pool drives: disk_srv_media_sdb_temperature
|
||||||
// Since pool_name can contain underscores, work backwards from known metric suffixes
|
// Physical drives: disk_nvme0n1_temperature
|
||||||
if metric_name.starts_with("disk_") {
|
if metric_name.starts_with("disk_") {
|
||||||
if let Some(suffix_pos) = metric_name.rfind("_temperature")
|
if let Some(suffix_pos) = metric_name.rfind("_temperature")
|
||||||
.or_else(|| metric_name.rfind("_wear_percent"))
|
.or_else(|| metric_name.rfind("_wear_percent"))
|
||||||
@ -440,6 +451,10 @@ impl SystemWidget {
|
|||||||
// Extract the last component as drive name (e.g., "sdb", "sdc", "nvme0n1")
|
// Extract the last component as drive name (e.g., "sdb", "sdc", "nvme0n1")
|
||||||
if let Some(drive_start) = before_suffix.rfind('_') {
|
if let Some(drive_start) = before_suffix.rfind('_') {
|
||||||
return Some(before_suffix[drive_start + 1..].to_string());
|
return Some(before_suffix[drive_start + 1..].to_string());
|
||||||
|
} else {
|
||||||
|
// Handle physical drive metrics: disk_{drive}_metric (no pool)
|
||||||
|
// Extract everything after "disk_" as the drive name
|
||||||
|
return Some(before_suffix[5..].to_string()); // Skip "disk_"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -453,8 +468,16 @@ impl SystemWidget {
|
|||||||
for pool in &self.storage_pools {
|
for pool in &self.storage_pools {
|
||||||
// Pool header line with type and health
|
// Pool header line with type and health
|
||||||
let pool_label = if pool.pool_type.starts_with("drive (") {
|
let pool_label = if pool.pool_type.starts_with("drive (") {
|
||||||
// For physical drives, show the drive name instead of mount point
|
// For physical drives, show the drive name with wear percentage if available
|
||||||
format!("{} ({}):", pool.name, pool.pool_type)
|
// Look for any drive with wear data (physical drives may have drives named after the pool)
|
||||||
|
let wear_opt = pool.drives.iter()
|
||||||
|
.find_map(|d| d.wear_percent);
|
||||||
|
|
||||||
|
if let Some(wear) = wear_opt {
|
||||||
|
format!("{} W: {:.0}%:", pool.name, wear)
|
||||||
|
} else {
|
||||||
|
format!("{}:", pool.name)
|
||||||
|
}
|
||||||
} else if pool.pool_type == "single" {
|
} else if pool.pool_type == "single" {
|
||||||
format!("{}:", pool.mount_point)
|
format!("{}:", pool.mount_point)
|
||||||
} else {
|
} else {
|
||||||
@ -468,7 +491,8 @@ impl SystemWidget {
|
|||||||
|
|
||||||
// Skip pool health line as discussed - removed
|
// Skip pool health line as discussed - removed
|
||||||
|
|
||||||
// Total usage line (always show for pools)
|
// Total usage line (only show for multi-drive pools, skip for single physical drives)
|
||||||
|
if !pool.pool_type.starts_with("drive (") {
|
||||||
let usage_text = match (pool.usage_percent, pool.used_gb, pool.total_gb) {
|
let usage_text = match (pool.usage_percent, pool.used_gb, pool.total_gb) {
|
||||||
(Some(pct), Some(used), Some(total)) => {
|
(Some(pct), Some(used), Some(total)) => {
|
||||||
format!("Total: {:.0}% {:.1}GB/{:.1}GB", pct, used, total)
|
format!("Total: {:.0}% {:.1}GB/{:.1}GB", pct, used, total)
|
||||||
@ -487,6 +511,7 @@ impl SystemWidget {
|
|||||||
];
|
];
|
||||||
usage_spans.extend(StatusIcons::create_status_spans(pool.status.clone(), &usage_text));
|
usage_spans.extend(StatusIcons::create_status_spans(pool.status.clone(), &usage_text));
|
||||||
lines.push(Line::from(usage_spans));
|
lines.push(Line::from(usage_spans));
|
||||||
|
}
|
||||||
|
|
||||||
// Drive lines with enhanced grouping
|
// Drive lines with enhanced grouping
|
||||||
if pool.pool_type.contains("mergerfs") && pool.drives.len() > 1 {
|
if pool.pool_type.contains("mergerfs") && pool.drives.len() > 1 {
|
||||||
@ -540,34 +565,7 @@ impl SystemWidget {
|
|||||||
self.render_drive_line(&mut lines, drive, tree_symbol);
|
self.render_drive_line(&mut lines, drive, tree_symbol);
|
||||||
}
|
}
|
||||||
} else if pool.pool_type.starts_with("drive (") {
|
} else if pool.pool_type.starts_with("drive (") {
|
||||||
// Physical drive pools: show drive info + filesystem children
|
// Physical drive pools: wear data shown in header, skip drive lines, show filesystems directly
|
||||||
// First show drive information
|
|
||||||
for drive in &pool.drives {
|
|
||||||
let mut drive_info = Vec::new();
|
|
||||||
if let Some(temp) = drive.temperature {
|
|
||||||
drive_info.push(format!("T: {:.0}°C", temp));
|
|
||||||
}
|
|
||||||
if let Some(wear) = drive.wear_percent {
|
|
||||||
drive_info.push(format!("W: {:.0}%", wear));
|
|
||||||
}
|
|
||||||
let drive_text = if drive_info.is_empty() {
|
|
||||||
format!("Drive: {}", drive.name)
|
|
||||||
} else {
|
|
||||||
format!("Drive: {}", drive_info.join(" "))
|
|
||||||
};
|
|
||||||
|
|
||||||
let has_filesystems = !pool.filesystems.is_empty();
|
|
||||||
let tree_symbol = if has_filesystems { "├─" } else { "└─" };
|
|
||||||
let mut drive_spans = vec![
|
|
||||||
Span::raw(" "),
|
|
||||||
Span::styled(tree_symbol, Typography::tree()),
|
|
||||||
Span::raw(" "),
|
|
||||||
];
|
|
||||||
drive_spans.extend(StatusIcons::create_status_spans(drive.status.clone(), &drive_text));
|
|
||||||
lines.push(Line::from(drive_spans));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Then show filesystem children
|
|
||||||
for (i, filesystem) in pool.filesystems.iter().enumerate() {
|
for (i, filesystem) in pool.filesystems.iter().enumerate() {
|
||||||
let is_last = i == pool.filesystems.len() - 1;
|
let is_last = i == pool.filesystems.len() - 1;
|
||||||
let tree_symbol = if is_last { "└─" } else { "├─" };
|
let tree_symbol = if is_last { "└─" } else { "├─" };
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "cm-dashboard-shared"
|
name = "cm-dashboard-shared"
|
||||||
version = "0.1.129"
|
version = "0.1.130"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user