init commit

This commit is contained in:
Nicholas Orlowsky 2024-05-15 22:12:14 +02:00
commit edc0dd01e8
Signed by: nickorlow
GPG key ID: 838827D8C4611687
20 changed files with 2920 additions and 0 deletions

236
src/main.rs Normal file
View file

@ -0,0 +1,236 @@
use axum::{routing::get, Router};
use chrono::DateTime;
use chrono::offset::Utc;
use log::*;
#[macro_use]
extern crate dotenv_codegen;
extern crate dotenv;
use dotenv::dotenv;
use std::env;
mod uptime_service;
use uptime_service::{UptimeService, Uptime, UptimeType, UptimeStatus};
#[derive(askama::Template)]
#[template(path = "layout.html")]
struct ContentTemplate <T: askama::Template> {
content: T
}
#[derive(askama::Template)]
#[template(path = "layout.html")]
struct RawContentTemplate {
content: String
}
struct UptimeInfo {
name: String,
uptime: String,
response_time: String,
status: String,
url: Option<String>
}
#[derive(askama::Template)]
#[template(path = "index.html")]
struct IndexTemplate {
uptime_infos: Vec<UptimeInfo>,
last_updated: String
}
#[derive(askama::Template)]
#[template(path = "system_status.html")]
struct StatusTemplate {
dctr_uptime_infos: Vec<UptimeInfo>,
svc_uptime_infos: Vec<UptimeInfo>,
last_updated: String
}
struct BlogInfo {
title: String,
date: String,
url: String
}
#[derive(askama::Template)]
#[template(path = "blog.html")]
struct BlogTemplate {
blogs: Vec<BlogInfo>
}
#[derive(askama::Template)]
#[template(path = "dashboard.html")]
struct DashboardTemplate {}
#[derive(Clone)]
struct AppState {
uptime_service: UptimeService
}
#[tokio::main]
async fn main() {
dotenv().ok();
env_logger::init();
info!("Starting Sharpe Mountain Compute Website");
let uptime_service: UptimeService = UptimeService::new();
uptime_service.start();
let state = AppState { uptime_service };
let app = Router::new()
.route("/", get(index_handler))
.route("/system_status", get(status_handler))
.route("/dashboard", get(dashboard_handler))
.route("/blog", get(blog_handler))
.route("/blogs/:blog_name", get(single_blog_handler))
.nest_service("/assets", tower_http::services::ServeDir::new("assets"))
.with_state(state);
let port_num = env::var("EXPOSE_PORT")
.unwrap_or("3000".to_string());
let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{}", port_num))
.await
.unwrap();
info!("Listening on port {}", port_num);
axum::serve(listener, app).await.unwrap();
}
async fn blog_handler() -> Result<ContentTemplate<impl askama::Template>, (axum::http::StatusCode, String)> {
Ok(ContentTemplate { content: BlogTemplate{ blogs: vec![
BlogInfo {
title: String::from("Goodbye, NWS"),
date: String::from("May 15th, 2024"),
url: String::from("goodbye-nws"),
},
BlogInfo {
title: String::from("Downtime Incident Postmortem"),
date: String::from("November 11th, 2023"),
url: String::from("11-08-2023-postmortem"),
},
BlogInfo {
title: String::from("SSL on Container Deployment Service (at nickorlow.com)"),
date: String::from("July 12th, 2023"),
url: String::from("https://nickorlow.com/blogs/side-project-7-12-23.html"),
},
] } })
}
async fn single_blog_handler(
axum::extract::Path((blog_name)): axum::extract::Path<(String)>
) -> Result<RawContentTemplate, (axum::http::StatusCode, String)> {
let blog_content = match std::fs::read_to_string(format!("templates/blogs/{}.html", blog_name)) {
Ok(ctn) => ctn,
_ => String::from("<h1>Not Found!</h1>")
};
Ok(RawContentTemplate { content: blog_content })
}
async fn dashboard_handler() -> Result<ContentTemplate<impl askama::Template>, (axum::http::StatusCode, String)> {
Ok(ContentTemplate { content: DashboardTemplate{} })
}
async fn index_handler(
axum::extract::State(state): axum::extract::State<AppState>,
)
-> Result<ContentTemplate<impl askama::Template>, (axum::http::StatusCode, String)> {
let uptimes: Vec<Uptime> = state.uptime_service.get_data();
let lu: DateTime<Utc> = state.uptime_service.get_last_updated().into();
let lu_str = format!("{} UTC", lu.format("%B %e, %Y %T"));
let mut uptime_infos: Vec<UptimeInfo> = vec![];
for uptime in uptimes {
if uptime.uptime_type != UptimeType::Provider {
continue;
}
uptime_infos.push(
UptimeInfo {
name: uptime.name,
uptime: uptime.uptime,
response_time: uptime.response_time,
status: match uptime.status {
UptimeStatus::Up => String::from("Up"),
UptimeStatus::Down => String::from("DOWN"),
UptimeStatus::Maintenance => String::from("Undergoing Maintenance"),
_ => String::from("Unknown")
},
url: None
}
);
}
let index_template = IndexTemplate {
uptime_infos,
last_updated: lu_str
};
Ok(ContentTemplate { content: index_template })
}
async fn status_handler(
axum::extract::State(state): axum::extract::State<AppState>,
)
-> Result<ContentTemplate<impl askama::Template>, (axum::http::StatusCode, String)> {
let uptimes: Vec<Uptime> = state.uptime_service.get_data();
let lu: DateTime<Utc> = state.uptime_service.get_last_updated().into();
let lu_str = format!("{} UTC", lu.format("%B %e, %Y %T"));
let mut dc_uptime_infos: Vec<UptimeInfo> = vec![];
let mut sv_uptime_infos: Vec<UptimeInfo> = vec![];
for uptime in uptimes {
match uptime.uptime_type {
UptimeType::Datacenter => {
dc_uptime_infos.push(
UptimeInfo {
name: uptime.name,
uptime: uptime.uptime,
response_time: uptime.response_time,
status: match uptime.status {
UptimeStatus::Up => String::from("Up"),
UptimeStatus::Down => String::from("DOWN"),
UptimeStatus::Maintenance => String::from("Undergoing Maintenance"),
_ => String::from("Unknown")
},
url: None
}
);
},
UptimeType::Service => {
sv_uptime_infos.push(
UptimeInfo {
name: uptime.name,
uptime: uptime.uptime,
response_time: uptime.response_time,
status: match uptime.status {
UptimeStatus::Up => String::from("Up"),
UptimeStatus::Down => String::from("DOWN"),
UptimeStatus::Maintenance => String::from("Undergoing Maintenance"),
_ => String::from("Unknown")
},
url: Some(uptime.url)
}
);
}
_ => continue
}
}
let service_template = StatusTemplate {
dctr_uptime_infos: dc_uptime_infos,
svc_uptime_infos: sv_uptime_infos,
last_updated: lu_str
};
Ok(ContentTemplate { content: service_template })
}

