minmon 0.13.0

An opinionated minimal monitoring and alarming tool
Documentation
#![deny(warnings)]

#[cfg(feature = "systemd")]
mod systemd;

use minmon::{config, Error, Result};

fn get_config_path() -> Result<std::path::PathBuf> {
    if let Some(path_str) = std::env::args().nth(1) {
        Ok(std::path::PathBuf::from(path_str))
    } else {
        Err(Error(String::from("Config path not specified.")))
    }
}

fn init_logging(config: &config::Config) -> Result<()> {
    match config.log.target {
        config::LogTarget::Stdout | config::LogTarget::Stderr => {
            let target = if config.log.target == config::LogTarget::Stdout {
                env_logger::Target::Stdout
            } else {
                env_logger::Target::Stderr
            };
            let mut builder = env_logger::Builder::from_default_env();
            builder
                .filter_level(log::LevelFilter::from(config.log.level))
                .format(|buf, record| {
                    use std::io::Write;
                    writeln!(
                        buf,
                        "{} [{}] {}",
                        buf.timestamp(),
                        record.level(),
                        record.args()
                    )
                })
                .target(target)
                .format_timestamp_secs()
                .init();
        }
        #[cfg(feature = "systemd")]
        config::LogTarget::Journal => {
            systemd::init_journal()?;
            log::set_max_level(log::LevelFilter::from(config.log.level));
        }
    }
    Ok(())
}

async fn random_interval(max: std::time::Duration) {
    let delay = rand::random::<f32>() * max.as_secs_f32() + 0.001;
    let mut delay = tokio::time::interval(std::time::Duration::from_secs_f32(delay));
    delay.tick().await; // the first tick completes immediately
    delay.tick().await;
}

async fn main_wrapper() -> Result<()> {
    minmon::uptime::init()?;

    let config_path = get_config_path()?;
    let config = config::Config::try_from(config_path.as_path())
        .map_err(|x| Error(format!("Failed to parse config:\n{x}")))?;

    init_logging(&config)?;

    const VERSION: &str = env!("CARGO_PKG_VERSION");

    log::info!("Starting MinMon v{VERSION}..");

    #[cfg(feature = "systemd")]
    {
        systemd::init().await;
    }

    minmon::init_env_vars(&config);

    let (report, checks) = minmon::from_config(&config)?;

    if let Some(start_delay) = minmon::start_delay(&config) {
        log::info!(
            "Waiting {} seconds before starting..",
            start_delay.as_secs()
        );
        tokio::time::sleep(start_delay).await;
    }

    for mut check in checks {
        tokio::spawn(async move {
            random_interval(check.interval()).await;
            let mut interval = tokio::time::interval(check.interval());
            loop {
                interval.tick().await;
                check.trigger().await;
            }
        });
    }

    if let Some(mut report) = report {
        tokio::spawn(async move {
            match report.when.clone() {
                minmon::ReportWhen::Interval(interval) => {
                    let mut interval = tokio::time::interval(interval);
                    loop {
                        interval.tick().await;
                        report.trigger().await;
                    }
                }
                minmon::ReportWhen::Cron(schedule) => {
                    report.trigger().await;
                    for datetime in schedule.upcoming(chrono::Utc) {
                        // here we split long sleep durations into smaller ones to compensate for
                        // clock drift and system standby/hibernation
                        let mut duration = datetime.signed_duration_since(chrono::Utc::now());
                        while duration > chrono::TimeDelta::minutes(10) {
                            tokio::time::sleep(std::time::Duration::from_secs(9 * 60)).await;
                            duration = datetime.signed_duration_since(chrono::Utc::now());
                        }
                        tokio::time::sleep(duration.to_std().unwrap()).await;
                        report.trigger().await;
                    }
                }
            }
        });
    }

    use tokio::signal::unix::{signal, SignalKind};
    let mut sigint = signal(SignalKind::interrupt()).unwrap();
    let mut sigterm = signal(SignalKind::interrupt()).unwrap();

    tokio::select! {
        _ = sigint.recv() => log::info!("Received signal SIGINT. Shutting down."),
        _ = sigterm.recv() => log::info!("Received signal SIGTERM. Shutting down."),
    }

    Ok(())
}

#[tokio::main(flavor = "current_thread")]
async fn main() {
    if let Err(error) = main_wrapper().await {
        log::error!("Exiting due to error: {error}");
        // Also print to stderr here because logging might not be initialized if the config
        // cannot be parsed.
        eprintln!("Exiting due to error: {error}");
        std::process::exit(1);
    }
}