watchbird_api 0.2.2-rc.0

A self-hosted and API-driven uptime monitor
Documentation
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{
    fmt::{Debug, Display},
    num::NonZeroU32,
    str::FromStr,
};
use url;
pub use url::Url;
pub use uuid::Uuid;

/// A probe is an individual test that is executed as part of a watch.
/// Each probe belongs to exactly one watch and a watch may have one or more probes.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct Probe {
    /// Optional description for documentation purposes only
    #[serde(skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,
    /// The probe to execute
    pub check: Check,
    /// Optionally override the number of seconds to wait between probe execution from the server default
    #[serde(skip_serializing_if = "Option::is_none")]
    pub interval_secs: Option<NonZeroU32>,
    /// Optionally override the seconds to wait before considering a probe failed from the server default
    #[serde(skip_serializing_if = "Option::is_none")]
    pub timeout_secs: Option<NonZeroU32>,
    /// Optionally override how many times the probes task should be retried from the server default.
    /// Set to 0 to disable retries.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub retries: Option<u8>,
    /// Optionally override the time to wait between retries in seconds from the server default.
    /// Each subsequent failure will double the backoff duration (i.e. 5s => 10s => 20s...)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub backoff_secs: Option<u32>,
}
impl Probe {
    pub fn new(check: Check) -> Probe {
        Probe {
            description: None,
            check,
            interval_secs: None,
            timeout_secs: None,
            retries: None,
            backoff_secs: None,
        }
    }
}
impl Display for Probe {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "{}",
            self.description.as_ref().unwrap_or(&self.check.to_string())
        )
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub enum Check {
    //Tcp(TcpProbe),
    /// Make a HTTP request
    Http(HttpCheck),
}
impl Check {
    /// Returns the HTTP config if this check is an HTTP check
    pub fn http(&self) -> Option<&HttpCheck> {
        match self {
            Check::Http(http_check) => Some(http_check),
            //_ => None,
        }
    }
    pub fn http_mut(&mut self) -> Option<&mut HttpCheck> {
        match self {
            Check::Http(http_check) => Some(http_check),
            //_ => None,
        }
    }
}
impl Display for Check {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Check::Http(http_probe) => std::fmt::Display::fmt(http_probe, f),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct TcpProbe {
    pub host: String,
    pub port: u16,
}

/// A HTTP request with optional checks of the returned content
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct HttpCheck {
    /// The full URL to GET. Redirects are followed
    pub url: Url,
    /// IP protocol that the check should use, or [IpProtocol::default()] if undefined
    #[serde(skip_serializing_if = "Option::is_none")]
    pub ip_protocol: Option<IpProtocol>,
    /// Disable TLS certificate validation while executing the request. Insecure.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub tls_insecure_skip_verify: Option<bool>,
    /// Optionally define response status codes that should be considered a probe success.
    /// By default, statuses in the 2XX range are considered successes
    #[serde(skip_serializing_if = "Option::is_none")]
    #[schemars(inner(range(min = 100, max = 999)))]
    pub response_codes: Option<Vec<u16>>,
    /// Optionally define a regex that the response body must match for the probe to succeed.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub response_regex: Option<String>,
    /// Override the default user agent string. Set to an empty string to disable
    #[serde(skip_serializing_if = "Option::is_none")]
    pub user_agent: Option<String>,
}
impl Display for HttpCheck {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "HTTP GET {} (via {})",
            self.url.as_str(),
            self.ip_protocol.unwrap_or_default()
        )
    }
}
impl HttpCheck {
    pub fn new(url: Url) -> HttpCheck {
        HttpCheck {
            url,
            ip_protocol: None,
            tls_insecure_skip_verify: None,
            response_codes: None,
            response_regex: None,
            user_agent: None,
        }
    }
}

#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
pub enum IpProtocol {
    IPv4,
    IPv6,
    #[default]
    #[serde(rename = "any")]
    Any,
}
impl Display for IpProtocol {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "{}",
            match self {
                IpProtocol::IPv4 => " IPv4",
                IpProtocol::IPv6 => " IPv6",
                IpProtocol::Any => "IPv4/6",
            }
        )
    }
}
impl FromStr for IpProtocol {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "ipv4" => Ok(IpProtocol::IPv4),
            "ipv6" => Ok(IpProtocol::IPv6),
            "any" => Ok(IpProtocol::Any),
            _ => Err("invalid ip protocol (chose from ipv4, ipv6, any)".to_string()),
        }
    }
}