Initial Commit
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/target
|
||||
config.ron
|
||||
2483
Cargo.lock
generated
Normal file
2483
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
42
Cargo.toml
Normal file
42
Cargo.toml
Normal file
@ -0,0 +1,42 @@
|
||||
[package]
|
||||
name = "healthpot"
|
||||
description = "health monitor for web services"
|
||||
version = "0.1.0"
|
||||
authors = ["Joakim Hulthe <joakim@hulthe.net>"]
|
||||
license = "MPL-2.0"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
dotenv = "0.13.0"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
log = "0.4.8"
|
||||
futures = "0.3"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
sled = "0.34"
|
||||
semver = "0.11"
|
||||
duplicate = "0.2"
|
||||
handlebars = "3"
|
||||
http = "0.2"
|
||||
ron = "0.6.4"
|
||||
|
||||
[dependencies.rocket]
|
||||
#version = "0.4"
|
||||
git = "https://github.com/SergioBenitez/Rocket"
|
||||
branch = "master"
|
||||
features = ["secrets"]
|
||||
|
||||
[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"]
|
||||
8
example.config.ron
Normal file
8
example.config.ron
Normal file
@ -0,0 +1,8 @@
|
||||
(
|
||||
services: {
|
||||
"example": (
|
||||
url: "https://example.org",
|
||||
mode: Check200,
|
||||
),
|
||||
},
|
||||
)
|
||||
68
src/health/mod.rs
Normal file
68
src/health/mod.rs
Normal file
@ -0,0 +1,68 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use futures::lock::Mutex;
|
||||
use log::info;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub type ServiceId = String;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum HealthStatus {
|
||||
/// The service is up and running
|
||||
Up,
|
||||
|
||||
/// The service gave an error when performing the health check
|
||||
Errored,
|
||||
|
||||
/// The service did not respond to the health check
|
||||
Down,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct ServiceConfig {
|
||||
pub url: String,
|
||||
pub mode: HttpHealthCheckMode,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub enum HttpHealthCheckMode {
|
||||
/// Expect the server to return a 200 status code. The body is ignored.
|
||||
Check200,
|
||||
}
|
||||
|
||||
pub struct HealthState {
|
||||
pub health: HashMap<ServiceId, Mutex<Option<HealthStatus>>>,
|
||||
pub config: HealthConfig,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct HealthConfig {
|
||||
pub services: HashMap<ServiceId, ServiceConfig>,
|
||||
}
|
||||
|
||||
impl HealthState {
|
||||
pub fn new(config: HealthConfig) -> HealthState {
|
||||
HealthState {
|
||||
health: config
|
||||
.services
|
||||
.iter()
|
||||
.map(|(id, _config)| (id.clone(), Mutex::new(None)))
|
||||
.collect(),
|
||||
config,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update(&self) {
|
||||
for (id, status) in &self.health {
|
||||
let url = &self.config.services[id].url;
|
||||
info!("service [{}] querying {}", id, url);
|
||||
let new_status = match reqwest::get(url).await {
|
||||
Err(_) => HealthStatus::Down,
|
||||
Ok(response) if response.status().is_success() => HealthStatus::Up,
|
||||
Ok(_response) => HealthStatus::Errored,
|
||||
};
|
||||
info!("service [{}] new status {:?}", id, new_status);
|
||||
*status.lock().await = Some(new_status);
|
||||
}
|
||||
}
|
||||
}
|
||||
69
src/health/uri.rs
Normal file
69
src/health/uri.rs
Normal file
@ -0,0 +1,69 @@
|
||||
use http::uri::InvalidUri;
|
||||
use rocket::http::hyper::Uri as HyperUri;
|
||||
use serde::{
|
||||
de::{Deserializer, Error, Visitor},
|
||||
ser::Serializer,
|
||||
Deserialize, Serialize,
|
||||
};
|
||||
use std::fmt;
|
||||
use std::ops::Deref;
|
||||
use std::str::FromStr;
|
||||
|
||||
pub struct Uri(HyperUri);
|
||||
|
||||
impl Deref for Uri {
|
||||
type Target = HyperUri;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for Uri {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.0.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
struct UriVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for UriVisitor {
|
||||
type Value = Uri;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("an uri")
|
||||
}
|
||||
|
||||
fn visit_str<E: Error>(self, v: &str) -> Result<Self::Value, E> {
|
||||
match v.parse() {
|
||||
Ok(uri) => Ok(Uri(uri)),
|
||||
Err(e) => Err(E::custom(format!("failed to parse uri: {}", e))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Uri {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Uri, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserializer.deserialize_string(UriVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Uri {
|
||||
type Err = InvalidUri;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Ok(Uri(s.parse()?))
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for Uri {
|
||||
fn to_string(&self) -> String {
|
||||
self.0.to_string()
|
||||
}
|
||||
}
|
||||
36
src/main.rs
Normal file
36
src/main.rs
Normal file
@ -0,0 +1,36 @@
|
||||
mod health;
|
||||
mod routes;
|
||||
|
||||
use dotenv::dotenv;
|
||||
use health::HealthState;
|
||||
use rocket_contrib::serve::StaticFiles;
|
||||
use rocket_contrib::templates::Template;
|
||||
use std::sync::Arc;
|
||||
use std::{env, io};
|
||||
use tokio::fs;
|
||||
|
||||
#[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 = HealthState::new(config);
|
||||
|
||||
let rocket = rocket::ignite()
|
||||
//.attach(Template::custom(|engines| {
|
||||
//handlebars_util::register_helpers(engines)
|
||||
//}))
|
||||
.manage(Arc::new(state))
|
||||
.mount("/static", StaticFiles::from("static"))
|
||||
.mount("/", rocket::routes![routes::pages::index,]);
|
||||
//.register(rocket::catchers![auth::login_page,]);
|
||||
|
||||
rocket.launch().await.expect("rocket failed to launch");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
0
src/routes/api/mod.rs
Normal file
0
src/routes/api/mod.rs
Normal file
2
src/routes/mod.rs
Normal file
2
src/routes/mod.rs
Normal file
@ -0,0 +1,2 @@
|
||||
pub mod api;
|
||||
pub mod pages;
|
||||
22
src/routes/pages/mod.rs
Normal file
22
src/routes/pages/mod.rs
Normal file
@ -0,0 +1,22 @@
|
||||
use crate::health::HealthState;
|
||||
use rocket::{get, State};
|
||||
use std::sync::Arc;
|
||||
use tokio::task::spawn;
|
||||
|
||||
#[get("/")]
|
||||
pub async fn index(state: State<'_, Arc<HealthState>>) -> String {
|
||||
{
|
||||
let state = Arc::clone(state.inner());
|
||||
spawn(async move {
|
||||
state.update().await;
|
||||
});
|
||||
}
|
||||
|
||||
let mut out = String::new();
|
||||
|
||||
for (id, status) in &state.health {
|
||||
out.push_str(&format!("{}: {:?}\n", id, &*status.lock().await));
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
Reference in New Issue
Block a user