pub mod cgroup_reader;
pub mod proc_reader;
use once_cell::sync::Lazy;
use prometheus::{IntGauge, Opts};
use std::time::Duration;
use tokio::time;
use tracing::{debug, info, trace, warn};
use crate::metrics::METRICS_REGISTRY;
pub static PROCESS_RESIDENT_MEMORY_BYTES: Lazy<IntGauge> = Lazy::new(|| {
let gauge = IntGauge::with_opts(
Opts::new(
"lazydns_process_resident_memory_bytes",
"Process resident memory (RSS) from /proc/self/status in bytes",
)
.const_label("source", "proc"),
)
.expect("Failed to create lazydns_process_resident_memory_bytes gauge");
METRICS_REGISTRY
.register(Box::new(gauge.clone()))
.expect("Failed to register lazydns_process_resident_memory_bytes");
gauge
});
pub static PROCESS_VIRTUAL_MEMORY_BYTES: Lazy<IntGauge> = Lazy::new(|| {
let gauge = IntGauge::with_opts(
Opts::new(
"lazydns_process_virtual_memory_bytes",
"Process virtual memory (VmSize) from /proc/self/status in bytes",
)
.const_label("source", "proc"),
)
.expect("Failed to create lazydns_process_virtual_memory_bytes gauge");
METRICS_REGISTRY
.register(Box::new(gauge.clone()))
.expect("Failed to register lazydns_process_virtual_memory_bytes");
gauge
});
pub static PROCESS_CGROUP_MEMORY_BYTES: Lazy<IntGauge> = Lazy::new(|| {
let gauge = IntGauge::with_opts(
Opts::new(
"lazydns_process_cgroup_memory_bytes",
"Process memory usage from cgroup (container-aware) in bytes",
)
.const_label("source", "cgroup"),
)
.expect("Failed to create lazydns_process_cgroup_memory_bytes gauge");
METRICS_REGISTRY
.register(Box::new(gauge.clone()))
.expect("Failed to register lazydns_process_cgroup_memory_bytes");
gauge
});
pub static PROCESS_CGROUP_MEMORY_LIMIT_BYTES: Lazy<IntGauge> = Lazy::new(|| {
let gauge = IntGauge::with_opts(
Opts::new(
"lazydns_process_cgroup_memory_limit_bytes",
"Process memory limit from cgroup in bytes (0 = unlimited)",
)
.const_label("source", "cgroup"),
)
.expect("Failed to create lazydns_process_cgroup_memory_limit_bytes gauge");
METRICS_REGISTRY
.register(Box::new(gauge.clone()))
.expect("Failed to register lazydns_process_cgroup_memory_limit_bytes");
gauge
});
#[derive(Debug, Clone, Copy)]
pub struct MemoryMetricsConfig {
pub enabled: bool,
pub interval_ms: u64,
}
impl Default for MemoryMetricsConfig {
fn default() -> Self {
Self {
enabled: true,
interval_ms: 5000, }
}
}
impl MemoryMetricsConfig {
pub fn with_interval(mut self, interval_ms: u64) -> Self {
self.interval_ms = interval_ms;
self
}
pub fn with_enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
}
pub fn start_memory_metrics_collector(config: MemoryMetricsConfig) -> tokio::task::JoinHandle<()> {
tokio::spawn(async move {
if !config.enabled {
debug!("Memory metrics collection is disabled");
return;
}
info!(
"Starting memory metrics collector (interval: {}ms)",
config.interval_ms
);
let cgroup_version = cgroup_reader::detect_cgroup_version();
match cgroup_version {
Some(v) => info!("Detected cgroup version: {:?}", v),
None => info!("No cgroup detected, using /proc metrics only"),
}
let mut interval = time::interval(Duration::from_millis(config.interval_ms));
interval.set_missed_tick_behavior(time::MissedTickBehavior::Skip);
loop {
interval.tick().await;
collect_memory_metrics(cgroup_version.is_some());
}
})
}
fn collect_memory_metrics(has_cgroup: bool) {
if has_cgroup {
if let Some(cgroup_stats) = cgroup_reader::read_cgroup_memory() {
PROCESS_CGROUP_MEMORY_BYTES.set(cgroup_stats.usage_bytes as i64);
if let Some(limit) = cgroup_stats.limit_bytes {
PROCESS_CGROUP_MEMORY_LIMIT_BYTES.set(limit as i64);
} else {
PROCESS_CGROUP_MEMORY_LIMIT_BYTES.set(0);
}
trace!(
"Updated cgroup memory metrics: usage={}MB, limit={}",
cgroup_stats.usage_bytes / (1024 * 1024),
cgroup_stats
.limit_bytes
.map(|l| format!("{}MB", l / (1024 * 1024)))
.unwrap_or_else(|| "unlimited".to_string())
);
} else {
warn!("Failed to read cgroup memory stats");
}
}
match proc_reader::read_proc_memory() {
Ok(proc_stats) => {
PROCESS_RESIDENT_MEMORY_BYTES.set(proc_stats.rss_bytes as i64);
PROCESS_VIRTUAL_MEMORY_BYTES.set(proc_stats.vms_bytes as i64);
trace!(
"Updated process memory metrics: RSS={}MB, VMS={}MB",
proc_stats.rss_bytes / (1024 * 1024),
proc_stats.vms_bytes / (1024 * 1024)
);
}
Err(e) => {
debug!("Failed to read process memory stats: {}", e);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_memory_metrics_config_default() {
let config = MemoryMetricsConfig::default();
assert!(config.enabled);
assert_eq!(config.interval_ms, 5000);
}
#[test]
fn test_memory_metrics_config_builder() {
let config = MemoryMetricsConfig::default()
.with_interval(10000)
.with_enabled(false);
assert!(!config.enabled);
assert_eq!(config.interval_ms, 10000);
}
#[test]
fn test_metrics_are_registered() {
let _ = &*PROCESS_RESIDENT_MEMORY_BYTES;
let _ = &*PROCESS_VIRTUAL_MEMORY_BYTES;
let _ = &*PROCESS_CGROUP_MEMORY_BYTES;
let _ = &*PROCESS_CGROUP_MEMORY_LIMIT_BYTES;
let metrics = METRICS_REGISTRY.gather();
let metric_names: Vec<_> = metrics.iter().map(|m| m.name()).collect();
assert!(metric_names.contains(&"lazydns_process_resident_memory_bytes"));
assert!(metric_names.contains(&"lazydns_process_virtual_memory_bytes"));
assert!(metric_names.contains(&"lazydns_process_cgroup_memory_bytes"));
assert!(metric_names.contains(&"lazydns_process_cgroup_memory_limit_bytes"));
}
#[cfg(target_os = "linux")]
#[test]
fn test_collect_memory_metrics_integration() {
let _ = &*PROCESS_RESIDENT_MEMORY_BYTES;
let _ = &*PROCESS_VIRTUAL_MEMORY_BYTES;
let has_cgroup = cgroup_reader::detect_cgroup_version().is_some();
collect_memory_metrics(has_cgroup);
let rss = PROCESS_RESIDENT_MEMORY_BYTES.get();
let vms = PROCESS_VIRTUAL_MEMORY_BYTES.get();
assert!(rss > 0, "RSS should be > 0");
assert!(vms > 0, "VMS should be > 0");
assert!(rss <= vms, "RSS should be <= VMS");
}
#[tokio::test]
async fn test_start_memory_metrics_collector_disabled() {
let config = MemoryMetricsConfig::default().with_enabled(false);
let handle = start_memory_metrics_collector(config);
tokio::time::sleep(Duration::from_millis(100)).await;
handle.abort();
}
#[cfg(target_os = "linux")]
#[tokio::test]
async fn test_start_memory_metrics_collector_enabled() {
let config = MemoryMetricsConfig::default()
.with_interval(100)
.with_enabled(true);
let handle = start_memory_metrics_collector(config);
tokio::time::sleep(Duration::from_millis(350)).await;
let rss = PROCESS_RESIDENT_MEMORY_BYTES.get();
assert!(rss > 0, "RSS should be updated");
handle.abort();
}
}