#![feature(exit_status_error)] pub mod api; pub mod check; pub mod log; pub mod mail; pub mod web; use ::log::error; use anyhow::{anyhow, Result}; use api::make_json_response; use axum::{ http::HeaderMap, response::{Html, IntoResponse}, routing::get, Json, Router, }; use check::{check_loop, Check}; use chrono::{DateTime, Utc}; use mail::MailConfig; use reqwest::{ header::{CACHE_CONTROL, CONTENT_TYPE, REFRESH}, StatusCode, }; use serde::Deserialize; use std::{collections::BTreeMap, net::SocketAddr, process::exit, sync::Arc}; use tokio::{fs::read_to_string, sync::RwLock}; use web::make_html_page; pub static GLOBAL_ERROR: RwLock> = RwLock::const_new(None); #[tokio::main] async fn main() { env_logger::init_from_env("LOG"); if let Err(e) = run().await { error!("{e:?}"); exit(1); } } #[derive(Debug, Deserialize)] pub struct Config { mail: Option, title: String, bind: SocketAddr, interval: u64, services: Vec, } #[derive(Debug, Deserialize)] pub struct Service { title: String, url: Option, checks: Vec, #[serde(default)] mail_to: Vec, } #[derive(Debug, Clone)] pub struct Status { pub time: DateTime, pub status: Result, } static STATUS: RwLock> = RwLock::const_new(BTreeMap::new()); async fn run() -> anyhow::Result<()> { let config = std::env::args() .nth(1) .ok_or(anyhow!("expected config path as first argument"))?; let config = read_to_string(config).await?; let config = Arc::::new(serde_yaml::from_str(&config)?); for i in 0..config.services.len() { tokio::task::spawn(check_loop(config.clone(), i)); } let app = Router::new() .route( "/", get({ let config = config.clone(); move |headers: HeaderMap| async move { ( [ ( CACHE_CONTROL, format!("max-age={}, public", config.interval), ), (REFRESH, format!("{}", config.interval)), ], if headers .get("accept") .map_or(false, |s| s == "application/json") { Json(make_json_response(config.clone()).await).into_response() } else { Html(make_html_page(config.clone()).await).into_response() }, ) } }), ) .route( "/font.woff2", get(|| async { ( [ (CONTENT_TYPE, "font/woff2"), (CACHE_CONTROL, "public, immutable"), ], include_bytes!("cantarell.woff2"), ) }), ) .route("/favicon.ico", get(|| async { StatusCode::NO_CONTENT })); let listener = tokio::net::TcpListener::bind(config.bind).await?; axum::serve(listener, app).await?; Ok(()) }