use std::collections::HashMap;
use std::fs::read_to_string;
use std::path::Path;
use eyre::{eyre, Result};
use nom::{
bytes::complete::tag,
character::complete::{alpha1, multispace1},
number::complete::double,
sequence::{delimited, terminated},
IResult,
};
use crate::metrics::{
system_metrics::SystemMetricFamilyCollector, KeyedMetricReading, MetricStringKey,
};
pub const PROC_MEMINFO_PATH: &str = "/proc/meminfo";
pub const MEMORY_METRIC_NAMESPACE: &str = "memory";
#[cfg_attr(test, mockall::automock)]
pub trait MemInfoParser {
fn get_meminfo_stats(&self) -> Result<HashMap<String, f64>>;
}
pub struct MemInfoParserImpl;
impl MemInfoParserImpl {
pub fn new() -> Self {
Self {}
}
fn parse_meminfo_key(meminfo_line: &str) -> IResult<&str, &str> {
terminated(alpha1, tag(":"))(meminfo_line)
}
fn parse_meminfo_kb(meminfo_line_suffix: &str) -> IResult<&str, f64> {
delimited(multispace1, double, tag(" kB"))(meminfo_line_suffix)
}
fn parse_meminfo_stats(meminfo: &str) -> HashMap<String, f64> {
meminfo
.trim()
.lines()
.map(|line| -> Result<(String, f64), String> {
let (suffix, key) = Self::parse_meminfo_key(line).map_err(|e| e.to_string())?;
let (_, kb) = Self::parse_meminfo_kb(suffix).map_err(|e| e.to_string())?;
Ok((key.to_string(), kb * 1024.0))
})
.filter_map(|result| result.ok())
.collect()
}
}
impl MemInfoParser for MemInfoParserImpl {
fn get_meminfo_stats(&self) -> Result<HashMap<String, f64>> {
let path = Path::new(PROC_MEMINFO_PATH);
let contents = read_to_string(path)?;
Ok(MemInfoParserImpl::parse_meminfo_stats(&contents))
}
}
pub struct MemoryMetricsCollector<T: MemInfoParser> {
mem_info_parser: T,
}
impl<T> MemoryMetricsCollector<T>
where
T: MemInfoParser,
{
pub fn new(mem_info_parser: T) -> Self {
MemoryMetricsCollector { mem_info_parser }
}
fn get_memory_metrics(&self) -> Result<Vec<KeyedMetricReading>> {
let mut stats = self.mem_info_parser.get_meminfo_stats()?;
let total = stats
.remove("MemTotal")
.ok_or_else(|| eyre!("{} is missing required value MemTotal", PROC_MEMINFO_PATH))?;
let free = stats
.remove("MemFree")
.ok_or_else(|| eyre!("{} is missing required value MemFree", PROC_MEMINFO_PATH))?;
let slab_unreclaimable = stats
.remove("SUnreclaim")
.ok_or_else(|| eyre!("{} is missing required value SUnreclaim", PROC_MEMINFO_PATH))?;
let slab_reclaimable = stats.remove("SReclaimable").ok_or_else(|| {
eyre!(
"{} is missing required value SReclaimable",
PROC_MEMINFO_PATH
)
})?;
let cached = stats
.remove("Cached")
.ok_or_else(|| eyre!("{} is missing required value Cached", PROC_MEMINFO_PATH))?;
let buffered = stats
.remove("Buffers")
.ok_or_else(|| eyre!("{} is missing required value Buffers", PROC_MEMINFO_PATH))?;
if total != 0.0 {
let available = stats.remove("MemAvailable").unwrap_or(free);
let used = total - available;
let _pct_used = (used / total) * 100.0;
Ok(vec![
KeyedMetricReading::new_histogram(
MetricStringKey::from("memory/memory/free"),
free,
),
KeyedMetricReading::new_histogram(
MetricStringKey::from("memory/memory/used"),
used,
),
KeyedMetricReading::new_histogram(
MetricStringKey::from("memory/memory/slab_recl"),
slab_reclaimable,
),
KeyedMetricReading::new_histogram(
MetricStringKey::from("memory/memory/slab_unrecl"),
slab_unreclaimable,
),
KeyedMetricReading::new_histogram(
MetricStringKey::from("memory/memory/buffered"),
buffered,
),
KeyedMetricReading::new_histogram(
MetricStringKey::from("memory/memory/cached"),
cached,
),
])
} else {
Err(eyre!("MemTotal is 0, can't calculate memory usage metrics"))
}
}
}
impl<T> SystemMetricFamilyCollector for MemoryMetricsCollector<T>
where
T: MemInfoParser,
{
fn collect_metrics(&mut self) -> Result<Vec<KeyedMetricReading>> {
self.get_memory_metrics()
}
fn family_name(&self) -> &'static str {
MEMORY_METRIC_NAMESPACE
}
}
#[cfg(test)]
mod test {
use insta::{assert_json_snapshot, rounded_redaction, with_settings};
use rstest::rstest;
use super::*;
#[rstest]
#[case("MemTotal: 365916 kB", "MemTotal", 365916.0)]
#[case("MemFree: 242276 kB", "MemFree", 242276.0)]
#[case("MemAvailable: 292088 kB", "MemAvailable", 292088.0)]
#[case("Buffers: 4544 kB", "Buffers", 4544.0)]
#[case("Cached: 52128 kB", "Cached", 52128.0)]
fn test_parse_meminfo_line(
#[case] proc_meminfo_line: &str,
#[case] expected_key: &str,
#[case] expected_value: f64,
) {
let (suffix, key) = MemInfoParserImpl::parse_meminfo_key(proc_meminfo_line).unwrap();
let (_, kb) = MemInfoParserImpl::parse_meminfo_kb(suffix).unwrap();
assert_eq!(key, expected_key);
assert_eq!(kb, expected_value);
}
#[rstest]
fn test_get_memory_metrics() {
let meminfo = "MemTotal: 365916 kB
MemFree: 242276 kB
MemAvailable: 292088 kB
Buffers: 4544 kB
Cached: 52128 kB
SwapCached: 0 kB
Active: 21668 kB
Inactive: 51404 kB
Active(anon): 2312 kB
Inactive(anon): 25364 kB
Active(file): 19356 kB
Inactive(file): 26040 kB
Unevictable: 3072 kB
Mlocked: 0 kB
SwapTotal: 0 kB
SwapFree: 0 kB
Dirty: 0 kB
Writeback: 0 kB
AnonPages: 19488 kB
Mapped: 29668 kB
Shmem: 11264 kB
KReclaimable: 14028 kB
Slab: 33420 kB
SReclaimable: 14912 kB
SUnreclaim: 18508 kB
KernelStack: 1904 kB
";
with_settings!({sort_maps => true}, {
assert_json_snapshot!(
MemInfoParserImpl::parse_meminfo_stats(meminfo),
{"[].value.**.timestamp" => "[timestamp]", "[].value.**.value" => rounded_redaction(5)})
});
}
#[rstest]
fn test_get_memory_metrics_no_memavailable() {
let meminfo = "MemTotal: 365916 kB
MemFree: 242276 kB
Buffers: 4544 kB
Cached: 52128 kB
SwapCached: 0 kB
Active: 21668 kB
Inactive: 51404 kB
Active(anon): 2312 kB
Inactive(anon): 25364 kB
Active(file): 19356 kB
Inactive(file): 26040 kB
Unevictable: 3072 kB
Mlocked: 0 kB
SwapTotal: 0 kB
SwapFree: 0 kB
Dirty: 0 kB
Writeback: 0 kB
AnonPages: 19488 kB
Mapped: 29668 kB
Shmem: 11264 kB
KReclaimable: 14028 kB
Slab: 33420 kB
SReclaimable: 14912 kB
SUnreclaim: 18508 kB
KernelStack: 1904 kB
";
let mut mock_meminfo_parser = MockMemInfoParser::new();
mock_meminfo_parser
.expect_get_meminfo_stats()
.times(1)
.returning(|| Ok(MemInfoParserImpl::parse_meminfo_stats(meminfo)));
let memory_metrics_collector = MemoryMetricsCollector::new(mock_meminfo_parser);
with_settings!({sort_maps => true}, {
assert_json_snapshot!(
memory_metrics_collector.get_memory_metrics().unwrap(),
{"[].value.**.timestamp" => "[timestamp]", "[].value.**.value" => rounded_redaction(5)})
});
}
#[rstest]
fn test_fail_to_parse_bad_meminfo_line() {
assert!(MemInfoParserImpl::parse_meminfo_key("MemFree=1080kB").is_err());
assert!(MemInfoParserImpl::parse_meminfo_kb("1080 mB").is_err());
}
#[rstest]
fn test_fail_get_metrics_with_missing_required_lines() {
let meminfo = "MemTotal: 365916 kB
MemAvailable: 292088 kB
Buffers: 4544 kB
Cached: 52128 kB
SwapCached: 0 kB
Active: 21668 kB
Inactive: 51404 kB
Active(anon): 2312 kB
Inactive(anon): 25364 kB
Active(file): 19356 kB
Inactive(file): 26040 kB
Unevictable: 3072 kB
Mlocked: 0 kB
SwapTotal: 0 kB
SwapFree: 0 kB
Dirty: 0 kB
Writeback: 0 kB
AnonPages: 19488 kB
Mapped: 29668 kB
Shmem: 11264 kB
KReclaimable: 14028 kB
Slab: 33420 kB
SReclaimable: 14912 kB
SUnreclaim: 18508 kB
KernelStack: 1904 kB
";
let mut mock_meminfo_parser = MockMemInfoParser::new();
mock_meminfo_parser
.expect_get_meminfo_stats()
.times(1)
.returning(|| Ok(MemInfoParserImpl::parse_meminfo_stats(meminfo)));
let memory_metrics_collector = MemoryMetricsCollector::new(mock_meminfo_parser);
assert!(memory_metrics_collector.get_memory_metrics().is_err())
}
#[rstest]
fn test_fail_get_metrics_with_bad_fmt() {
let meminfo = "MemTotal: 365916 kB MemFree: 242276 kB
Buffers: 4544 kB Cached: 52128 kB Shmem: 11264 kB
";
let mut mock_meminfo_parser = MockMemInfoParser::new();
mock_meminfo_parser
.expect_get_meminfo_stats()
.times(1)
.returning(|| Ok(MemInfoParserImpl::parse_meminfo_stats(meminfo)));
let memory_metrics_collector = MemoryMetricsCollector::new(mock_meminfo_parser);
assert!(memory_metrics_collector.get_memory_metrics().is_err())
}
#[rstest]
fn test_fail_get_metrics_when_mem_total_is_zero() {
let meminfo = "MemTotal: 0 kB
MemAvailable: 292088 kB
Buffers: 4544 kB
Cached: 52128 kB
SwapCached: 0 kB
Active: 21668 kB
Inactive: 51404 kB
Active(anon): 2312 kB
Inactive(anon): 25364 kB
Active(file): 19356 kB
Inactive(file): 26040 kB
Unevictable: 3072 kB
Mlocked: 0 kB
SwapTotal: 0 kB
SwapFree: 0 kB
Dirty: 0 kB
Writeback: 0 kB
AnonPages: 19488 kB
Mapped: 29668 kB
Shmem: 11264 kB
KReclaimable: 14028 kB
Slab: 33420 kB
SReclaimable: 14912 kB
SUnreclaim: 18508 kB
KernelStack: 1904 kB
";
let mut mock_meminfo_parser = MockMemInfoParser::new();
mock_meminfo_parser
.expect_get_meminfo_stats()
.times(1)
.returning(|| Ok(MemInfoParserImpl::parse_meminfo_stats(meminfo)));
let memory_metrics_collector = MemoryMetricsCollector::new(mock_meminfo_parser);
assert!(memory_metrics_collector.get_memory_metrics().is_err())
}
}