Implement hysteresis for metric status changes to prevent flapping
Add comprehensive hysteresis support to prevent status oscillation near threshold boundaries while maintaining responsive alerting. Key Features: - HysteresisThresholds with configurable upper/lower limits - StatusTracker for per-metric status history - Default gaps: CPU load 10%, memory 5%, disk temp 5°C Updated Components: - CPU load collector (5-minute average with hysteresis) - Memory usage collector (percentage-based thresholds) - Disk temperature collector (SMART data monitoring) - All collectors updated to support StatusTracker interface Cache Interval Adjustments: - Service status: 60s → 10s (faster response) - Disk usage: 300s → 60s (more frequent checks) - Backup status: 900s → 60s (quicker updates) - SMART data: moved to 600s tier (10 minutes) Architecture: - Individual metric status calculation in collectors - Centralized StatusTracker in MetricCollectionManager - Status aggregation preserved in dashboard widgets
This commit is contained in:
@@ -7,7 +7,7 @@ use ratatui::{
|
||||
use tracing::debug;
|
||||
|
||||
use super::Widget;
|
||||
use crate::ui::theme::{Typography, StatusIcons};
|
||||
use crate::ui::theme::{StatusIcons, Typography};
|
||||
|
||||
/// CPU widget displaying load, temperature, and frequency
|
||||
#[derive(Clone)]
|
||||
@@ -38,7 +38,7 @@ impl CpuWidget {
|
||||
has_data: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Format load average for display
|
||||
fn format_load(&self) -> String {
|
||||
match (self.load_1min, self.load_5min, self.load_15min) {
|
||||
@@ -48,7 +48,7 @@ impl CpuWidget {
|
||||
_ => "— — —".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Format frequency for display
|
||||
fn format_frequency(&self) -> String {
|
||||
match self.frequency {
|
||||
@@ -56,16 +56,15 @@ impl CpuWidget {
|
||||
None => "— MHz".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl Widget for CpuWidget {
|
||||
fn update_from_metrics(&mut self, metrics: &[&Metric]) {
|
||||
debug!("CPU widget updating with {} metrics", metrics.len());
|
||||
|
||||
|
||||
// Reset status aggregation
|
||||
let mut statuses = Vec::new();
|
||||
|
||||
|
||||
for metric in metrics {
|
||||
match metric.name.as_str() {
|
||||
"cpu_load_1min" => {
|
||||
@@ -101,33 +100,40 @@ impl Widget for CpuWidget {
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Aggregate status
|
||||
self.status = if statuses.is_empty() {
|
||||
Status::Unknown
|
||||
} else {
|
||||
Status::aggregate(&statuses)
|
||||
};
|
||||
|
||||
|
||||
self.has_data = !metrics.is_empty();
|
||||
|
||||
debug!("CPU widget updated: load={:?}, temp={:?}, freq={:?}, status={:?}",
|
||||
self.load_1min, self.temperature, self.frequency, self.status);
|
||||
|
||||
debug!(
|
||||
"CPU widget updated: load={:?}, temp={:?}, freq={:?}, status={:?}",
|
||||
self.load_1min, self.temperature, self.frequency, self.status
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
fn render(&mut self, frame: &mut Frame, area: Rect) {
|
||||
let content_chunks = Layout::default().direction(Direction::Vertical).constraints([Constraint::Length(1), Constraint::Length(1)]).split(area);
|
||||
let content_chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Length(1), Constraint::Length(1)])
|
||||
.split(area);
|
||||
let cpu_title = Paragraph::new("CPU:").style(Typography::widget_title());
|
||||
frame.render_widget(cpu_title, content_chunks[0]);
|
||||
let load_freq_spans = StatusIcons::create_status_spans(self.status, &format!("Load: {} • {}", self.format_load(), self.format_frequency()));
|
||||
let load_freq_spans = StatusIcons::create_status_spans(
|
||||
self.status,
|
||||
&format!("Load: {} • {}", self.format_load(), self.format_frequency()),
|
||||
);
|
||||
let load_freq_para = Paragraph::new(ratatui::text::Line::from(load_freq_spans));
|
||||
frame.render_widget(load_freq_para, content_chunks[1]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl Default for CpuWidget {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ use ratatui::{
|
||||
use tracing::debug;
|
||||
|
||||
use super::Widget;
|
||||
use crate::ui::theme::{Typography, StatusIcons};
|
||||
use crate::ui::theme::{StatusIcons, Typography};
|
||||
|
||||
/// Memory widget displaying usage, totals, and swap information
|
||||
#[derive(Clone)]
|
||||
@@ -52,8 +52,7 @@ impl MemoryWidget {
|
||||
has_data: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// Get memory usage percentage for gauge
|
||||
fn get_memory_percentage(&self) -> u16 {
|
||||
match self.usage_percent {
|
||||
@@ -108,10 +107,8 @@ impl MemoryWidget {
|
||||
let total_str = Self::format_size_units(total_mb);
|
||||
format!("{}/{}", used_str, total_str)
|
||||
}
|
||||
(None, Some(used_mb), None) => {
|
||||
Self::format_size_units(used_mb)
|
||||
}
|
||||
_ => "—".to_string()
|
||||
(None, Some(used_mb), None) => Self::format_size_units(used_mb),
|
||||
_ => "—".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,16 +126,15 @@ impl MemoryWidget {
|
||||
Status::Unknown
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl Widget for MemoryWidget {
|
||||
fn update_from_metrics(&mut self, metrics: &[&Metric]) {
|
||||
debug!("Memory widget updating with {} metrics", metrics.len());
|
||||
|
||||
|
||||
// Reset status aggregation
|
||||
let mut statuses = Vec::new();
|
||||
|
||||
|
||||
for metric in metrics {
|
||||
match metric.name.as_str() {
|
||||
"memory_usage_percent" => {
|
||||
@@ -198,36 +194,53 @@ impl Widget for MemoryWidget {
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Aggregate status
|
||||
self.status = if statuses.is_empty() {
|
||||
Status::Unknown
|
||||
} else {
|
||||
Status::aggregate(&statuses)
|
||||
};
|
||||
|
||||
|
||||
self.has_data = !metrics.is_empty();
|
||||
|
||||
|
||||
debug!("Memory widget updated: usage={:?}%, total={:?}GB, swap_total={:?}GB, tmp={:?}/{:?}MB, status={:?}",
|
||||
self.usage_percent, self.total_gb, self.swap_total_gb, self.tmp_size_mb, self.tmp_total_mb, self.status);
|
||||
}
|
||||
|
||||
|
||||
fn render(&mut self, frame: &mut Frame, area: Rect) {
|
||||
let content_chunks = Layout::default().direction(Direction::Vertical).constraints([Constraint::Length(1), Constraint::Length(1), Constraint::Length(1)]).split(area);
|
||||
let content_chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(1),
|
||||
])
|
||||
.split(area);
|
||||
let mem_title = Paragraph::new("RAM:").style(Typography::widget_title());
|
||||
frame.render_widget(mem_title, content_chunks[0]);
|
||||
|
||||
|
||||
// Format used and total memory with smart units, percentage, and status icon
|
||||
let used_str = self.used_gb.map_or("—".to_string(), |v| Self::format_size_units(v * 1024.0)); // Convert GB to MB for formatting
|
||||
let total_str = self.total_gb.map_or("—".to_string(), |v| Self::format_size_units(v * 1024.0)); // Convert GB to MB for formatting
|
||||
let used_str = self
|
||||
.used_gb
|
||||
.map_or("—".to_string(), |v| Self::format_size_units(v * 1024.0)); // Convert GB to MB for formatting
|
||||
let total_str = self
|
||||
.total_gb
|
||||
.map_or("—".to_string(), |v| Self::format_size_units(v * 1024.0)); // Convert GB to MB for formatting
|
||||
let percentage = self.get_memory_percentage();
|
||||
let mem_details_spans = StatusIcons::create_status_spans(self.status, &format!("Used: {}% {}/{}", percentage, used_str, total_str));
|
||||
let mem_details_spans = StatusIcons::create_status_spans(
|
||||
self.status,
|
||||
&format!("Used: {}% {}/{}", percentage, used_str, total_str),
|
||||
);
|
||||
let mem_details_para = Paragraph::new(ratatui::text::Line::from(mem_details_spans));
|
||||
frame.render_widget(mem_details_para, content_chunks[1]);
|
||||
|
||||
|
||||
// /tmp usage line with status icon
|
||||
let tmp_status = self.get_tmp_status();
|
||||
let tmp_spans = StatusIcons::create_status_spans(tmp_status, &format!("tmp: {}", self.format_tmp_usage()));
|
||||
let tmp_spans = StatusIcons::create_status_spans(
|
||||
tmp_status,
|
||||
&format!("tmp: {}", self.format_tmp_usage()),
|
||||
);
|
||||
let tmp_para = Paragraph::new(ratatui::text::Line::from(tmp_spans));
|
||||
frame.render_widget(tmp_para, content_chunks[2]);
|
||||
}
|
||||
@@ -237,4 +250,4 @@ impl Default for MemoryWidget {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
use cm_dashboard_shared::Metric;
|
||||
use ratatui::{layout::Rect, Frame};
|
||||
|
||||
pub mod backup;
|
||||
pub mod cpu;
|
||||
pub mod memory;
|
||||
pub mod services;
|
||||
pub mod backup;
|
||||
|
||||
pub use backup::BackupWidget;
|
||||
pub use cpu::CpuWidget;
|
||||
pub use memory::MemoryWidget;
|
||||
pub use services::ServicesWidget;
|
||||
pub use backup::BackupWidget;
|
||||
|
||||
/// Widget trait for UI components that display metrics
|
||||
pub trait Widget {
|
||||
/// Update widget with new metrics data
|
||||
fn update_from_metrics(&mut self, metrics: &[&Metric]);
|
||||
|
||||
|
||||
/// Render the widget to a terminal frame
|
||||
fn render(&mut self, frame: &mut Frame, area: Rect);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user