diff options
-rw-r--r-- | src/check.rs | 77 | ||||
-rw-r--r-- | src/log.rs | 19 | ||||
-rw-r--r-- | src/main.rs | 36 | ||||
-rw-r--r-- | src/style.css | 5 | ||||
-rw-r--r-- | src/web.rs | 41 |
5 files changed, 85 insertions, 93 deletions
diff --git a/src/check.rs b/src/check.rs index 4b6bc95..a979e96 100644 --- a/src/check.rs +++ b/src/check.rs @@ -1,16 +1,24 @@ -use crate::{log::update_service, Check, Config, Success, GLOBAL_ERROR, STATUS}; +use crate::{log::update_service, Config, Status, GLOBAL_ERROR, STATUS}; use anyhow::{anyhow, bail, Context, Result}; +use chrono::Utc; use futures::{stream::FuturesUnordered, StreamExt}; use log::info; -use std::{ - sync::Arc, - time::{Duration, Instant}, -}; +use serde::Deserialize; +use std::{sync::Arc, time::Duration}; use tokio::{ process::Command, time::{sleep, timeout}, }; +#[derive(Debug, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum Check { + Systemd(String), + Pacman(String), + Http { title: Option<String>, url: String }, + Shell { title: String, command: String }, +} + pub async fn check_loop(config: Arc<Config>, i: usize) { loop { check_service(&config, i).await; @@ -27,19 +35,18 @@ async fn check_service(config: &Arc<Config>, i: usize) { Ok(Err(e)) => Err(e), Err(_) => Err(anyhow!("timed out")), }; - let r2 = r - .as_ref() - .err() - .map(|e| (format!("{e}"), format!("{e:?}"))) - .to_owned(); info!("check {i}:{j} => {r:?}"); + let status = Status { + time: Utc::now(), + status: r.map_err(|e| format!("{e:?}")), + }; { let mut g = STATUS.write().await; - g.insert((i, j), r); + g.insert((i, j), status.clone()); } let config = config.clone(); tokio::task::spawn(async move { - if let Err(e) = update_service(config.clone(), i, j, r2).await { + if let Err(e) = update_service(config.clone(), i, j, status).await { *GLOBAL_ERROR.write().await = Some(e); } }) @@ -49,29 +56,43 @@ async fn check_service(config: &Arc<Config>, i: usize) { } impl Check { - pub async fn check(&self) -> Result<Success> { + pub async fn check(&self) -> Result<String> { match self { + Check::Pacman(package) => { + let output = Command::new("pacman") + .arg("-Q") + .arg(package) + .output() + .await?; + output.status.exit_ok()?; + Ok(String::from_utf8(output.stdout)?) + } Check::Systemd(sname) => { let output = Command::new("systemctl") .arg("show") .arg("--no-pager") .arg(sname) .output() - .await - .context("systemctl")?; + .await?; let output = String::from_utf8(output.stdout).context("systemctl output")?; + let mut active = ""; + let mut sub = ""; for line in output.split("\n") { if let Some((key, value)) = line.split_once("=") { match key { - "ActiveState" if value != "active" => { - bail!("{value}") - } + "ActiveState" => active = value, + "SubState" => sub = value, _ => (), } } } - Ok(Success::default()) + let s = format!("{active} ({sub})"); + if active != "active" { + Err(anyhow!(s)) + } else { + Ok(s) + } } Check::Shell { command, .. } => { let args = shlex::split(&command).ok_or(anyhow!("command syntax invalid"))?; @@ -81,21 +102,23 @@ impl Check { .await; match status { - Ok(status) if status.success() => Ok(Success::default()), + Ok(status) if status.success() => Ok(Default::default()), Ok(status) => bail!("failed with code {}", status.code().unwrap_or(1)), Err(e) => bail!("command failed to execute: {e}"), } } Check::Http { url, .. } => { - let k = Instant::now(); let r = reqwest::get(url).await?; - if !r.status().is_success() { - bail!("http status: {}", r.status().as_str()) + let s = format!( + "{} {}", + r.status().as_str(), + r.status().canonical_reason().unwrap_or_default() + ); + if r.status().is_success() { + Ok(s) + } else { + bail!("{s}") } - Ok(Success { - latency: Some(k.elapsed()), - ..Default::default() - }) } } } @@ -1,5 +1,4 @@ -use crate::{mail::send_mail, Config, STATUS}; -use chrono::{DateTime, Utc}; +use crate::{mail::send_mail, Config, Status, STATUS}; use std::{ collections::{BTreeMap, VecDeque}, sync::Arc, @@ -10,43 +9,39 @@ static LAST_STATUS: RwLock<BTreeMap<usize, bool>> = RwLock::const_new(BTreeMap:: pub static LOG: RwLock<VecDeque<Event>> = RwLock::const_new(VecDeque::new()); pub struct Event { - pub time: DateTime<Utc>, pub service: usize, pub check: usize, - pub error: Option<String>, + pub status: Status, } pub async fn update_service( config: Arc<Config>, service: usize, check: usize, - error: Option<(String, String)>, + status: Status, ) -> anyhow::Result<()> { let mut last_status = LAST_STATUS.write().await; let last_status = last_status.entry(service).or_insert(true); - eprintln!("{service} {error:?}"); - let current_status = { let status = STATUS.read().await; !status .range((service, usize::MIN)..(service, usize::MAX)) - .any(|(_, v)| v.is_err()) + .any(|(_, v)| v.status.is_err()) }; if *last_status != current_status { *last_status = current_status; let mut log = LOG.write().await; log.push_front(Event { - error: error.clone().map(|(e, _)| e), + status: status.clone(), service, check, - time: Utc::now(), }); while log.len() > 32 { log.pop_back(); } - if let Some((_short, long)) = error.clone() { - send_mail(&config, service, check, long).await?; + if let Err(error) = status.status { + send_mail(&config, service, check, error).await?; } } Ok(()) diff --git a/src/main.rs b/src/main.rs index 706effe..67df74e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +#![feature(exit_status_error)] pub mod check; pub mod log; pub mod mail; @@ -6,15 +7,11 @@ pub mod web; use ::log::error; use anyhow::{anyhow, Result}; use axum::{routing::get, Router}; -use check::check_loop; +use check::{check_loop, Check}; +use chrono::{DateTime, Utc}; use mail::MailConfig; use serde::Deserialize; -use std::{ - collections::BTreeMap, - net::SocketAddr, - sync::Arc, - time::{Duration, SystemTime}, -}; +use std::{collections::BTreeMap, net::SocketAddr, sync::Arc}; use tokio::{fs::read_to_string, sync::RwLock}; use web::send_html_page; @@ -44,30 +41,13 @@ pub struct Service { checks: Vec<Check>, } -#[derive(Debug, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum Check { - Systemd(String), - Http { title: Option<String>, url: String }, - Shell { title: String, command: String }, -} - #[derive(Debug, Clone)] -pub struct Success { - pub latency: Option<Duration>, - pub updated: SystemTime, -} -impl Default for Success { - fn default() -> Self { - Self { - latency: None, - updated: SystemTime::now(), - } - } +pub struct Status { + pub time: DateTime<Utc>, + pub status: Result<String, String>, } -static STATUS: RwLock<BTreeMap<(usize, usize), Result<Success>>> = - RwLock::const_new(BTreeMap::new()); +static STATUS: RwLock<BTreeMap<(usize, usize), Status>> = RwLock::const_new(BTreeMap::new()); async fn run() -> anyhow::Result<()> { let config = std::env::args() diff --git a/src/style.css b/src/style.css index 1cd0375..9c2cb52 100644 --- a/src/style.css +++ b/src/style.css @@ -44,7 +44,7 @@ div.service a { div.checks { display: grid; width: 100%; - grid-template-columns: 30% 10% 60%; + grid-template-columns: 30% 10% 50% 10%; } div.checks span { text-overflow: ellipsis; @@ -52,7 +52,8 @@ div.checks span { text-wrap: nowrap; } -span.details.ok { +span.details.ok, +span.time { color: rgb(173, 173, 173); } span.status { @@ -1,8 +1,8 @@ -use crate::{log::LOG, Check, Config, Service, Success, STATUS}; -use anyhow::Result; +use crate::{log::LOG, Check, Config, Service, Status, STATUS}; use axum::response::Html; +use chrono::SubsecRound; use markup::{doctype, Render}; -use std::{collections::BTreeMap, ops::Deref, sync::Arc, time::Duration}; +use std::{collections::BTreeMap, ops::Deref, sync::Arc}; pub async fn send_html_page(config: Arc<Config>) -> Html<String> { let mut out = String::new(); @@ -35,11 +35,11 @@ pub async fn send_html_page(config: Arc<Config>) -> Html<String> { h2 { "Past Events" } ul { @for event in log.iter() { - li.{if event.error.is_some() { "error" } else { "ok" }} { + li.{if event.status.status.is_ok() { "ok" } else { "error" }} { @let service = &config.services[event.service]; - b { @event.time.to_rfc2822() ": " } + b { @event.status.time.to_rfc2822() ": " } @service.title " " - @if let Some(error) = &event.error { + @if let Err(error) = &event.status.status { " failed. " @service.checks[event.check].display() " reported " @error } else { " is working again." @@ -58,8 +58,8 @@ pub async fn send_html_page(config: Arc<Config>) -> Html<String> { } markup::define!( - ServiceCard<'a>(status: &'a BTreeMap<(usize, usize), Result<Success>>, service: &'a Service, i: usize) { - @let any_err = status.range((*i,usize::MIN)..(*i,usize::MAX)).any(|(_,v)|v.is_err()); + ServiceCard<'a>(status: &'a BTreeMap<(usize, usize), Status>, service: &'a Service, i: usize) { + @let any_err = status.range((*i,usize::MIN)..(*i,usize::MAX)).any(|(_,v)|v.status.is_err()); div.service.{if any_err { "error" } else { "ok" }} { h2 { @service.title } @if let Some(url) = &service.url { a[href=url] { "Link" } } @@ -67,19 +67,19 @@ markup::define!( @for (j, check) in service.checks.iter().enumerate() { @if let Some(status) = status.get(&(*i, j)) { span.name { @check.display() } - @match status { - Ok(Success { latency, updated }) => { + @match &status.status { + Ok(s) => { span.status.ok { "Ok" } - span.details.ok { - "Checked " @format_duration(updated.elapsed().unwrap_or_default()) " ago" - @if let Some(latency) = latency { ", took " @format_duration(*latency) } "." - } + span.details.ok { @s } } - Err(e) => { + Err(s) => { span.status.error { "Error" } - span.details { @format!("{e}") } + span.details.error { @s } } } + span.time { + @status.time.time().round_subsecs(0).to_string() + } } } } @@ -93,14 +93,7 @@ impl Check { Check::Systemd(_) => "Service".to_string(), Check::Http { title, .. } => title.clone().unwrap_or("HTTP".to_string()), Check::Shell { title, .. } => title.to_owned(), + Check::Pacman(_) => "Installed".to_string(), } } } - -fn format_duration(d: Duration) -> String { - if d.as_millis() < 1500 { - format!("{}ms", d.as_millis()) - } else { - format!("{}s", d.as_secs()) - } -} |