minmon/
lib.rs

1#![deny(warnings)]
2#![allow(clippy::too_many_arguments, clippy::large_enum_variant)]
3#[cfg(not(target_os = "linux"))]
4compile_error!("Only Linux is supported");
5
6mod action;
7mod alarm;
8mod check;
9pub mod config;
10mod filter;
11mod measurement;
12mod process;
13mod report;
14pub mod uptime;
15mod window_buffer;
16
17pub type Result<T> = std::result::Result<T, Error>;
18type PlaceholderMap = std::collections::HashMap<String, String>;
19type ActionMap = std::collections::HashMap<String, std::sync::Arc<dyn action::Action>>;
20pub type ReportWhen = report::ReportWhen;
21
22pub fn user_agent() -> String {
23    format!("MinMon/v{}", env!("CARGO_PKG_VERSION"))
24}
25
26#[derive(Debug, Clone)]
27pub struct Error(pub String);
28impl std::error::Error for Error {}
29
30impl std::fmt::Display for Error {
31    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
32        write!(f, "{}", self.0)
33    }
34}
35
36static ENV_VARS: std::sync::OnceLock<std::collections::HashMap<String, String>> =
37    std::sync::OnceLock::new();
38
39pub fn init_env_vars(config: &config::Config) {
40    let mut env_vars = PlaceholderMap::new();
41    for (name, value) in std::env::vars() {
42        if name.starts_with(&config.general.env_var_prefix) {
43            env_vars.insert(format!("env:{name}"), value);
44        }
45    }
46    ENV_VARS.set(env_vars).unwrap();
47}
48
49fn global_placeholders() -> PlaceholderMap {
50    let mut res = ENV_VARS.get().unwrap().clone();
51    let system_uptime = uptime::system();
52    let minmon_uptime = uptime::process();
53    res.insert(
54        String::from("system_uptime"),
55        system_uptime.as_secs().to_string(),
56    );
57    res.insert(
58        String::from("system_uptime_iso"),
59        duration_iso8601(system_uptime),
60    );
61    res.insert(
62        String::from("minmon_uptime"),
63        minmon_uptime.as_secs().to_string(),
64    );
65    res.insert(
66        String::from("minmon_uptime_iso"),
67        duration_iso8601(minmon_uptime),
68    );
69    res
70}
71
72fn merge_placeholders(target: &mut PlaceholderMap, source: &PlaceholderMap) {
73    for (key, value) in source.iter() {
74        target.insert(key.clone(), value.clone());
75    }
76}
77
78fn fill_placeholders(template: &str, placeholders: &PlaceholderMap) -> String {
79    let template = text_placeholder::Template::new(template);
80    template.fill_with_hashmap(
81        &placeholders
82            .iter()
83            .map(|(k, v)| (k.as_str(), v.as_str()))
84            .collect(),
85    )
86}
87
88fn datetime_iso8601(system_time: std::time::SystemTime) -> String {
89    let date_time: chrono::DateTime<chrono::Utc> = system_time.into();
90    date_time.format("%FT%TZ").to_string()
91}
92
93// only up to "days" because the number of days in a month/year is not defined in the standard
94fn duration_iso8601(duration: std::time::Duration) -> String {
95    const SECONDS_PER_MINUTE: u64 = 60;
96    const MINUTES_PER_HOUR: u64 = 60;
97    const HOURS_PER_DAY: u64 = 24;
98    const SECONDS_PER_HOUR: u64 = SECONDS_PER_MINUTE * MINUTES_PER_HOUR;
99    const SECONDS_PER_DAY: u64 = SECONDS_PER_HOUR * HOURS_PER_DAY;
100    let mut remainder = duration.as_secs();
101    if remainder == 0 {
102        return String::from("PT0S");
103    }
104    let mut res = String::new();
105    let days = remainder / SECONDS_PER_DAY;
106    if days > 0 {
107        res = format!("P{days}D");
108    }
109    remainder %= SECONDS_PER_DAY;
110    if remainder == 0 {
111        return res;
112    }
113    res.push('T');
114    let hours = remainder / SECONDS_PER_HOUR;
115    if hours > 0 {
116        res = format!("{res}{hours}H");
117    }
118    remainder %= SECONDS_PER_HOUR;
119    if remainder == 0 {
120        return res;
121    }
122    let minutes = remainder / SECONDS_PER_MINUTE;
123    if minutes > 0 {
124        res = format!("{res}{minutes}M");
125    }
126    remainder %= SECONDS_PER_MINUTE;
127    if remainder > 0 {
128        res = format!("{res}{remainder}S");
129    }
130    res
131}
132
133fn init_actions(config: &config::Config) -> Result<ActionMap> {
134    log::info!("Initializing {} actions(s)..", config.actions.len());
135    let mut res = ActionMap::new();
136    for action_config in config.actions.iter() {
137        if res.contains_key(&action_config.name) {
138            return Err(Error(format!(
139                "Found duplicate action name: {}",
140                action_config.name
141            )));
142        }
143        let action = action::from_action_config(action_config)?;
144        res.insert(action_config.name.clone(), action);
145        log::info!("Action '{}' initialized.", action_config.name);
146    }
147    Ok(res)
148}
149
150fn init_report(config: &config::Config, actions: &ActionMap) -> Result<Option<report::Report>> {
151    log::info!("Initializing report..");
152    let report_config = &config.report;
153    if report_config.disable {
154        log::info!("Report is disabled.");
155        return Ok(None);
156    }
157    let report = report::from_report_config(report_config, actions)?;
158    match &report.when {
159        report::ReportWhen::Interval(interval) => {
160            log::info!(
161                "Report will be triggered every {} seconds.",
162                interval.as_secs()
163            )
164        }
165        report::ReportWhen::Cron(schedule) => {
166            log::info!("Report will be triggered by cron schedule '{schedule}'.")
167        }
168    }
169    Ok(Some(report))
170}
171
172fn init_checks(config: &config::Config, actions: &ActionMap) -> Result<Vec<Box<dyn check::Check>>> {
173    log::info!("Initializing {} check(s)..", config.checks.len());
174    let mut res: Vec<Box<dyn check::Check>> = Vec::new();
175    let mut used_names = std::collections::HashSet::new();
176    for check_config in config.checks.iter() {
177        if !used_names.insert(check_config.name.clone()) {
178            return Err(Error(format!(
179                "Found duplicate check name: {}",
180                check_config.name
181            )));
182        }
183        if check_config.disable {
184            log::info!("Check '{}' is disabled.", check_config.name);
185            continue;
186        }
187        let check = check::from_check_config(check_config, actions)?;
188        log::info!(
189            "Check '{}' will be triggered every {} seconds.",
190            check.name(),
191            check.interval().as_secs()
192        );
193        res.push(check);
194    }
195    Ok(res)
196}
197
198type ConfigState = (Option<report::Report>, Vec<Box<dyn check::Check>>);
199
200pub fn from_config(config: &config::Config) -> Result<ConfigState> {
201    let actions = init_actions(config)?;
202    let report = init_report(config, &actions)?;
203    let checks = init_checks(config, &actions)?;
204    Ok((report, checks))
205}
206
207pub fn start_delay(config: &config::Config) -> Option<std::time::Duration> {
208    let uptime = uptime::system();
209    let boot_delay = config
210        .general
211        .boot_delay
212        .map(|x| std::time::Duration::from_secs(x.into()))
213        .filter(|x| x > &uptime)
214        .map(|x| x - uptime);
215    let start_delay = config
216        .general
217        .start_delay
218        .map(|x| std::time::Duration::from_secs(x.into()));
219
220    boot_delay.map_or(start_delay, |x| {
221        start_delay.map_or(boot_delay, |y| Some(x.max(y)))
222    })
223}
224
225fn get_number<T>(error_message: &str, line: &str, column: usize) -> Result<T>
226where
227    T: std::str::FromStr,
228    <T as std::str::FromStr>::Err: std::fmt::Display,
229{
230    {
231        line.split_whitespace()
232            .nth(column)
233            .ok_or_else(|| Error(String::from("Column not found.")))?
234            .parse()
235            .map_err(|x| Error(format!("{x}")))
236    }
237    .map_err(|x| Error(format!("{error_message}: {x}")))
238}
239
240#[cfg(test)]
241mod test {
242    use super::*;
243
244    #[test]
245    fn test_merge_placeholders() {
246        let mut target = PlaceholderMap::from([(String::from("A"), String::from("?"))]);
247        let source = PlaceholderMap::from([
248            (String::from("A"), String::from("B")),
249            (String::from("C"), String::from("D")),
250        ]);
251        merge_placeholders(&mut target, &source);
252        assert_eq!(target.get("A").unwrap(), "B");
253        assert_eq!(target.get("C").unwrap(), "D");
254    }
255
256    #[test]
257    fn test_fill_placeholders() {
258        let template = "X{{A}}{{missing}}Z";
259        let placeholders = PlaceholderMap::from([(String::from("A"), String::from("Y"))]);
260        let filled = fill_placeholders(template, &placeholders);
261        assert_eq!(filled, "XYZ");
262    }
263
264    #[test]
265    fn test_datetime_iso8601() {
266        let system_time = std::time::SystemTime::UNIX_EPOCH;
267        assert_eq!(datetime_iso8601(system_time), "1970-01-01T00:00:00Z");
268    }
269
270    #[test]
271    fn test_duration_iso8601() {
272        let duration = std::time::Duration::from_secs(123630);
273        assert_eq!(duration_iso8601(duration), "P1DT10H20M30S");
274        let duration = std::time::Duration::from_secs(37230);
275        assert_eq!(duration_iso8601(duration), "T10H20M30S");
276        let duration = std::time::Duration::from_secs(0);
277        assert_eq!(duration_iso8601(duration), "PT0S");
278    }
279
280    #[test]
281    fn test_get_number() {
282        let line = "0 1 2 3 4 5";
283        assert_eq!(get_number::<u32>("error", line, 0).unwrap(), 0);
284        assert_eq!(get_number::<u32>("error", line, 5).unwrap(), 5);
285        assert!(get_number::<u32>("error", line, 6).is_err());
286    }
287}