Compare commits

...

4 Commits

Author SHA1 Message Date
e9ac30898b Add json api 2024-01-31 22:14:50 +01:00
cff7dddf4e Use eyre 2024-01-31 17:06:39 +01:00
f40922030c Use clap for cli 2024-01-31 16:55:09 +01:00
38186e4371 Update dependencies 2024-01-31 16:51:49 +01:00
7 changed files with 1485 additions and 1055 deletions

2385
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -19,24 +19,10 @@ duplicate = "0.2"
handlebars = "3" handlebars = "3"
http = "0.2" http = "0.2"
ron = "0.6.4" ron = "0.6.4"
rocket = { version = "0.5", features = ["secrets", "msgpack", "json"] }
[dependencies.rocket] rocket_dyn_templates = { version = "0.1.0", features = ["handlebars"] }
#version = "0.4" tokio = { version = "1", features = ["fs"] }
git = "https://github.com/SergioBenitez/Rocket" reqwest = { version = "0.11", default-features = false, features = ["rustls-tls"] }
branch = "master" clap = { version = "4.4.18", features = ["derive", "env"] }
features = ["secrets"] eyre = "0.6.11"
color-eyre = "0.6.2"
[dependencies.rocket_contrib]
#version = "0.4"
git = "https://github.com/SergioBenitez/Rocket"
branch = "master"
features = ["handlebars_templates", "uuid"]
[dependencies.tokio]
version = "1"
features = ["fs"]
[dependencies.reqwest]
version = "0.11"
default-features = false
features = ["rustls-tls"]

View File