232
src/uptime_service.rs Normal file
View file

@ -0,0 +1,232 @@
use std::collections::HashMap;
use tokio::time::{sleep};
use std::sync::{Arc, Mutex};
use std::time::{Duration};
use chrono::{Datelike, NaiveDate};
use std::time::{SystemTime, UNIX_EPOCH};
use anyhow::Context;
use anyhow::{anyhow};
use log::*;
#[macro_use]
use dotenv::dotenv;
use std::env;
#[derive(Debug, PartialEq, Clone)]
pub enum UptimeType {
Provider,
Service,
Datacenter,
Unknown
}
#[derive(Debug, PartialEq, Clone)]
pub enum UptimeStatus {
Up,
Down,
Maintenance,
Unknown
}
#[derive(Debug, Clone)]
pub struct Uptime {
pub name: String,
pub uptime: String,
pub response_time: String,
pub status: UptimeStatus,
pub uptime_type: UptimeType,
pub url: String
}
#[derive(Debug, Clone)]
pub struct UptimeServiceState {
uptimes: Vec<Uptime>,
last_updated: SystemTime
}
#[derive(Debug, Clone)]
pub struct UptimeService {
state: Arc<Mutex<UptimeServiceState>>
}
impl UptimeService {
const UPDATE_SECONDS: u64 = 300;
pub fn new() -> Self {
let init_state = Arc::new(Mutex::new(
UptimeServiceState { uptimes: vec![], last_updated: UNIX_EPOCH }
));
Self { state: init_state }
}
pub fn start(&self) {
info!("Starting UptimeService");
let cloned_state = Arc::clone(&self.state);
tokio::spawn(async move {
loop {
let clonedx_state = Arc::clone(&cloned_state);
let res = Self::update_data(clonedx_state).await;
match res {
Err(err) => {
error!("{}", err);
},
_ => {}
}
sleep(tokio::time::Duration::from_secs(Self::UPDATE_SECONDS)).await;
}
});
}
pub fn get_data(&self) -> Vec<Uptime> {
let state = self.state.lock().unwrap();
let uptimes = state.uptimes.clone();
return uptimes;
}
pub fn get_last_updated(&self) -> SystemTime {
let state = self.state.lock().unwrap();
let lu = state.last_updated.clone();
return lu;
}
async fn update_data(arc_state: Arc<Mutex<UptimeServiceState>>) -> ::anyhow::Result<()> {
debug!("Starting data update for UptimeService");
let mut request_vars = HashMap::new();
let api_key = env::var("UPTIMEROBOT_API_KEY")?;
request_vars.insert("api_key", api_key.as_str());
request_vars.insert("all_time_uptime_ratio", "1");
let now = SystemTime::now();
//let thirty_days_ago = now - Duration::from_secs(30 * 24 * 3600);
let current_year = chrono::Utc::today().year();
let january_1st = NaiveDate::from_ymd(current_year, 1, 1).and_hms(0, 0, 0);
let duration = january_1st.signed_duration_since(NaiveDate::from_ymd(1970, 1, 1).and_hms(0, 0, 0));
let year_start = UNIX_EPOCH + Duration::from_secs(duration.num_seconds() as u64);
//let ranges = &format!(
// "{}_{}-{}_{}",
// thirty_days_ago.duration_since(SystemTime::UNIX_EPOCH)?.as_secs(),
// now.duration_since(SystemTime::UNIX_EPOCH)?.as_secs(),
// year_start.duration_since(SystemTime::UNIX_EPOCH)?.as_secs(),
// now.duration_since(SystemTime::UNIX_EPOCH)?.as_secs(),
//);
let ranges = &format!(
"{}_{}",
year_start.duration_since(SystemTime::UNIX_EPOCH)?.as_secs(),
now.duration_since(SystemTime::UNIX_EPOCH)?.as_secs(),
);
request_vars.insert("custom_uptime_ranges", ranges);
request_vars.insert("response_times_average", "1440");
request_vars.insert("response_times", "1");
let client = reqwest::Client::new();
let res = client.post("https://api.uptimerobot.com/v2/getMonitors")
.form(&request_vars)
.send()
.await?;
let resp = res.json::<serde_json::Value>()
.await?;
let monitors = resp.get("monitors")
.context("Response did not have a monitors subobject")?
.as_array()
.context("Monitors subobject was not an array")?;
let mut state = match arc_state.lock(){
Ok(val) => val,
Err(_) => {return Err(anyhow!("Could not lock shared state"));}
};
state.uptimes.clear();
for monitor in monitors {
let monitor_fqn = monitor.get("friendly_name")
.context("Monitor did not have property 'friendly_name'")?;
debug!("Monitor '{}' processing", monitor_fqn);
let split_str: Vec<&str> = monitor_fqn.as_str()
.context("Expected 'friendly_name' to be a string")?
.split(".").collect();
if split_str.len() != 2 {
debug!("Monitor '{}' excluded due to bad format", monitor_fqn);
continue;
}
let monitor_nt = String::from(*split_str.get(0).context("Expected name to have first part")?);
let monitor_name = String::from(*split_str.get(1).context("Expected name to have second part")?);
let monitor_type = match monitor_nt.as_str() {
"datacenter" => UptimeType::Datacenter,
"service" => UptimeType::Service,
"competitor" => UptimeType::Provider,
_ => UptimeType::Unknown
};
if monitor_type == UptimeType::Unknown {
debug!("Monitor '{}' excluded due to unknown type", monitor_fqn);
continue;
}
let monitor_status_num = monitor.get("status")
.context("Expected monitor to have 'status' property")?
.as_u64()
.context("Expected 'status' property to be u64")?;
let monitor_status = match monitor_status_num {
0 => UptimeStatus::Maintenance,
1 | 8 | 9 => UptimeStatus::Down,
2 => UptimeStatus::Up,
_ => UptimeStatus::Unknown
};
if monitor_status == UptimeStatus::Unknown {
debug!("Monitor '{}' excluded due to unknown status (status was {})", monitor_fqn, monitor_status_num);
continue;
}
let monitor_rt_val = monitor.get("average_response_time")
.context("Expected monitor to have property 'average_response_time'")?;
// Because UptimeRobot has the world's worst API ever
// and decided that it's okay to return multiple datatypes
// for one property based on how they're feeling
let monitor_rt = match monitor_rt_val.as_str() {
Some(string) => format!("{}ms", string),
_ => format!("N/A")
};
let monitor_uptime = format!("{}%",
monitor.get("custom_uptime_ranges")
.context("Expected monitor to have property 'custom_uptime_ranges'")?
.as_str()
.context("Expected 'custom_uptime_ranges' to be String")?
);
let monitor_url = String::from(monitor.get("url")
.context("Expected monitor to have property 'url'")?
.as_str()
.context("Expected 'url' to be String")?);
;
state.uptimes.push(
Uptime {
name: monitor_name,
uptime: monitor_uptime,
response_time: monitor_rt,
status: monitor_status,
uptime_type: monitor_type,
url: monitor_url
}
);
}
state.last_updated = SystemTime::now();
Ok(())
}
}