diff --git a/example.config.ron b/example.config.ron index 64ee2d0..cc2d454 100644 --- a/example.config.ron +++ b/example.config.ron @@ -1,4 +1,5 @@ ( + update_period: 30, services: { "example": ( url: "https://example.org", diff --git a/src/health/mod.rs b/src/health/mod.rs index a9266c0..08526cd 100644 --- a/src/health/mod.rs +++ b/src/health/mod.rs @@ -31,18 +31,24 @@ pub enum HttpHealthCheckMode { } pub struct HealthState { + pub last_update: Mutex>>, pub health: HashMap>>, pub config: HealthConfig, } #[derive(Serialize, Deserialize)] pub struct HealthConfig { + /// The time between updates (in seconds) + pub update_period: u64, + + /// The list of services pub services: HashMap, } impl HealthState { pub fn new(config: HealthConfig) -> HealthState { HealthState { + last_update: Mutex::new(None), health: config .services .iter() @@ -53,6 +59,8 @@ impl HealthState { } pub async fn update(&self) { + *self.last_update.lock().await = Some(Utc::now()); + for (id, status) in &self.health { let url = &self.config.services[id].url; info!("service [{}] querying {}", id, url); diff --git a/src/main.rs b/src/main.rs index 154ec3c..fe3913b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,8 +6,18 @@ use health::HealthState; use rocket_contrib::serve::StaticFiles; use rocket_contrib::templates::Template; use std::sync::Arc; +use std::time::Duration; use std::{env, io}; -use tokio::fs; +use tokio::{fs, task, time::sleep}; + +fn start_poller(state: Arc) { + task::spawn(async move { + loop { + state.update().await; + sleep(Duration::from_secs(state.config.update_period)).await; + } + }); +} #[rocket::main] async fn main() -> io::Result<()> { @@ -19,17 +29,20 @@ async fn main() -> io::Result<()> { .await .expect("failed to read config file"); let config = ron::from_str(&config).expect("failed to parse config file"); - let state = HealthState::new(config); + let state = Arc::new(HealthState::new(config)); let rocket = rocket::ignite() //.attach(Template::custom(|engines| { //handlebars_util::register_helpers(engines) //})) - .manage(Arc::new(state)) + .attach(Template::fairing()) + .manage(Arc::clone(&state)) .mount("/static", StaticFiles::from("static")) - .mount("/", rocket::routes![routes::pages::index,]); + .mount("/", rocket::routes![routes::pages::dashboard]); //.register(rocket::catchers![auth::login_page,]); + start_poller(state); + rocket.launch().await.expect("rocket failed to launch"); Ok(()) diff --git a/src/routes/pages/mod.rs b/src/routes/pages/mod.rs index c6e92a0..035e0c8 100644 --- a/src/routes/pages/mod.rs +++ b/src/routes/pages/mod.rs @@ -1,22 +1,57 @@ use crate::health::HealthState; +use crate::health::HealthStatus; +use chrono::Utc; use rocket::{get, State}; +use rocket_contrib::templates::Template; +use serde::Serialize; use std::sync::Arc; -use tokio::task::spawn; #[get("/")] -pub async fn index(state: State<'_, Arc>) -> String { - { - let state = Arc::clone(state.inner()); - spawn(async move { - state.update().await; - }); +pub async fn dashboard(state: State<'_, Arc>) -> Template { + #[derive(Debug, Serialize)] + struct Service { + name: String, + status_text: &'static str, + status_color: &'static str, } - let mut out = String::new(); + #[derive(Debug, Serialize)] + struct TemplateContext { + last_update: String, + services: Vec, + } + + let last_update = *state.last_update.lock().await; + + let mut context = TemplateContext { + last_update: last_update + .map(|last_update| { + let now = Utc::now(); + if now.date() == last_update.date() { + format!("UTC {}", last_update.format("%H:%M:%S")) + } else { + format!("UTC {}", last_update.format("%Y-%m-%d %H:%M:%S")) + } + }) + .unwrap_or_else(|| "never".to_string()), + services: vec![], + }; for (id, status) in &state.health { - out.push_str(&format!("{}: {:?}\n", id, &*status.lock().await)); + let (status_color, status_text) = match *status.lock().await { + Some(HealthStatus::Up) => ("green", "UP"), + Some(HealthStatus::Down) => ("red", "DOWN"), + Some(HealthStatus::Errored) => ("orange", "ERRORED"), + None => ("#5b5b5b", "UNKNOWN"), + } + .into(); + + context.services.push(Service { + name: id.clone(), + status_color, + status_text, + }) } - out + Template::render("dashboard", &context) } diff --git a/static/styles/common.css b/static/styles/common.css new file mode 100644 index 0000000..abe422e --- /dev/null +++ b/static/styles/common.css @@ -0,0 +1,37 @@ +body { + font-family: Ubuntu; +} + +h1 { + text-decoration: underline; + text-decoration-color: gray; + margin-top: 0.1em; + margin-bottom: 0.4em; + font-style: italic; + letter-spacing: 0.2em; + font-size: 2em; +} + +.service_list { +} + +.service_entry { + margin-top: 0.6em; + margin-bottom: 0.6em; +} + +.service_name { + color: #19345e; +} + +.service_status { + padding: 0.2em; + color: white; +} + +.last_update { +} + +.last_update_time { + color: purple; +} diff --git a/templates/dashboard.hbs b/templates/dashboard.hbs new file mode 100644 index 0000000..9d0e76f --- /dev/null +++ b/templates/dashboard.hbs @@ -0,0 +1,36 @@ + + + + + + + + + + + + healthpot + + + +

healthpot

+ +
+ last update: + {{last_update}} +
+ +
    + {{#each services}} +
  • + {{this.name}} + is + + {{this.status_text}} + +
  • + {{/each}} +
+ +