aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock258
-rw-r--r--Cargo.toml2
-rw-r--r--src/check.rs13
-rw-r--r--src/log.rs53
-rw-r--r--src/mail.rs81
-rw-r--r--src/main.rs10
-rw-r--r--src/style.css12
-rw-r--r--src/web.rs22
8 files changed, 445 insertions, 6 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 5df0c8b..59ecc6d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -18,6 +18,18 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -27,6 +39,27 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -211,6 +244,30 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -233,6 +290,22 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -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"
@@ -443,6 +520,17 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -552,6 +640,29 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -605,6 +716,31 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -671,6 +807,12 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -709,6 +851,25 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -854,6 +1015,15 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -863,6 +1033,12 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1134,13 +1310,28 @@ dependencies = [
]
[[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",
@@ -1380,6 +1571,12 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1471,6 +1668,47 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -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<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%;
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<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)) {