minmon 0.13.0

An opinionated minimal monitoring and alarming tool
Documentation
use crate::config;
use crate::ActionMap;
use crate::{Error, PlaceholderMap, Result};
use async_trait::async_trait;
extern crate log as log_ext;

#[cfg(feature = "smtp")]
mod email;
mod log;
mod process;
#[cfg(feature = "http")]
mod webhook;
pub use self::log::Log;
#[cfg(feature = "smtp")]
pub use email::Email;
pub use process::Process;
#[cfg(feature = "http")]
pub use webhook::Webhook;

#[cfg_attr(test, mockall::automock)]
#[async_trait]
pub trait Action: Send + Sync {
    async fn trigger(&self, mut placeholders: PlaceholderMap) -> Result<()>;
}

pub struct ActionBase<T>
where
    T: Action,
{
    name: String,
    timeout: std::time::Duration,
    placeholders: PlaceholderMap,
    action: T,
}

impl<T> ActionBase<T>
where
    T: Action,
{
    pub fn new(
        name: String,
        timeout: std::time::Duration,
        placeholders: PlaceholderMap,
        action: T,
    ) -> Result<Self> {
        if name.is_empty() {
            Err(Error(String::from("'name' cannot be empty.")))
        } else if timeout.is_zero() {
            Err(Error(String::from("'timeout' cannot be 0.")))
        } else {
            Ok(Self {
                name,
                timeout,
                placeholders,
                action,
            })
        }
    }

    fn add_placeholders(&self, placeholders: &mut PlaceholderMap) {
        placeholders.insert(String::from("action_name"), self.name.clone());
        crate::merge_placeholders(placeholders, &self.placeholders);
    }
}

#[async_trait]
impl<T> Action for ActionBase<T>
where
    T: Action,
{
    async fn trigger(&self, mut placeholders: PlaceholderMap) -> Result<()> {
        self.add_placeholders(&mut placeholders);
        if placeholders.contains_key("event_name") {
            log_ext::info!(
                "Action '{}' triggered for report event '{}'.",
                placeholders.get("action_name").unwrap(),
                placeholders.get("event_name").unwrap()
            );
        } else {
            log_ext::info!(
                "Action '{}' triggered for alarm '{}', id '{}' from check '{}'.",
                placeholders.get("action_name").unwrap(),
                placeholders.get("alarm_name").unwrap(),
                placeholders.get("check_id").unwrap(),
                placeholders.get("check_name").unwrap()
            );
        }
        let res = tokio::time::timeout(self.timeout, self.action.trigger(placeholders)).await;
        match res {
            Ok(inner) => inner,
            Err(_) => Err(Error(format!(
                "Action '{}' timed out after {} seconds.",
                self.name,
                self.timeout.as_secs()
            ))),
        }
    }
}

struct DisabledAction {}

#[async_trait]
impl Action for DisabledAction {
    async fn trigger(&self, placeholders: PlaceholderMap) -> Result<()> {
        if placeholders.contains_key("event_name") {
            log_ext::debug!(
                "Disabled action '{}' triggered for report event '{}'.",
                placeholders.get("action_name").unwrap(),
                placeholders.get("event_name").unwrap()
            );
        } else {
            log_ext::debug!(
                "Disabled action '{}' triggered for alarm '{}', id '{}' from check '{}'.",
                placeholders.get("action_name").unwrap(),
                placeholders.get("alarm_name").unwrap(),
                placeholders.get("check_id").unwrap(),
                placeholders.get("check_name").unwrap()
            );
        }
        Ok(())
    }
}

pub fn from_action_config(action_config: &config::Action) -> Result<std::sync::Arc<dyn Action>> {
    macro_rules! create_action {
        ($action:expr) => {
            Ok(std::sync::Arc::new(ActionBase::new(
                action_config.name.clone(),
                std::time::Duration::from_secs(action_config.timeout as u64),
                action_config.placeholders.clone(),
                $action,
            )?) as std::sync::Arc<dyn Action>)
        };
    }

    if action_config.disable {
        log_ext::info!("Action '{}' is disabled.", action_config.name);
        create_action!(DisabledAction {})
    } else {
        match &action_config.type_ {
            #[cfg(feature = "smtp")]
            config::ActionType::Email(_) => create_action!(Email::try_from(action_config)?),
            config::ActionType::Log(_) => create_action!(Log::try_from(action_config)?),
            config::ActionType::Process(_) => create_action!(Process::try_from(action_config)?),
            #[cfg(feature = "http")]
            config::ActionType::Webhook(_) => create_action!(Webhook::try_from(action_config)?),
        }
    }
}

pub fn get_action(action: &str, actions: &ActionMap) -> Result<std::sync::Arc<dyn Action>> {
    if action.is_empty() {
        Err(Error(String::from("'name' cannot be empty.")))
    } else {
        Ok(actions
            .get(action)
            .ok_or_else(|| Error(format!("Action '{action}' not found.")))?
            .clone())
    }
}

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

    #[tokio::test]
    async fn test_placeholders() {
        let mut mock_action = MockAction::new();
        mock_action
            .expect_trigger()
            .once()
            .with(eq(PlaceholderMap::from([
                (String::from("action_name"), String::from("Name")),
                (String::from("Hello"), String::from("World")),
                (String::from("Foo"), String::from("Bar")),
            ])))
            .returning(|_| Ok(()));
        let action = ActionBase::new(
            String::from("Name"),
            std::time::Duration::from_secs(1),
            PlaceholderMap::from([(String::from("Hello"), String::from("World"))]),
            mock_action,
        )
        .unwrap();
        action
            .trigger(PlaceholderMap::from([(
                String::from("Foo"),
                String::from("Bar"),
            )]))
            .await
            .unwrap();
    }

    #[tokio::test]
    async fn test_timeout() {
        struct TimeoutMockAction {}
        #[async_trait]
        impl Action for TimeoutMockAction {
            async fn trigger(&self, mut _placeholders: PlaceholderMap) -> Result<()> {
                tokio::time::sleep(std::time::Duration::from_secs(2)).await;
                Ok(())
            }
        }
        let action = ActionBase::new(
            String::from("Name"),
            std::time::Duration::from_secs(1),
            PlaceholderMap::new(),
            TimeoutMockAction {},
        )
        .unwrap();
        assert!(action.trigger(PlaceholderMap::new()).await.is_err());
    }
}