From 5d8d77a98fb84d05d34b57df73e0bc180c3140c2 Mon Sep 17 00:00:00 2001 From: metamuffin Date: Fri, 3 May 2024 22:06:37 +0200 Subject: works --- Cargo.lock | 258 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 2 + src/check.rs | 13 ++- src/log.rs | 53 ++++++++++++ src/mail.rs | 81 ++++++++++++++++++ src/main.rs | 10 ++- src/style.css | 12 ++- src/web.rs | 22 ++++- 8 files changed, 445 insertions(+), 6 deletions(-) create mode 100644 src/log.rs diff --git a/Cargo.lock b/Cargo.lock index 5df0c8b..59ecc6d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,18 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -26,6 +38,27 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "0.6.14" @@ -210,6 +243,30 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets 0.52.5", +] + +[[package]] +name = "chumsky" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eebd66744a15ded14960ab4ccdbfb51ad3b81f51f3f04a80adac98c985396c9" +dependencies = [ + "hashbrown", + "stacker", +] + [[package]] name = "colorchoice" version = "1.0.1" @@ -232,6 +289,22 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +[[package]] +name = "email-encoding" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60d1d33cdaede7e24091f039632eb5d3c7469fe5b066a985281a34fc70fa317f" +dependencies = [ + "base64", + "memchr", +] + +[[package]] +name = "email_address" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2153bd83ebc09db15bcbdc3e2194d901804952e3dc96967e1cd3b0c5c32d112" + [[package]] name = "encoding_rs" version = "0.8.34" @@ -435,6 +508,10 @@ name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] [[package]] name = "hermit-abi" @@ -442,6 +519,17 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +[[package]] +name = "hostname" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9c7c7c8ac16c798734b8a24560c1362120597c40d5e1459f09498f8f6c8f2ba" +dependencies = [ + "cfg-if", + "libc", + "windows", +] + [[package]] name = "http" version = "1.1.0" @@ -551,6 +639,29 @@ dependencies = [ "tracing", ] +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "idna" version = "0.5.0" @@ -604,6 +715,31 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "lettre" +version = "0.11.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a62049a808f1c4e2356a2a380bd5f2aca3b011b0b482cf3b914ba1731426969" +dependencies = [ + "base64", + "chumsky", + "email-encoding", + "email_address", + "fastrand", + "futures-util", + "hostname", + "httpdate", + "idna", + "mime", + "native-tls", + "nom", + "percent-encoding", + "quoted_printable", + "socket2", + "tokio", + "url", +] + [[package]] name = "libc" version = "0.2.154" @@ -670,6 +806,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.7.2" @@ -708,6 +850,25 @@ dependencies = [ "tempfile", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-traits" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +dependencies = [ + "autocfg", +] + [[package]] name = "num_cpus" version = "1.16.0" @@ -853,6 +1014,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "psm" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5787f7cda34e3033a72192c018bc5883100330f362ef279a8cbccfce8bb4e874" +dependencies = [ + "cc", +] + [[package]] name = "quote" version = "1.0.36" @@ -862,6 +1032,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "quoted_printable" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79ec282e887b434b68c18fe5c121d38e72a5cf35119b59e54ec5b992ea9c8eb0" + [[package]] name = "redox_syscall" version = "0.5.1" @@ -1133,14 +1309,29 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "stacker" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c886bd4480155fd3ef527d45e9ac8dd7118a898a46530b7b94c3e21866259fce" +dependencies = [ + "cc", + "cfg-if", + "libc", + "psm", + "winapi", +] + [[package]] name = "statuspage" version = "0.1.0" dependencies = [ "anyhow", "axum", + "chrono", "env_logger", "futures", + "lettre", "log", "markup", "reqwest", @@ -1379,6 +1570,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + [[package]] name = "want" version = "0.3.1" @@ -1470,6 +1667,47 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +dependencies = [ + "windows-core", + "windows-targets 0.52.5", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.5", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -1618,3 +1856,23 @@ dependencies = [ "cfg-if", "windows-sys 0.48.0", ] + +[[package]] +name = "zerocopy" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index aeffe1b..472b16c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,3 +15,5 @@ futures = "0.3.30" shlex = "1.3.0" reqwest = "0.12.4" markup = "0.15.0" +chrono = "0.4.38" +lettre = "0.11.7" 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, 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> = RwLock::const_new(BTreeMap::new()); +pub static LOG: RwLock> = RwLock::const_new(VecDeque::new()); + +pub struct Event { + pub time: DateTime, + pub service: usize, + pub check: usize, + pub error: Option, +} + +pub async fn update_service( + config: Arc, + 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, + smtp_auth: SmtpAuth, + smtp_port: u16, + smtp_timeout: Option, + smtp_server: String, + smtp_username: Option, + smtp_password: Option, +} + +#[derive(Clone, Deserialize, Debug)] +#[serde(rename_all = "snake_case")] +enum SmtpAuth { + Plain, + Tls, + Starttls, +} + +pub async fn send_mail( + config: &Arc, + 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> = 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, title: String, bind: SocketAddr, interval: u64, @@ -35,6 +40,7 @@ pub struct Config { #[derive(Debug, Deserialize)] pub struct Service { title: String, + url: Option, checks: Vec, } 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%; diff --git a/src/web.rs b/src/web.rs index 0080697..daf5101 100644 --- a/src/web.rs +++ b/src/web.rs @@ -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) -> Html { 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) -> Html { @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)) { -- cgit v1.2.3-70-g09d2