aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2024-05-03 22:06:37 +0200
committermetamuffin <metamuffin@disroot.org>2024-05-03 22:06:37 +0200
commit5d8d77a98fb84d05d34b57df73e0bc180c3140c2 (patch)
tree3cb47c2eca0c815f14f4db85f282845796889484 /src
parentd60bedc20dc0cc54cb697f3a45e5b222de6f1479 (diff)
downloadstatuspage-5d8d77a98fb84d05d34b57df73e0bc180c3140c2.tar
statuspage-5d8d77a98fb84d05d34b57df73e0bc180c3140c2.tar.bz2
statuspage-5d8d77a98fb84d05d34b57df73e0bc180c3140c2.tar.zst
works
Diffstat (limited to 'src')
-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
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%;
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)) {