- Remove /tmp autodetection from disk collector (57 lines removed) - Add tmpfs monitoring to memory collector with get_tmpfs_metrics() method - Generate memory_tmp_* metrics for proper RAM-based tmpfs monitoring - Fix type annotations in tmpfs parsing for compilation - System widget now correctly displays tmpfs usage in RAM section
323 lines
11 KiB
Rust
323 lines
11 KiB
Rust
use async_trait::async_trait;
|
|
use cm_dashboard_shared::{registry, Metric, MetricValue, Status, StatusTracker, HysteresisThresholds};
|
|
|
|
use tracing::debug;
|
|
|
|
use super::{utils, Collector, CollectorError};
|
|
use crate::config::MemoryConfig;
|
|
|
|
/// Extremely efficient memory metrics collector
|
|
///
|
|
/// EFFICIENCY OPTIMIZATIONS:
|
|
/// - Single /proc/meminfo read for all memory metrics
|
|
/// - Minimal string parsing with split operations
|
|
/// - Pre-calculated KB to GB conversion
|
|
/// - No regex or complex parsing
|
|
/// - <0.1ms collection time target
|
|
pub struct MemoryCollector {
|
|
name: String,
|
|
usage_thresholds: HysteresisThresholds,
|
|
}
|
|
|
|
/// Memory information parsed from /proc/meminfo
|
|
#[derive(Debug, Default)]
|
|
struct MemoryInfo {
|
|
total_kb: u64,
|
|
available_kb: u64,
|
|
free_kb: u64,
|
|
buffers_kb: u64,
|
|
cached_kb: u64,
|
|
swap_total_kb: u64,
|
|
swap_free_kb: u64,
|
|
}
|
|
|
|
impl MemoryCollector {
|
|
pub fn new(config: MemoryConfig) -> Self {
|
|
// Create hysteresis thresholds with 5% gap for memory usage
|
|
let usage_thresholds = HysteresisThresholds::with_custom_gaps(
|
|
config.usage_warning_percent,
|
|
5.0, // 5% gap for warning recovery
|
|
config.usage_critical_percent,
|
|
5.0, // 5% gap for critical recovery
|
|
);
|
|
|
|
Self {
|
|
name: "memory".to_string(),
|
|
usage_thresholds,
|
|
}
|
|
}
|
|
|
|
/// Calculate memory usage status using hysteresis thresholds
|
|
fn calculate_usage_status(&self, metric_name: &str, usage_percent: f32, status_tracker: &mut StatusTracker) -> Status {
|
|
status_tracker.calculate_with_hysteresis(metric_name, usage_percent, &self.usage_thresholds)
|
|
}
|
|
|
|
/// Parse /proc/meminfo efficiently
|
|
/// Format: "MemTotal: 16384000 kB"
|
|
async fn parse_meminfo(&self) -> Result<MemoryInfo, CollectorError> {
|
|
let content = utils::read_proc_file("/proc/meminfo")?;
|
|
let mut info = MemoryInfo::default();
|
|
|
|
// Parse each line efficiently - only extract what we need
|
|
for line in content.lines() {
|
|
if let Some(colon_pos) = line.find(':') {
|
|
let key = &line[..colon_pos];
|
|
let value_part = &line[colon_pos + 1..];
|
|
|
|
// Extract number from value part (format: " 12345 kB")
|
|
if let Some(number_str) = value_part.split_whitespace().next() {
|
|
if let Ok(value_kb) = utils::parse_u64(number_str) {
|
|
match key {
|
|
"MemTotal" => info.total_kb = value_kb,
|
|
"MemAvailable" => info.available_kb = value_kb,
|
|
"MemFree" => info.free_kb = value_kb,
|
|
"Buffers" => info.buffers_kb = value_kb,
|
|
"Cached" => info.cached_kb = value_kb,
|
|
"SwapTotal" => info.swap_total_kb = value_kb,
|
|
"SwapFree" => info.swap_free_kb = value_kb,
|
|
_ => {} // Skip other fields for efficiency
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Validate that we got essential fields
|
|
if info.total_kb == 0 {
|
|
return Err(CollectorError::Parse {
|
|
value: "MemTotal".to_string(),
|
|
error: "MemTotal not found or zero in /proc/meminfo".to_string(),
|
|
});
|
|
}
|
|
|
|
// If MemAvailable is not available (older kernels), calculate it
|
|
if info.available_kb == 0 {
|
|
info.available_kb = info.free_kb + info.buffers_kb + info.cached_kb;
|
|
}
|
|
|
|
Ok(info)
|
|
}
|
|
|
|
/// Convert KB to GB efficiently (avoiding floating point in hot path)
|
|
fn kb_to_gb(kb: u64) -> f32 {
|
|
kb as f32 / 1_048_576.0 // 1024 * 1024
|
|
}
|
|
|
|
/// Calculate memory metrics from parsed info
|
|
fn calculate_metrics(&self, info: &MemoryInfo, status_tracker: &mut StatusTracker) -> Vec<Metric> {
|
|
let mut metrics = Vec::with_capacity(6);
|
|
|
|
// Calculate derived values
|
|
let used_kb = info.total_kb - info.available_kb;
|
|
let usage_percent = (used_kb as f32 / info.total_kb as f32) * 100.0;
|
|
let usage_status = self.calculate_usage_status(registry::MEMORY_USAGE_PERCENT, usage_percent, status_tracker);
|
|
|
|
let swap_used_kb = info.swap_total_kb - info.swap_free_kb;
|
|
|
|
// Convert to GB for metrics
|
|
let total_gb = Self::kb_to_gb(info.total_kb);
|
|
let used_gb = Self::kb_to_gb(used_kb);
|
|
let available_gb = Self::kb_to_gb(info.available_kb);
|
|
let swap_total_gb = Self::kb_to_gb(info.swap_total_kb);
|
|
let swap_used_gb = Self::kb_to_gb(swap_used_kb);
|
|
|
|
// Memory usage percentage (primary metric with status)
|
|
metrics.push(
|
|
Metric::new(
|
|
registry::MEMORY_USAGE_PERCENT.to_string(),
|
|
MetricValue::Float(usage_percent),
|
|
usage_status,
|
|
)
|
|
.with_description("Memory usage percentage".to_string())
|
|
.with_unit("%".to_string()),
|
|
);
|
|
|
|
// Total memory
|
|
metrics.push(
|
|
Metric::new(
|
|
registry::MEMORY_TOTAL_GB.to_string(),
|
|
MetricValue::Float(total_gb),
|
|
Status::Ok, // Total memory doesn't have status
|
|
)
|
|
.with_description("Total system memory".to_string())
|
|
.with_unit("GB".to_string()),
|
|
);
|
|
|
|
// Used memory
|
|
metrics.push(
|
|
Metric::new(
|
|
registry::MEMORY_USED_GB.to_string(),
|
|
MetricValue::Float(used_gb),
|
|
Status::Ok, // Used memory absolute value doesn't have status
|
|
)
|
|
.with_description("Used system memory".to_string())
|
|
.with_unit("GB".to_string()),
|
|
);
|
|
|
|
// Available memory
|
|
metrics.push(
|
|
Metric::new(
|
|
registry::MEMORY_AVAILABLE_GB.to_string(),
|
|
MetricValue::Float(available_gb),
|
|
Status::Ok, // Available memory absolute value doesn't have status
|
|
)
|
|
.with_description("Available system memory".to_string())
|
|
.with_unit("GB".to_string()),
|
|
);
|
|
|
|
// Swap metrics (only if swap exists)
|
|
if info.swap_total_kb > 0 {
|
|
metrics.push(
|
|
Metric::new(
|
|
registry::MEMORY_SWAP_TOTAL_GB.to_string(),
|
|
MetricValue::Float(swap_total_gb),
|
|
Status::Ok,
|
|
)
|
|
.with_description("Total swap space".to_string())
|
|
.with_unit("GB".to_string()),
|
|
);
|
|
|
|
metrics.push(
|
|
Metric::new(
|
|
registry::MEMORY_SWAP_USED_GB.to_string(),
|
|
MetricValue::Float(swap_used_gb),
|
|
Status::Ok,
|
|
)
|
|
.with_description("Used swap space".to_string())
|
|
.with_unit("GB".to_string()),
|
|
);
|
|
}
|
|
|
|
// Monitor tmpfs (/tmp) usage
|
|
if let Ok(tmpfs_metrics) = self.get_tmpfs_metrics() {
|
|
metrics.extend(tmpfs_metrics);
|
|
}
|
|
|
|
metrics
|
|
}
|
|
|
|
/// Get tmpfs (/tmp) usage metrics
|
|
fn get_tmpfs_metrics(&self) -> Result<Vec<Metric>, CollectorError> {
|
|
use std::process::Command;
|
|
|
|
let output = Command::new("df")
|
|
.arg("--block-size=1")
|
|
.arg("/tmp")
|
|
.output()
|
|
.map_err(|e| CollectorError::SystemRead {
|
|
path: "/tmp".to_string(),
|
|
error: e.to_string(),
|
|
})?;
|
|
|
|
if !output.status.success() {
|
|
return Ok(Vec::new()); // Return empty if /tmp not available
|
|
}
|
|
|
|
let output_str = String::from_utf8(output.stdout)
|
|
.map_err(|e| CollectorError::Parse {
|
|
value: "df output".to_string(),
|
|
error: e.to_string(),
|
|
})?;
|
|
|
|
let lines: Vec<&str> = output_str.lines().collect();
|
|
if lines.len() < 2 {
|
|
return Ok(Vec::new());
|
|
}
|
|
|
|
let fields: Vec<&str> = lines[1].split_whitespace().collect();
|
|
if fields.len() < 4 {
|
|
return Ok(Vec::new());
|
|
}
|
|
|
|
let total_bytes: u64 = fields[1].parse()
|
|
.map_err(|e: std::num::ParseIntError| CollectorError::Parse {
|
|
value: fields[1].to_string(),
|
|
error: e.to_string(),
|
|
})?;
|
|
let used_bytes: u64 = fields[2].parse()
|
|
.map_err(|e: std::num::ParseIntError| CollectorError::Parse {
|
|
value: fields[2].to_string(),
|
|
error: e.to_string(),
|
|
})?;
|
|
|
|
let total_gb = total_bytes as f32 / (1024.0 * 1024.0 * 1024.0);
|
|
let used_gb = used_bytes as f32 / (1024.0 * 1024.0 * 1024.0);
|
|
let usage_percent = if total_bytes > 0 {
|
|
(used_bytes as f32 / total_bytes as f32) * 100.0
|
|
} else {
|
|
0.0
|
|
};
|
|
|
|
let mut metrics = Vec::new();
|
|
let timestamp = chrono::Utc::now().timestamp() as u64;
|
|
|
|
metrics.push(Metric {
|
|
name: "memory_tmp_usage_percent".to_string(),
|
|
value: MetricValue::Float(usage_percent),
|
|
unit: Some("%".to_string()),
|
|
description: Some("tmpfs /tmp usage percentage".to_string()),
|
|
status: Status::Ok,
|
|
timestamp,
|
|
});
|
|
|
|
metrics.push(Metric {
|
|
name: "memory_tmp_used_gb".to_string(),
|
|
value: MetricValue::Float(used_gb),
|
|
unit: Some("GB".to_string()),
|
|
description: Some("tmpfs /tmp used space".to_string()),
|
|
status: Status::Ok,
|
|
timestamp,
|
|
});
|
|
|
|
metrics.push(Metric {
|
|
name: "memory_tmp_total_gb".to_string(),
|
|
value: MetricValue::Float(total_gb),
|
|
unit: Some("GB".to_string()),
|
|
description: Some("tmpfs /tmp total space".to_string()),
|
|
status: Status::Ok,
|
|
timestamp,
|
|
});
|
|
|
|
Ok(metrics)
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl Collector for MemoryCollector {
|
|
fn name(&self) -> &str {
|
|
&self.name
|
|
}
|
|
|
|
async fn collect(&self, status_tracker: &mut StatusTracker) -> Result<Vec<Metric>, CollectorError> {
|
|
debug!("Collecting memory metrics");
|
|
let start = std::time::Instant::now();
|
|
|
|
// Parse memory info from /proc/meminfo
|
|
let info = self.parse_meminfo().await?;
|
|
|
|
// Calculate all metrics from parsed info
|
|
let metrics = self.calculate_metrics(&info, status_tracker);
|
|
|
|
let duration = start.elapsed();
|
|
debug!(
|
|
"Memory collection completed in {:?} with {} metrics",
|
|
duration,
|
|
metrics.len()
|
|
);
|
|
|
|
// Efficiency check: warn if collection takes too long
|
|
if duration.as_millis() > 1 {
|
|
debug!(
|
|
"Memory collection took {}ms - consider optimization",
|
|
duration.as_millis()
|
|
);
|
|
}
|
|
|
|
// Store performance metrics
|
|
// Performance tracking handled by cache system
|
|
|
|
Ok(metrics)
|
|
}
|
|
|
|
}
|