limon-core 0.3.3

limon core library
Documentation
use time::OffsetDateTime;

use crate::collectors::monitor::errors::CollectorError;
use crate::collectors::monitor::{Http, Ping};
use crate::models::monitor::config::Config;
use crate::models::monitor::measurement::{Data, Measurement};
use crate::schedule::Schedulable;

/// Represents a monitor for a host, which can be measured.
#[derive(Debug)]
pub struct Monitor {
  /// Monitor identifier.
  pub id: i64,

  /// Host without protocol specified.
  pub host: String,

  /// Monitor's config.
  pub config: Config,
}

impl Monitor {
  /// Performs a measurement for this monitor asynchronously.
  ///
  /// The exact behavior depends on the type of configuration (`self.config`):
  ///
  /// - **`Config::Ping`** – Sends a network ping to the monitor's host using
  ///   the settings in the Ping configuration.
  /// - **`Config::Http`** – Performs an HTTP request to the monitor's host
  ///   using the parameters in [`HttpConfig`](crate::models::monitor::HttpConfig),
  ///   such as method, path, timeout, expected status code, and follow redirects.
  ///
  /// The returned [`Measurement`] includes:
  /// - [`data`](Measurement#structfield.data): containing the collected
  ///   measurement if successful.
  /// - [`error`](Measurement#structfield.error): containing any error
  ///   that occurred during the measurement.
  pub async fn measure(&self) -> Measurement {
    let mut measure = Measurement {
      timestamp: OffsetDateTime::now_utc(),
      monitor_id: self.id,
      data: None,
      error: None,
    };

    let result: Result<Data, CollectorError> = match &self.config {
      #[cfg(not(tarpaulin_include))]
      // This branch is excluded from code coverage (`tarpaulin_include`) because
      // raw sockets are required for performing ICMP (ping) measurements.
      // Such operations usually cannot be executed in test environments, since
      // they require elevated privileges or special OS-level capabilities.
      Config::Ping(config) => Ping::measure(&self.host, config)
        .await
        .map_err(|error| error.into()),
      Config::Http(config) => Http::measure(&self.host, config)
        .await
        .map_err(|error| error.into()),
    };

    if result.is_ok() {
      measure.data = result.ok();
    } else {
      measure.error = result.err();
    }

    measure
  }
}

/// Trait implementation for scheduling monitors.
impl Schedulable for Monitor {
  type Id = i64;
  type Interval = i64;

  fn get_id(&self) -> Self::Id {
    self.id
  }

  fn get_interval(&self) -> Self::Interval {
    match &self.config {
      Config::Ping(config) => config.check_frequency,
      Config::Http(config) => config.check_frequency,
    }
  }
}

#[cfg(test)]
mod tests {
  use httpmock::Method::GET;
  use httpmock::MockServer;
  use rstest::rstest;

  use super::*;
  use crate::models::monitor::config::{Header, HttpConfig, PingConfig};

  #[rstest]
  #[case(Config::Ping(PingConfig { check_frequency: 10, ..Default::default() }))]
  #[case(Config::Http(HttpConfig { check_frequency: 10, ..Default::default() }))]
  fn monitor_is_schedulable(#[case] config: Config) {
    let monitor = Monitor {
      id: 1,
      host: String::from("test"),
      config,
    };

    assert_eq!(monitor.get_id(), 1, "monitor id is correct");
    assert_eq!(monitor.get_interval(), 10, "monitor interval is correct");
  }

  #[tokio::test]
  async fn measure_http_with_data() {
    let server = MockServer::start_async().await;

    let mock = server
      .mock_async(|when, then| {
        when
          .header("Authorization", "token")
          .method(GET)
          .path("/check");
        then.status(200).body("index");
      })
      .await;

    let monitor = Monitor {
      id: 1,
      host: format!("{}:{}", &server.host(), &server.port()),
      config: Config::Http(HttpConfig {
        timeout: 3,
        method: String::from("GET"),
        protocol: String::from("HTTP"),
        path: Some(String::from("/check")),
        headers: Some(vec![Header {
          name: String::from("Authorization"),
          value: String::from("token"),
        }]),
        expected_status_code: 200,
        keyword: Some(String::from("index")),
        ..Default::default()
      }),
    };

    let result = monitor.measure().await;

    mock.assert();

    assert!(
      result.data.is_some() && result.error.is_none(),
      "monitor measurement has data"
    );
  }

  #[tokio::test]
  async fn measure_http_with_error() {
    let server = MockServer::start_async().await;

    let mock = server
      .mock_async(|when, then| {
        when.method(GET).path("/check");
        then.status(400);
      })
      .await;

    let monitor = Monitor {
      id: 1,
      host: format!("{}:{}", &server.host(), &server.port()),
      config: Config::Http(HttpConfig {
        timeout: 3,
        method: String::from("GET"),
        protocol: String::from("HTTP"),
        path: Some(String::from("/check")),
        expected_status_code: 200,
        ..Default::default()
      }),
    };

    let result = monitor.measure().await;

    mock.assert();

    assert!(
      result.data.is_none() && result.error.is_some(),
      "monitor measurement has error"
    );
  }
}