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