aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/check.rs77
-rw-r--r--src/log.rs19
-rw-r--r--src/main.rs36
-rw-r--r--src/style.css5
-rw-r--r--src/web.rs41
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()
- })
}
}
}
diff --git a/src/log.rs b/src/log.rs
index 408df88..50a1895 100644
--- a/src/log.rs
+++ b/src/log.rs
@@ -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 {
diff --git a/src/web.rs b/src/web.rs
index daf5101..b0d2ca9 100644
--- a/src/web.rs
+++ b/src/web.rs
@@ -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())
- }
-}