use std::{fmt::Display, process::Command}; use reqwest::blocking::Client; use serde::Deserialize; use crate::Error; #[derive(Debug, Clone, Deserialize)] pub struct Service { pub name: String, #[serde(flatten)] pub kind: Kind, #[serde(skip)] pub state: State, } impl Service { pub fn check(&mut self, client: Client) -> Result { self.state = self.kind.get_state(client)?; Ok(self.state.is_operational()) } } #[derive(Debug, Clone, Deserialize)] #[serde(tag = "type", rename_all = "lowercase")] pub enum Kind { Tcp { address: String, }, Http { url: String, #[serde(default = "Kind::default_method")] method: String, #[serde(default = "Kind::default_code")] status_code: u16, }, Systemd { service: String, }, } impl Kind { fn default_method() -> String { "GET".to_string() } fn default_code() -> u16 { 200 } pub fn get_state(&self, client: Client) -> Result { let state = match self { Kind::Tcp { address } => { if std::net::TcpStream::connect(address).is_ok() { State::Operational } else { State::Down("Unreachable".to_string()) } } Kind::Http { method, url, status_code, } => { match client .request(method.parse().map_err(|_| Error::Method)?, url) .send()? .status() { s if s.as_u16() == *status_code => State::Operational, s => State::Down(s.to_string()), } } Kind::Systemd { service } => { let output = Command::new("systemctl") .arg("is-active") .arg(service) .output()?; if output.status.success() { State::Operational } else { State::Down(String::from_utf8_lossy(&output.stdout).to_string()) } } }; Ok(state) } } impl Display for Kind { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Kind::Tcp { address } => write!(f, "tcp://{address}"), Kind::Http { method, url, .. } => write!(f, "{method} {url}"), Kind::Systemd { service } => write!(f, "{service}"), } } } #[derive(Debug, Clone, Default)] pub struct Status { pub info: String, pub state: State, } #[derive(Debug, Clone, Default)] pub enum State { #[default] Unknown, Operational, Down(String), } impl State { /// Returns `true` if this is a `Unknown` variant. pub fn is_unknown(&self) -> bool { matches!(self, Self::Unknown) } /// Returns `true` if this is a `Operational` variant. pub fn is_operational(&self) -> bool { matches!(self, Self::Operational) } /// Returns `true` if this is a `Down` variant. pub fn is_down(&self) -> bool { matches!(self, Self::Down(_)) } /// Converts the `State` into an `Option` containing `String` description if the `State` was /// `Down` and `None` otherwise. pub fn down_value(self) -> Option { match self { State::Unknown => None, State::Operational => None, State::Down(s) => Some(s), } } pub fn as_level(&self) -> String { match self { State::Unknown => "warning", State::Operational => "ok", State::Down(_) => "error", } .to_string() } } impl Display for State { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { State::Unknown => write!(f, "Unknown"), State::Operational => write!(f, "Operational"), State::Down(s) => write!(f, "{s}"), } } }