Compare commits

...

3 Commits

Author SHA1 Message Date
43fb838c9b Fix duplicate drive display in mergerfs pools
All checks were successful
Build and Release / build-and-release (push) Successful in 2m9s
- Restructure storage rendering logic to prevent drive duplication
- Use specific mergerfs check instead of generic multi-drive condition
- Ensure drives only appear once under organized data/parity sections
2025-11-23 17:46:09 +01:00
54483653f9 Fix mergerfs drive metric parsing for proper pool consolidation
All checks were successful
Build and Release / build-and-release (push) Successful in 2m11s
- Update extract_pool_name to handle data_/parity_ drive metrics correctly
- Fix extract_drive_name to parse mergerfs drive roles properly
- Prevent srv_media_data from being parsed as separate pool
2025-11-23 17:40:12 +01:00
e47803b705 Fix mergerfs pool consolidation and naming
All checks were successful
Build and Release / build-and-release (push) Successful in 1m18s
- Improve pool name extraction in dashboard parsing
- Use consistent mergerfs pool naming in agent
- Add mount_point metric parsing to use actual mount paths
- Fix pool consolidation to prevent duplicate entries
2025-11-23 17:35:23 +01:00
6 changed files with 73 additions and 38 deletions

6
Cargo.lock generated
View File

