Add html page

This commit is contained in:
2021-01-31 01:42:39 +01:00
parent e491d4aff6
commit 36834dd5fd
6 changed files with 144 additions and 14 deletions

View File

@ -1,4 +1,5 @@
( (
update_period: 30,
services: { services: {
"example": ( "example": (
url: "https://example.org", url: "https://example.org",

View File

@ -31,18 +31,24 @@ pub enum HttpHealthCheckMode {
} }
pub struct HealthState { pub struct HealthState {
pub last_update: Mutex<Option<DateTime<Utc>>>,
pub health: HashMap<ServiceId, Mutex<Option<HealthStatus>>>, pub health: HashMap<ServiceId, Mutex<Option<HealthStatus>>>,
pub config: HealthConfig, pub config: HealthConfig,
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct HealthConfig { pub struct HealthConfig {
/// The time between updates (in seconds)
pub update_period: u64,
/// The list of services
pub services: HashMap<ServiceId, ServiceConfig>, pub services: HashMap<ServiceId, ServiceConfig>,
} }
impl HealthState { impl HealthState {
pub fn new(config: HealthConfig) -> HealthState { pub fn new(config: HealthConfig) -> HealthState {
HealthState { HealthState {
last_update: Mutex::new(None),
health: config health: config
.services .services
.iter() .iter()
@ -53,6 +59,8 @@ impl HealthState {
} }
pub async fn update(&self) { pub async fn update(&self) {
*self.last_update.lock().await = Some(Utc::now());
for (id, status) in &self.health { for (id, status) in &self.health {
let url = &self.config.services[id].url; let url = &self.config.services[id].url;
info!("service [{}] querying {}", id, url); info!("service [{}] querying {}", id, url);

View File

@ -6,8 +6,18 @@ use health::HealthState;
use rocket_contrib::serve::StaticFiles; use rocket_contrib::serve::StaticFiles;
use rocket_contrib::templates::Template; use rocket_contrib::templates::Template;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration;
use std::{env, io}; use std::{env, io};
use tokio::fs; use tokio::{fs, task, time::sleep};
fn start_poller(state: Arc<HealthState>) {
task::spawn(async move {
loop {
state.update().await;
sleep(Duration::from_secs(state.config.update_period)).await;
}
});
}
#[rocket::main] #[rocket::main]
async fn main() -> io::Result<()> { async fn main() -> io::Result<()> {
@ -19,17 +29,20 @@ async fn main() -> io::Result<()> {
.await .await
.expect("failed to read config file"); .expect("failed to read config file");
let config = ron::from_str(&config).expect("failed to parse 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() let rocket = rocket::ignite()
//.attach(Template::custom(|engines| { //.attach(Template::custom(|engines| {
//handlebars_util::register_helpers(engines) //handlebars_util::register_helpers(engines)
//})) //}))
.manage(Arc::new(state)) .attach(Template::fairing())
.manage(Arc::clone(&state))
.mount("/static", StaticFiles::from("static")) .mount("/static", StaticFiles::from("static"))
.mount("/", rocket::routes![routes::pages::index,]); .mount("/", rocket::routes![routes::pages::dashboard]);
//.register(rocket::catchers![auth::login_page,]); //.register(rocket::catchers![auth::login_page,]);
start_poller(state);
rocket.launch().await.expect("rocket failed to launch"); rocket.launch().await.expect("rocket failed to launch");
Ok(()) Ok(())

View File

@ -1,22 +1,57 @@
use crate::health::HealthState; use crate::health::HealthState;
use crate::health::HealthStatus;
use chrono::Utc;
use rocket::{get, State}; use rocket::{get, State};
use rocket_contrib::templates::Template;
use serde::Serialize;
use std::sync::Arc; use std::sync::Arc;
use tokio::task::spawn;
#[get("/")] #[get("/")]
pub async fn index(state: State<'_, Arc<HealthState>>) -> String { pub async fn dashboard(state: State<'_, Arc<HealthState>>) -> Template {
{ #[derive(Debug, Serialize)]
let state = Arc::clone(state.inner()); struct Service {
spawn(async move { name: String,
state.update().await; status_text: &'static str,
}); status_color: &'static str,
} }
let mut out = String::new(); #[derive(Debug, Serialize)]
struct TemplateContext {
last_update: String,
services: Vec<Service>,
}
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 { 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)
} }

37
static/styles/common.css Normal file
View File

@ -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;
}

36
templates/dashboard.hbs Normal file
View File

@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<link rel="icon" type="image/svg+xml" href="/static/icon.svg">
<link rel="stylesheet" href="/static/styles/common.css">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Ubuntu|Ubuntu+Mono&display=swap">
<title>healthpot</title>
</head>
<body>
<h1 class="title">healthpot</h1>
<div class="last_update">
<span>last update:</span>
<span class="last_update_time">{{last_update}}</span>
</div>
<ul class="service_list">
{{#each services}}
<li class="service_entry">
<span class="service_name">{{this.name}}</span>
<span> is </span>
<span class="service_status"
style="background-color: {{this.status_color}}">
{{this.status_text}}
</span>
</li>
{{/each}}
</ul>
</body>
</html>