Initial Commit

This commit is contained in:
2021-01-31 00:39:30 +01:00
commit 6085860bc6
10 changed files with 2732 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target
config.ron

2483
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

42
Cargo.toml Normal file
View 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
View File

@ -0,0 +1,8 @@
(
services: {
"example": (
url: "https://example.org",
mode: Check200,
),
},
)

68
src/health/mod.rs Normal file
View 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
View 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
View 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
View File

2
src/routes/mod.rs Normal file
View File

@ -0,0 +1,2 @@
pub mod api;
pub mod pages;

22
src/routes/pages/mod.rs Normal file
View 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
}