@@ -279,7 +279,7 @@ checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d"
[[package]] [[package]]
name = "cm-dashboard" name = "cm-dashboard"
version = "0.1.120" version = "0.1.123"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
@@ -301,7 +301,7 @@ dependencies = [
[[package]] [[package]]
name = "cm-dashboard-agent" name = "cm-dashboard-agent"
version = "0.1.120" version = "0.1.123"
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.120" version = "0.1.123"
dependencies = [ dependencies = [
"chrono", "chrono",
"serde", "serde",

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "cm-dashboard-agent" name = "cm-dashboard-agent"
version = "0.1.120" version = "0.1.123"
edition = "2021" edition = "2021"
[dependencies] [dependencies]

View File

@@ -781,7 +781,13 @@ impl DiskCollector {
timestamp: u64, timestamp: u64,
status_tracker: &mut StatusTracker status_tracker: &mut StatusTracker
) { ) {
let pool_name = pool.mount_point.trim_start_matches('/').replace('/', "_"); // Use consistent pool naming: extract mount point without leading slash
let pool_name = if pool.mount_point == "/" {
"root".to_string()
} else {
pool.mount_point.trim_start_matches('/').replace('/', "_")
};
if pool_name.is_empty() { if pool_name.is_empty() {
return; return;
} }

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "cm-dashboard" name = "cm-dashboard"
version = "0.1.120" version = "0.1.123"
edition = "2021" edition = "2021"
[dependencies] [dependencies]

View File

@@ -164,10 +164,9 @@ impl SystemWidget {
for metric in metrics { for metric in metrics {
if metric.name.starts_with("disk_") { if metric.name.starts_with("disk_") {
if let Some(pool_name) = self.extract_pool_name(&metric.name) { if let Some(pool_name) = self.extract_pool_name(&metric.name) {
let mount_point = self.get_mount_point_for_pool(&pool_name);
let pool = pools.entry(pool_name.clone()).or_insert_with(|| StoragePool { let pool = pools.entry(pool_name.clone()).or_insert_with(|| StoragePool {
name: pool_name.clone(), name: pool_name.clone(),
mount_point: mount_point.clone(), mount_point: self.get_mount_point_for_pool(&pool_name), // Default fallback
pool_type: "single".to_string(), // Default, will be updated pool_type: "single".to_string(), // Default, will be updated
pool_health: None, pool_health: None,
drives: Vec::new(), drives: Vec::new(),
@@ -196,6 +195,10 @@ impl SystemWidget {
if let MetricValue::Float(total) = metric.value { if let MetricValue::Float(total) = metric.value {
pool.total_gb = Some(total); pool.total_gb = Some(total);
} }
} else if metric.name.contains("_mount_point") {
if let MetricValue::String(mount_point) = &metric.value {
pool.mount_point = mount_point.clone();
}
} else if metric.name.contains("_pool_type") { } else if metric.name.contains("_pool_type") {
if let MetricValue::String(pool_type) = &metric.value { if let MetricValue::String(pool_type) = &metric.value {
pool.pool_type = pool_type.clone(); pool.pool_type = pool_type.clone();
@@ -343,35 +346,52 @@ impl SystemWidget {
/// Extract pool name from disk metric name /// Extract pool name from disk metric name
fn extract_pool_name(&self, metric_name: &str) -> Option<String> { fn extract_pool_name(&self, metric_name: &str) -> Option<String> {
// Pattern: disk_{pool_name}_{drive_name}_{metric_type} // Pattern: disk_{pool_name}_{various suffixes}
// Since pool_name can contain underscores, work backwards from known metric suffixes // Since pool_name can contain underscores, work backwards from known metric suffixes
if metric_name.starts_with("disk_") { if metric_name.starts_with("disk_") {
// First try drive-specific metrics that have device names // Handle filesystem metrics: disk_{pool}_fs_{filesystem}_{metric}
if let Some(suffix_pos) = metric_name.rfind("_temperature") if metric_name.contains("_fs_") {
if let Some(fs_pos) = metric_name.find("_fs_") {
return Some(metric_name[5..fs_pos].to_string()); // Skip "disk_", extract pool name before "_fs_"
}
}
// Handle pool-level metrics (usage_percent, used_gb, total_gb, mount_point, pool_type, pool_health)
else if let Some(suffix_pos) = metric_name.rfind("_usage_percent")
.or_else(|| metric_name.rfind("_used_gb"))
.or_else(|| metric_name.rfind("_total_gb"))
.or_else(|| metric_name.rfind("_available_gb"))
.or_else(|| metric_name.rfind("_mount_point"))
.or_else(|| metric_name.rfind("_pool_type"))
.or_else(|| metric_name.rfind("_pool_health")) {
return Some(metric_name[5..suffix_pos].to_string()); // Skip "disk_"
}
// Handle drive-specific metrics: disk_{pool}_{drive_role}_{metric} (for mergerfs) or disk_{pool}_{drive}_{metric} (for physical drives)
else if let Some(suffix_pos) = metric_name.rfind("_temperature")
.or_else(|| metric_name.rfind("_wear_percent")) .or_else(|| metric_name.rfind("_wear_percent"))
.or_else(|| metric_name.rfind("_health")) { .or_else(|| metric_name.rfind("_health")) {
// Find the second-to-last underscore to get pool name // For mergerfs pools, metrics look like: disk_srv_media_data_0_temperature or disk_srv_media_parity_0_temperature
// We need to extract just "srv_media" as the pool name
let before_suffix = &metric_name[..suffix_pos]; let before_suffix = &metric_name[..suffix_pos];
// Check if this looks like a mergerfs drive metric (contains data_ or parity_)
if before_suffix.contains("_data_") {
if let Some(data_pos) = before_suffix.find("_data_") {
return Some(metric_name[5..data_pos].to_string()); // Extract pool name before "_data_"
}
} else if before_suffix.contains("_parity_") {
if let Some(parity_pos) = before_suffix.find("_parity_") {
return Some(metric_name[5..parity_pos].to_string()); // Extract pool name before "_parity_"
}
}
// Fallback for physical drive metrics: find the second-to-last underscore
if let Some(drive_start) = before_suffix.rfind('_') { if let Some(drive_start) = before_suffix.rfind('_') {
if drive_start > 5 { if drive_start > 5 {
return Some(metric_name[5..drive_start].to_string()); // Skip "disk_" return Some(metric_name[5..drive_start].to_string()); // Skip "disk_"
} }
} }
} }
// Handle filesystem metrics: disk_{pool}_fs_{filesystem}_{metric} // Fallback: extract first component after disk_ prefix
else if metric_name.contains("_fs_") {
if let Some(fs_pos) = metric_name.find("_fs_") {
return Some(metric_name[5..fs_pos].to_string()); // Skip "disk_", extract pool name before "_fs_"
}
}
// For pool-level metrics (usage_percent, used_gb, total_gb), take everything before the metric suffix
else if let Some(suffix_pos) = metric_name.rfind("_usage_percent")
.or_else(|| metric_name.rfind("_used_gb"))
.or_else(|| metric_name.rfind("_total_gb"))
.or_else(|| metric_name.rfind("_available_gb")) {
return Some(metric_name[5..suffix_pos].to_string()); // Skip "disk_"
}
// Fallback to old behavior for unknown patterns
else if let Some(captures) = metric_name.strip_prefix("disk_") { else if let Some(captures) = metric_name.strip_prefix("disk_") {
if let Some(pos) = captures.find('_') { if let Some(pos) = captures.find('_') {
return Some(captures[..pos].to_string()); return Some(captures[..pos].to_string());
@@ -409,13 +429,22 @@ 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}
// For mergerfs: disk_{pool_name}_{data|parity}_{index}_{metric_type}
// Since pool_name can contain underscores, work backwards from known metric suffixes // Since pool_name can contain underscores, work backwards from known metric suffixes
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"))
.or_else(|| metric_name.rfind("_health")) { .or_else(|| metric_name.rfind("_health")) {
// Find the second-to-last underscore to get the drive name
let before_suffix = &metric_name[..suffix_pos]; let before_suffix = &metric_name[..suffix_pos];
// For mergerfs drive metrics: extract the role_index part (e.g., "data_0", "parity_1")
if before_suffix.contains("_data_") || before_suffix.contains("_parity_") {
if let Some(role_start) = before_suffix.rfind("_data_").or_else(|| before_suffix.rfind("_parity_")) {
return Some(before_suffix[role_start + 1..].to_string()); // e.g., "data_0" or "parity_1"
}
}
// Fallback for physical drive metrics: get the last component
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());
} }
@@ -483,7 +512,7 @@ impl SystemWidget {
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 != "single" && pool.drives.len() > 1 { if pool.pool_type.contains("mergerfs") && pool.drives.len() > 1 {
// Group drives by type for mergerfs pools // Group drives by type for mergerfs pools
let (data_drives, parity_drives): (Vec<_>, Vec<_>) = pool.drives.iter().enumerate() let (data_drives, parity_drives): (Vec<_>, Vec<_>) = pool.drives.iter().enumerate()
.partition(|(_, drive)| { .partition(|(_, drive)| {
@@ -492,7 +521,7 @@ impl SystemWidget {
}); });
// Show data drives // Show data drives
if !data_drives.is_empty() && pool.pool_type.contains("mergerfs") { if !data_drives.is_empty() {
lines.push(Line::from(vec![ lines.push(Line::from(vec![
Span::raw(" "), Span::raw(" "),
Span::styled("├─ ", Typography::tree()), Span::styled("├─ ", Typography::tree()),
@@ -510,7 +539,7 @@ impl SystemWidget {
} }
// Show parity drives // Show parity drives
if !parity_drives.is_empty() && pool.pool_type.contains("mergerfs") { if !parity_drives.is_empty() {
lines.push(Line::from(vec![ lines.push(Line::from(vec![
Span::raw(" "), Span::raw(" "),
Span::styled("└─ ", Typography::tree()), Span::styled("└─ ", Typography::tree()),
@@ -525,13 +554,13 @@ impl SystemWidget {
self.render_drive_line(&mut lines, drive, " ├─"); self.render_drive_line(&mut lines, drive, " ├─");
} }
} }
} else { }
// Regular drive listing for non-mergerfs pools } else if pool.pool_type != "single" && pool.drives.len() > 1 {
for (i, drive) in pool.drives.iter().enumerate() { // Regular drive listing for non-mergerfs multi-drive pools
let is_last = i == pool.drives.len() - 1; for (i, drive) in pool.drives.iter().enumerate() {
let tree_symbol = if is_last { "└─" } else { "├─" }; let is_last = i == pool.drives.len() - 1;
self.render_drive_line(&mut lines, drive, tree_symbol); let tree_symbol = if is_last { "└─" } else { "├─" };
} 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: show drive info + filesystem children

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "cm-dashboard-shared" name = "cm-dashboard-shared"
version = "0.1.120" version = "0.1.123"
edition = "2021" edition = "2021"
[dependencies] [dependencies]