minmon 0.13.0

An opinionated minimal monitoring and alarming tool
Documentation
use super::DataSource;
use crate::{config, measurement};
use crate::{Error, PlaceholderMap, Result};
use async_trait::async_trait;
use measurement::Measurement;

const MEMINFO_PATH: &str = "/proc/meminfo";

const PARSE_ERROR: &str = "Could not parse meminfo file.";

pub struct MemoryUsage {
    id: Vec<String>,
    memory: bool,
    swap: bool,
}

impl TryFrom<&config::Check> for MemoryUsage {
    type Error = Error;

    fn try_from(check: &config::Check) -> std::result::Result<Self, Self::Error> {
        if let config::CheckType::MemoryUsage(memory_usage) = &check.type_ {
            if !memory_usage.memory && !memory_usage.swap {
                Err(Error(String::from(
                    "Either 'memory' or 'swap' or both need to be enabled.",
                )))
            } else {
                let mut id = Vec::new();
                if memory_usage.memory {
                    id.push(String::from("Memory"));
                }
                if memory_usage.swap {
                    id.push(String::from("Swap"));
                }
                Ok(Self {
                    id,
                    memory: memory_usage.memory,
                    swap: memory_usage.swap,
                })
            }
        } else {
            panic!();
        }
    }
}

#[async_trait]
impl DataSource for MemoryUsage {
    type Item = measurement::Level;

    async fn get_data(
        &mut self,
        _placeholders: &mut PlaceholderMap,
    ) -> Result<Vec<Result<Option<Self::Item>>>> {
        let meminfo = MeminfoFileContent::try_from_file(MEMINFO_PATH).await?;
        let mut res = Vec::new();
        if self.memory {
            res.push(if meminfo.mem_total != 0 {
                Ok(((meminfo.mem_total - meminfo.mem_available) * 100 / meminfo.mem_total) as u8)
                    .and_then(Self::Item::new)
                    .map(Some)
            } else {
                Err(Error(String::from("Could not read memory usage.")))
            });
        }
        if self.swap {
            res.push(if meminfo.swap_total != 0 {
                Ok(((meminfo.swap_total - meminfo.swap_free) * 100 / meminfo.swap_total) as u8)
                    .and_then(Self::Item::new)
                    .map(Some)
            } else {
                Err(Error(String::from("Could not read swap usage.")))
            });
        }
        Ok(res)
    }

    fn format_data(&self, data: &Self::Item) -> String {
        format!("usage level {data}")
    }

    fn ids(&self) -> &[String] {
        &self.id[..]
    }
}

struct MeminfoFileContent {
    mem_total: usize,
    mem_available: usize,
    swap_total: usize,
    swap_free: usize,
}

impl MeminfoFileContent {
    async fn try_from_file(path: &str) -> Result<Self> {
        let buffer = tokio::fs::read_to_string(path)
            .await
            .map_err(|x| Error(format!("Could not open {path} for reading: {x}")))?;
        Self::try_from(&*buffer)
    }

    fn get_number(id: &str, line: &str) -> Result<usize> {
        crate::get_number(&format!("Could not read {id} from {MEMINFO_PATH}"), line, 1)
    }
}

impl TryFrom<&str> for MeminfoFileContent {
    type Error = Error;

    fn try_from(text: &str) -> std::result::Result<Self, Self::Error> {
        let mut mem_total: Option<usize> = None;
        let mut mem_available: Option<usize> = None;
        let mut swap_total: Option<usize> = None;
        let mut swap_free: Option<usize> = None;
        for line in text.lines() {
            if line.starts_with("MemTotal") {
                mem_total = Some(Self::get_number("MemTotal", line)?);
            } else if line.starts_with("MemAvailable") {
                mem_available = Some(Self::get_number("MemAvailable", line)?);
            } else if line.starts_with("SwapTotal") {
                swap_total = Some(Self::get_number("SwapTotal", line)?);
            } else if line.starts_with("SwapFree") {
                swap_free = Some(Self::get_number("SwapFree", line)?);
            }
        }
        Ok(Self {
            mem_total: mem_total.ok_or_else(|| Error(PARSE_ERROR.to_string()))?,
            mem_available: mem_available.ok_or_else(|| Error(PARSE_ERROR.to_string()))?,
            swap_total: swap_total.ok_or_else(|| Error(PARSE_ERROR.to_string()))?,
            swap_free: swap_free.ok_or_else(|| Error(PARSE_ERROR.to_string()))?,
        })
    }
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn test_meminfo_file_content_from_str() {
        let content_str = "MemTotal:           1234 kB\n\
                           MemFree:            2345 kB\n\
                           MemAvailable:       3456 kB\n\
                           Cached:             4567 kB\n\
                           SwapTotal:          5678 kB\n\
                           SwapFree:           6789 kB";
        let content = MeminfoFileContent::try_from(content_str).unwrap();
        assert_eq!(content.mem_total, 1234);
        assert_eq!(content.mem_available, 3456);
        assert_eq!(content.swap_total, 5678);
        assert_eq!(content.swap_free, 6789);
    }
}