#![warn(missing_docs)]
#![warn(rustdoc::missing_crate_level_docs)]
#![warn(missing_debug_implementations)]
#![warn(clippy::pedantic)]
use std::{fmt, time::Duration};
use libpt_log::*;
use reqwest;
use humantime::{format_duration, format_rfc3339};
use std::time::SystemTime;
use serde::{Deserialize, Serialize};
use serde_json;
use libpt_core::divider;
pub const DEFAULT_CHECK_URLS: &'static [&'static str] =
&["https://www.cscherr.de", "https://www.cloudflare.com"];
#[derive(Serialize, Deserialize)]
pub struct UptimeStatus {
pub success: bool,
pub success_ratio: u8,
pub success_ratio_target: u8,
pub reachable: usize,
pub urls: Vec<String>,
pub timeout: u64,
}
impl UptimeStatus {
pub fn new(success_ratio_target: u8, urls: Vec<String>, timeout: u64) -> Self {
assert!(success_ratio_target <= 100);
let mut status = UptimeStatus {
success: false,
success_ratio: 0,
success_ratio_target,
reachable: 0,
urls,
timeout,
};
status.urls.dedup();
status.check();
return status;
}
pub fn check(&mut self) {
self.reachable = 0;
self.urls.iter().for_each(|url| {
let client = reqwest::blocking::Client::builder()
.timeout(Duration::from_millis(self.timeout))
.build()
.expect("could not build a client for https requests");
let response = client.get(url.clone()).send();
if response.is_ok() {
self.reachable += 1
}
});
self.calc_success();
}
pub fn calc_success(&mut self) {
if self.urls.len() == 0 {
self.success = true;
self.success_ratio = 0;
return;
}
let ratio: f32 = (self.reachable as f32) / (self.urls.len() as f32) * 100f32;
trace!("calculated success_ratio: {}", ratio);
self.success_ratio = ratio.floor() as u8;
self.success = self.success_ratio >= self.success_ratio_target;
trace!("calculated success as: {}", self.success)
}
}
impl fmt::Debug for UptimeStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut urls: Vec<&str> = Vec::new();
for url in &self.urls {
urls.push(url.as_str());
}
write!(f, "{}", serde_json::to_string(self).unwrap())
}
}
impl fmt::Display for UptimeStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut urls: Vec<&str> = Vec::new();
for url in &self.urls {
urls.push(url.as_str());
}
write!(f, "{}", serde_json::to_string_pretty(self).unwrap())
}
}
pub fn continuous_uptime_monitor(
success_ratio_target: u8,
urls: Vec<String>,
interval: u64,
timeout: u64,
) {
if urls.len() == 0 {
error!("No URLs provided. There is nothing to monitor.");
return;
}
let interval = std::time::Duration::from_millis(interval);
let mut last_downtime: Option<SystemTime> = None;
let mut last_uptime: Option<SystemTime> = None;
let mut status = UptimeStatus::new(success_ratio_target, urls, timeout);
let mut last_was_up: bool = true;
let mut last_ratio: u8 = status.success_ratio;
loop {
trace!(
?status,
?last_was_up,
"loop iteration for continuous uptime monitor"
);
if !status.success {
if last_was_up {
trace!("displaying status");
display_uptime_status("fail", last_uptime, last_downtime, &status)
}
last_downtime = Some(SystemTime::now());
last_was_up = false;
} else if status.success_ratio < 100 {
if status.success_ratio != last_ratio {
let msg = format!(
"uptime check: not all urls are reachable ({}%)",
status.success_ratio
);
display_uptime_status(&msg, last_uptime, last_downtime, &status)
}
last_uptime = Some(SystemTime::now());
last_was_up = true;
} else {
if !last_was_up {
display_uptime_status("success", last_uptime, last_downtime, &status)
}
last_uptime = Some(SystemTime::now());
last_was_up = true;
}
last_ratio = status.success_ratio;
std::thread::sleep(interval);
status.check();
}
}
fn display_uptime_status(
msg: &str,
last_uptime: Option<SystemTime>,
last_downtime: Option<SystemTime>,
status: &UptimeStatus,
) {
info!("uptime check: {}", msg);
info!("last uptime: {}", match_format_time(last_uptime));
info!("last downtime: {}", match_format_time(last_downtime));
info!(
"since downtime: {}",
match_format_duration_since(last_downtime)
);
info!(
"since uptime: {}",
match_format_duration_since(last_uptime)
);
debug!("\n{}", status);
info!("{}", divider!());
}
fn match_format_time(time: Option<SystemTime>) -> String {
match time {
Some(time) => format_rfc3339(time).to_string(),
None => String::from("None"),
}
}
fn match_format_duration_since(time: Option<SystemTime>) -> String {
match time {
Some(time) => format_duration(
SystemTime::now()
.duration_since(time)
.expect("could not calculate elapsed time"),
)
.to_string(),
None => String::from("None"),
}
}