diff options
author | metamuffin <metamuffin@disroot.org> | 2024-05-03 22:06:37 +0200 |
---|---|---|
committer | metamuffin <metamuffin@disroot.org> | 2024-05-03 22:06:37 +0200 |
commit | 5d8d77a98fb84d05d34b57df73e0bc180c3140c2 (patch) | |
tree | 3cb47c2eca0c815f14f4db85f282845796889484 /src | |
parent | d60bedc20dc0cc54cb697f3a45e5b222de6f1479 (diff) | |
download | statuspage-5d8d77a98fb84d05d34b57df73e0bc180c3140c2.tar statuspage-5d8d77a98fb84d05d34b57df73e0bc180c3140c2.tar.bz2 statuspage-5d8d77a98fb84d05d34b57df73e0bc180c3140c2.tar.zst |
works
Diffstat (limited to 'src')
-rw-r--r-- | src/check.rs | 13 | ||||
-rw-r--r-- | src/log.rs | 53 | ||||
-rw-r--r-- | src/mail.rs | 81 | ||||
-rw-r--r-- | src/main.rs | 10 | ||||
-rw-r--r-- | src/style.css | 12 | ||||
-rw-r--r-- | src/web.rs | 22 |
6 files changed, 185 insertions, 6 deletions
diff --git a/src/check.rs b/src/check.rs index bfde1b9..4b6bc95 100644 --- a/src/check.rs +++ b/src/check.rs @@ -1,4 +1,4 @@ -use crate::{Check, Config, Success, STATUS}; +use crate::{log::update_service, Check, Config, Success, GLOBAL_ERROR, STATUS}; use anyhow::{anyhow, bail, Context, Result}; use futures::{stream::FuturesUnordered, StreamExt}; use log::info; @@ -27,11 +27,22 @@ 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 mut g = STATUS.write().await; g.insert((i, j), r); } + let config = config.clone(); + tokio::task::spawn(async move { + if let Err(e) = update_service(config.clone(), i, j, r2).await { + *GLOBAL_ERROR.write().await = Some(e); + } + }) }, )); while let Some(_) = futs.next().await {} diff --git a/src/log.rs b/src/log.rs new file mode 100644 index 0000000..408df88 --- /dev/null +++ b/src/log.rs @@ -0,0 +1,53 @@ +use crate::{mail::send_mail, Config, STATUS}; +use chrono::{DateTime, Utc}; +use std::{ + collections::{BTreeMap, VecDeque}, + sync::Arc, +}; +use tokio::sync::RwLock; + +static LAST_STATUS: RwLock<BTreeMap<usize, bool>> = RwLock::const_new(BTreeMap::new()); +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 async fn update_service( + config: Arc<Config>, + service: usize, + check: usize, + error: Option<(String, String)>, +) -> 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()) + }; + + 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), + 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?; + } + } + Ok(()) +} diff --git a/src/mail.rs b/src/mail.rs index e69de29..841b552 100644 --- a/src/mail.rs +++ b/src/mail.rs @@ -0,0 +1,81 @@ +use std::{str::FromStr, sync::Arc, time::Duration}; + +use anyhow::{anyhow, Result}; +use lettre::{ + message::Mailbox, transport::smtp::authentication::Credentials, Message, SmtpTransport, + Transport, +}; +use log::info; +use serde::Deserialize; + +use crate::Config; + +#[derive(Debug, Deserialize)] +pub struct MailConfig { + from: String, + to: Vec<String>, + smtp_auth: SmtpAuth, + smtp_port: u16, + smtp_timeout: Option<f64>, + smtp_server: String, + smtp_username: Option<String>, + smtp_password: Option<String>, +} + +#[derive(Clone, Deserialize, Debug)] +#[serde(rename_all = "snake_case")] +enum SmtpAuth { + Plain, + Tls, + Starttls, +} + +pub async fn send_mail( + config: &Arc<Config>, + service: usize, + check: usize, + error: String, +) -> Result<()> { + let config = config.to_owned(); + tokio::task::spawn_blocking(move || { + if let Some(mconfig) = &config.mail { + info!("sending mail!"); + let mut transport = match mconfig.smtp_auth { + SmtpAuth::Plain => SmtpTransport::builder_dangerous(&mconfig.smtp_server), + SmtpAuth::Tls => SmtpTransport::relay(&mconfig.smtp_server)?, + SmtpAuth::Starttls => SmtpTransport::starttls_relay(&mconfig.smtp_server)?, + } + .port(mconfig.smtp_port); + + if let Some(username) = mconfig.smtp_username.clone() { + let password = mconfig + .smtp_password + .clone() + .ok_or(anyhow!("smtp password missing"))?; + transport = transport.credentials(Credentials::new(username, password)); + } + if let Some(timeout) = mconfig.smtp_timeout { + transport = transport.timeout(Some(Duration::from_secs_f64(timeout))) + } + let transport = transport.build(); + + for recipient in &mconfig.to { + let service = &config.services[service]; + + let message = Message::builder() + .from(Mailbox::from_str(&mconfig.from)?) + .to(Mailbox::from_str(&recipient)?) + .subject(format!("{} failed.", service.title)) + .body(format!( + "Check {:?} reported:\n{}", + service.checks[check].display(), + error + ))?; + transport.send(&message)?; + } + } + Ok::<_, anyhow::Error>(()) + }) + .await??; + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index 0136f94..706effe 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,13 @@ pub mod check; -pub mod web; +pub mod log; pub mod mail; +pub mod web; +use ::log::error; use anyhow::{anyhow, Result}; use axum::{routing::get, Router}; use check::check_loop; -use log::error; +use mail::MailConfig; use serde::Deserialize; use std::{ collections::BTreeMap, @@ -16,6 +18,8 @@ use std::{ use tokio::{fs::read_to_string, sync::RwLock}; use web::send_html_page; +pub static GLOBAL_ERROR: RwLock<Option<anyhow::Error>> = RwLock::const_new(None); + #[tokio::main] async fn main() { env_logger::init_from_env("LOG"); @@ -26,6 +30,7 @@ async fn main() { #[derive(Debug, Deserialize)] pub struct Config { + mail: Option<MailConfig>, title: String, bind: SocketAddr, interval: u64, @@ -35,6 +40,7 @@ pub struct Config { #[derive(Debug, Deserialize)] pub struct Service { title: String, + url: Option<String>, checks: Vec<Check>, } diff --git a/src/style.css b/src/style.css index c207f62..1cd0375 100644 --- a/src/style.css +++ b/src/style.css @@ -16,11 +16,13 @@ h1 { } h2 { - margin-top: 0.5em; + margin: 0.5em; + display: inline-block; color: black; } -div.service { +div.service, +div.log { padding: 1em; margin: 2em; box-shadow: 0 10px 60px rgba(0, 0, 0, 0.15); @@ -33,6 +35,12 @@ div.service.error { border-top: 12px solid rgb(255, 80, 80); } +div.service a { + font-weight: bold; + color: black; + float: inline-end; +} + div.checks { display: grid; width: 100%; @@ -1,4 +1,4 @@ -use crate::{Check, Config, Service, Success, STATUS}; +use crate::{log::LOG, Check, Config, Service, Success, STATUS}; use anyhow::Result; use axum::response::Html; use markup::{doctype, Render}; @@ -8,6 +8,8 @@ pub async fn send_html_page(config: Arc<Config>) -> Html<String> { let mut out = String::new(); let status = STATUS.read().await; let status = status.deref(); + let log = LOG.read().await; + let log = log.deref(); #[cfg(not(debug_assertions))] let css = include_str!("style.css"); @@ -29,6 +31,23 @@ pub async fn send_html_page(config: Arc<Config>) -> Html<String> { @for (i, service) in config.services.iter().enumerate() { @ServiceCard { i, status, service } } + div.log { + h2 { "Past Events" } + ul { + @for event in log.iter() { + li.{if event.error.is_some() { "error" } else { "ok" }} { + @let service = &config.services[event.service]; + b { @event.time.to_rfc2822() ": " } + @service.title " " + @if let Some(error) = &event.error { + " failed. " @service.checks[event.check].display() " reported " @error + } else { + " is working again." + } + } + } + } + } } } } @@ -43,6 +62,7 @@ markup::define!( @let any_err = status.range((*i,usize::MIN)..(*i,usize::MAX)).any(|(_,v)|v.is_err()); div.service.{if any_err { "error" } else { "ok" }} { h2 { @service.title } + @if let Some(url) = &service.url { a[href=url] { "Link" } } div.checks { @for (j, check) in service.checks.iter().enumerate() { @if let Some(status) = status.get(&(*i, j)) { |