@ -7,7 +7,7 @@ use std::collections::HashMap;
pub type ServiceId = String; pub type ServiceId = String;
#[derive(Debug, Copy, Clone)] #[derive(Debug, Copy, Clone, Serialize)]
pub enum HealthStatus { pub enum HealthStatus {
/// The service is up and running /// The service is up and running
Up, Up,
@ -69,8 +69,8 @@ impl HealthState {
last_update: Mutex::new(None), last_update: Mutex::new(None),
health: config health: config
.services .services
.iter() .keys()
.map(|(id, _config)| (id.clone(), Mutex::new(None))) .map(|id| (id.clone(), Mutex::new(None)))
.collect(), .collect(),
config, config,
} }
@ -109,7 +109,7 @@ impl HttpHealthCheckMode {
.map_err(|e| error!("invalid status code: {}", e)) .map_err(|e| error!("invalid status code: {}", e))
.ok() .ok()
.filter(|status| status == &actual) .filter(|status| status == &actual)
.and_then(|_| if_valid), .and(if_valid),
PartialStatusCode::Status5XX if actual.is_server_error() => if_valid, PartialStatusCode::Status5XX if actual.is_server_error() => if_valid,
PartialStatusCode::Status4XX if actual.is_server_error() => if_valid, PartialStatusCode::Status4XX if actual.is_server_error() => if_valid,
PartialStatusCode::Status3XX if actual.is_redirection() => if_valid, PartialStatusCode::Status3XX if actual.is_redirection() => if_valid,
@ -123,13 +123,13 @@ impl HttpHealthCheckMode {
let check_up = self let check_up = self
.up_status_codes .up_status_codes
.iter() .iter()
.flat_map(|up_code| validate_status(&up_code, response_status, HealthStatus::Up)); .flat_map(|up_code| validate_status(up_code, response_status, HealthStatus::Up));
// Check if response status matches expected status codes for Down // Check if response status matches expected status codes for Down
let check_down = self let check_down = self
.down_status_codes .down_status_codes
.iter() .iter()
.flat_map(|down_code| validate_status(&down_code, response_status, HealthStatus::Down)); .flat_map(|down_code| validate_status(down_code, response_status, HealthStatus::Down));
// Compute status, defaulting to Errored if neigher Up nor Down matched // Compute status, defaulting to Errored if neigher Up nor Down matched
check_up check_up

View File

@ -1,15 +1,53 @@
mod health; mod health;
mod routes; mod routes;
use clap::Parser;
use dotenv::dotenv; use dotenv::dotenv;
use eyre::Context;
use health::HealthState; use health::HealthState;
use rocket_contrib::serve::StaticFiles; use rocket::fs::FileServer;
use rocket_contrib::templates::Template; use rocket_dyn_templates::Template;
use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use std::{env, io};
use tokio::{fs, task, time::sleep}; use tokio::{fs, task, time::sleep};
#[derive(Parser)]
struct Opt {
/// Path to the config file.
#[clap(short, long, env = "CONFIG_PATH", default_value = "config.ron")]
config: PathBuf,
}
#[rocket::main]
async fn main() -> eyre::Result<()> {
dotenv().ok();
let opt = Opt::parse();
color_eyre::install()?;
let config = fs::read_to_string(&opt.config)
.await
.wrap_err_with(|| format!("failed to read config file: {:?}", opt.config))?;
let config = ron::from_str(&config)
.wrap_err_with(|| format!("failed to parse config file: {:?}", opt.config))?;
let state = Arc::new(HealthState::new(config));
let rocket = rocket::build()
.attach(Template::fairing())
.manage(Arc::clone(&state))
.mount("/static", FileServer::from("static"))
.mount(
"/",
rocket::routes![routes::pages::dashboard, routes::api::status],
);
start_poller(state);
rocket.launch().await.wrap_err("rocket failed to launch")?;
Ok(())
}
fn start_poller(state: Arc<HealthState>) { fn start_poller(state: Arc<HealthState>) {
task::spawn(async move { task::spawn(async move {
loop { loop {
@ -18,32 +56,3 @@ fn start_poller(state: Arc<HealthState>) {
} }
}); });
} }
#[rocket::main]
async fn main() -> io::Result<()> {
dotenv().ok();
let config_path = env::var("CONFIG_PATH").unwrap_or("config.ron".into());
let config = fs::read_to_string(&config_path)
.await
.expect("failed to read config file");
let config = ron::from_str(&config).expect("failed to parse config file");
let state = Arc::new(HealthState::new(config));
let rocket = rocket::ignite()
//.attach(Template::custom(|engines| {
//handlebars_util::register_helpers(engines)
//}))
.attach(Template::fairing())
.manage(Arc::clone(&state))
.mount("/static", StaticFiles::from("static"))
.mount("/", rocket::routes![routes::pages::dashboard]);
//.register(rocket::catchers![auth::login_page,]);
start_poller(state);
rocket.launch().await.expect("rocket failed to launch");
Ok(())
}

View File

@ -0,0 +1,33 @@
use crate::health::HealthState;
use crate::health::HealthStatus;
use chrono::DateTime;
use chrono::Utc;
use rocket::serde::json::Json;
use rocket::{get, State};
use serde::Serialize;
use std::collections::BTreeMap;
use std::sync::Arc;
#[derive(Serialize)]
pub struct Status {
last_update: Option<DateTime<Utc>>,
services: BTreeMap<String, HealthStatus>,
}
#[get("/status")]
pub async fn status(state: &State<Arc<HealthState>>) -> Json<Status> {
let last_update = *state.last_update.lock().await;
let mut services = BTreeMap::new();
for (id, status) in &state.health {
let Some(status) = *status.lock().await else {
continue;
};
services.insert(id.clone(), status);
}
Json(Status {
last_update,
services,
})
}

View File

@ -2,12 +2,12 @@ use crate::health::HealthState;
use crate::health::HealthStatus; use crate::health::HealthStatus;
use chrono::Utc; use chrono::Utc;
use rocket::{get, State}; use rocket::{get, State};
use rocket_contrib::templates::Template; use rocket_dyn_templates::Template;
use serde::Serialize; use serde::Serialize;
use std::sync::Arc; use std::sync::Arc;
#[get("/")] #[get("/")]
pub async fn dashboard(state: State<'_, Arc<HealthState>>) -> Template { pub async fn dashboard(state: &State<Arc<HealthState>>) -> Template {
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
struct Service { struct Service {
name: String, name: String,
@ -27,7 +27,7 @@ pub async fn dashboard(state: State<'_, Arc<HealthState>>) -> Template {
last_update: last_update last_update: last_update
.map(|last_update| { .map(|last_update| {
let now = Utc::now(); let now = Utc::now();
if now.date() == last_update.date() { if now.date_naive() == last_update.date_naive() {
format!("UTC {}", last_update.format("%H:%M:%S")) format!("UTC {}", last_update.format("%H:%M:%S"))
} else { } else {
format!("UTC {}", last_update.format("%Y-%m-%d %H:%M:%S")) format!("UTC {}", last_update.format("%Y-%m-%d %H:%M:%S"))
@ -43,8 +43,7 @@ pub async fn dashboard(state: State<'_, Arc<HealthState>>) -> Template {
Some(HealthStatus::Down) => ("red", "DOWN"), Some(HealthStatus::Down) => ("red", "DOWN"),
Some(HealthStatus::Errored) => ("orange", "ERRORED"), Some(HealthStatus::Errored) => ("orange", "ERRORED"),
None => ("#5b5b5b", "UNKNOWN"), None => ("#5b5b5b", "UNKNOWN"),
} };
.into();
context.services.push(Service { context.services.push(Service {
name: id.clone(), name: id.clone(),