use nom::{
character::complete::{char, space1, u64},
sequence::{preceded, separated_pair},
IResult,
};
use std::{
fs::File,
io::{BufReader, Read},
path::Path,
};
#[cfg(test)]
use std::iter::zip;
use eyre::{eyre, Result};
use crate::metrics::{
system_metrics::SystemMetricFamilyCollector, KeyedMetricReading, MetricStringKey,
};
#[cfg(test)]
use crate::metrics::metric_reading::MetricReading;
const FILE_NR_PATH: &str = "/proc/sys/fs/file-nr";
pub const FD_METRIC_NAMESPACE: &str = "fs/handles";
pub struct FdMetricCollector;
impl FdMetricCollector {
pub fn new() -> Self {
Self
}
fn calculate_fd_metrics(&mut self, line: &str) -> Result<Vec<KeyedMetricReading>> {
let (alloc_info, max_info) = Self::parse_fdmetric(line)?;
Ok(vec![
KeyedMetricReading::new_gauge(
MetricStringKey::from("fs/handles/allocated"),
alloc_info as f64,
),
KeyedMetricReading::new_gauge(MetricStringKey::from("fs/handles/max"), max_info as f64),
])
}
pub fn get_fd_metrics(&mut self) -> Result<Vec<KeyedMetricReading>> {
let path = Path::new(FILE_NR_PATH);
let file = File::open(path)?;
let mut reader = BufReader::new(file);
let mut buf = String::new();
reader.read_to_string(&mut buf)?;
self.calculate_fd_metrics(&buf)
}
fn parse_fd_stats(input: &str) -> IResult<&str, (u64, u64)> {
separated_pair(u64, preceded(space1, char('0')), preceded(space1, u64))(input)
}
fn parse_fdmetric(line: &str) -> Result<(u64, u64)> {
let (_, (alloc_info, max_info)) = Self::parse_fd_stats(line)
.map_err(|_e| eyre!("Failed to parse FD stats line: {}", line))?;
Ok((alloc_info, max_info))
}
}
impl SystemMetricFamilyCollector for FdMetricCollector {
fn family_name(&self) -> &'static str {
FD_METRIC_NAMESPACE
}
fn collect_metrics(&mut self) -> Result<Vec<KeyedMetricReading>> {
self.get_fd_metrics()
}
}
#[cfg(test)]
mod test {
use super::*;
use rstest::rstest;
#[rstest]
#[case("7759 0 9223372036854775807", (7759, 9223372036854775807))]
#[case("1 0 2", (1, 2))]
#[case("92834570876 0 0", (92834570876, 0))]
#[case("1 0 2", (1, 2))]
#[case("1 0 2", (1, 2))]
fn test_valid_filenr_line(#[case] filenr_line: &str, #[case] expected: (u64, u64)) {
let parsed =
FdMetricCollector::parse_fdmetric(filenr_line).expect("fed valid filenr fails parsing");
assert_eq!(parsed, expected);
}
#[rstest]
#[case("1 0 ")]
#[case("92834570876 00")]
#[case("1 2 3 4")]
#[case("1 2")]
fn test_invalid_filenr_line_is_err(#[case] filenr_line: &str) {
let parsed = FdMetricCollector::parse_fdmetric(filenr_line);
assert!(parsed.is_err());
}
#[rstest]
#[case("7759 0 922",
Ok(vec![
KeyedMetricReading::new_gauge(
MetricStringKey::from("fs/file_nr/allocated"),
7759f64,
),
KeyedMetricReading::new_gauge(MetricStringKey::from("fs/file_nr/max"), 922f64),
])
)]
#[case("1 0 2",
Ok(vec![
KeyedMetricReading::new_gauge(
MetricStringKey::from("fs/file_nr/allocated"),
1f64,
),
KeyedMetricReading::new_gauge(MetricStringKey::from("fs/file_nr/max"), 2f64),
])
)]
#[case("928 0 0",
Ok(vec![
KeyedMetricReading::new_gauge(
MetricStringKey::from("fs/file_nr/allocated"),
928f64,
),
KeyedMetricReading::new_gauge(MetricStringKey::from("fs/file_nr/max"), 0f64),
])
)]
fn test_calculate_fd_metrics(
#[case] filenr_line: &str,
#[case] expected: Result<Vec<KeyedMetricReading>>,
) {
let res = FdMetricCollector::new().calculate_fd_metrics(filenr_line);
match expected {
Err(_) => assert!(res.is_err()),
Ok(vec_expected) => {
let vec_res = res.expect("result expected to also be ok");
zip(vec_expected, vec_res).for_each(|(kmr_expected, kmr_res)| {
let mr_expected = kmr_expected.value;
let mr_res = kmr_res.value;
match mr_expected {
MetricReading::Gauge {
value: val_expected,
..
} => match mr_res {
MetricReading::Gauge { value: val_res, .. } => {
assert_eq!(val_expected, val_res)
}
_ => panic!(
"expected MetricReading and result MetricReading differ variant"
),
},
_ => panic!("expected a MetricReading::Gauge, got a {:#?}", mr_expected),
}
})
}
}
}
}