libpt_net/monitoring/uptime.rs
1//! # monitor your network uptime
2//!
3//! This method offers a way to monitor your networks/hosts uptime. This is achieved by making
4//! HTTPS requests to a given list of
5//!
6//! Warning: This module is not unit tested.
7
8//// ATTRIBUTES ////////////////////////////////////////////////////////////////////////////////////
9// we want docs
10#![warn(missing_docs)]
11#![warn(rustdoc::missing_crate_level_docs)]
12////////////////////////////////////////////////////////////////////////////////////////////////////
13// we want Debug everywhere.
14#![warn(missing_debug_implementations)]
15////////////////////////////////////////////////////////////////////////////////////////////////////
16// enable clippy's extra lints, the pedantic version
17#![warn(clippy::pedantic)]
18
19use std::{fmt, time::Duration};
20
21//// IMPORTS ///////////////////////////////////////////////////////////////////////////////////////
22use libpt_log::*;
23
24use reqwest;
25
26use humantime::{format_duration, format_rfc3339};
27use std::time::SystemTime;
28
29use serde::{Deserialize, Serialize};
30use serde_json;
31
32use libpt_core::divider;
33
34//// TYPES /////////////////////////////////////////////////////////////////////////////////////////
35
36//// CONSTANTS /////////////////////////////////////////////////////////////////////////////////////
37/// urls used for checking by default
38pub const DEFAULT_CHECK_URLS: &'static [&'static str] =
39 &["https://www.cscherr.de", "https://www.cloudflare.com"];
40
41//// STATICS ///////////////////////////////////////////////////////////////////////////////////////
42
43//// MACROS ////////////////////////////////////////////////////////////////////////////////////////
44
45//// ENUMS /////////////////////////////////////////////////////////////////////////////////////////
46
47//// STRUCTS ///////////////////////////////////////////////////////////////////////////////////////
48/// ## Describes an uptime status
49///
50/// [`UptimeStatus`] describes the result of an uptime check.
51#[derive(Serialize, Deserialize)]
52pub struct UptimeStatus {
53 /// true if the [`UptimeStatus`] is considered successful
54 pub success: bool,
55 /// the percentage of reachable urls out of the total urls
56 pub success_ratio: u8,
57 /// the percentage of reachable urls out of the total urls that need to be reachable in order
58 /// for this [`UptimeStatus`] to be considered a success.
59 pub success_ratio_target: u8,
60 /// the number of reachable [`urls`](UptimeStatus::urls)
61 pub reachable: usize,
62 /// which urls to check in [`check()`](UptimeStatus::check)
63 pub urls: Vec<String>,
64 /// timeout length for requests (in ms)
65 pub timeout: u64,
66}
67
68//// IMPLEMENTATION ////////////////////////////////////////////////////////////////////////////////
69/// Main implementation
70impl UptimeStatus {
71 /// ## create a new `UptimeStatus` and perform it's check
72 pub fn new(success_ratio_target: u8, urls: Vec<String>, timeout: u64) -> Self {
73 assert!(success_ratio_target <= 100);
74 let mut status = UptimeStatus {
75 success: false,
76 success_ratio: 0,
77 success_ratio_target,
78 reachable: 0,
79 urls,
80 timeout,
81 };
82 status.urls.dedup();
83
84 status.check();
85
86 return status;
87 }
88
89 /// ## check for success with the given urls
90 ///
91 /// Makes the actual https requests and updates fields accordingly.
92 ///
93 /// Note: Blocking execution for all requests, timeout is set to
94 /// [REQUEST_TIMEOUT](crate::networking::REQUEST_TIMEOUT).
95 pub fn check(&mut self) {
96 self.reachable = 0;
97 self.urls.iter().for_each(|url| {
98 let client = reqwest::blocking::Client::builder()
99 .timeout(Duration::from_millis(self.timeout))
100 .build()
101 .expect("could not build a client for https requests");
102 let response = client.get(url.clone()).send();
103 if response.is_ok() {
104 self.reachable += 1
105 }
106 });
107 self.calc_success();
108 }
109
110 /// ## calculate the success based on the `reachable` and `total`
111 ///
112 /// Calculates the ratio of [`reachable`](UptimeStatus::reachable) /
113 /// (length of [urls](UptimeStatus::urls)).
114 ///
115 /// Calculates a [`success_ratio`](UptimeStatus::success_ratio) (as [u8]) from that,
116 /// by multiplying with 100, then flooring.
117 ///
118 /// If the [`success_ratio`](UptimeStatus::success_ratio) is greater than or equal to the
119 /// [`success_ratio_target`](UptimeStatus::success_ratio_target), the [`UptimeStatus`] will be
120 /// considered a success.
121 ///
122 /// In the special case that no URLs to check for have been provided, the check will be
123 /// considered a success, but the [`success_ratio`](UptimeStatus::success_ratio) will be `0`.
124 ///
125 /// Note: does not check for networking, use [`check()`](UptimeStatus::check) for that.
126 pub fn calc_success(&mut self) {
127 // if no urls need to be checked, success without checking
128 if self.urls.len() == 0 {
129 self.success = true;
130 self.success_ratio = 0;
131 return;
132 }
133 let ratio: f32 = (self.reachable as f32) / (self.urls.len() as f32) * 100f32;
134 trace!("calculated success_ratio: {}", ratio);
135 self.success_ratio = ratio.floor() as u8;
136 self.success = self.success_ratio >= self.success_ratio_target;
137 trace!("calculated success as: {}", self.success)
138 }
139}
140
141////////////////////////////////////////////////////////////////////////////////////////////////////
142impl fmt::Debug for UptimeStatus {
143 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
144 let mut urls: Vec<&str> = Vec::new();
145 for url in &self.urls {
146 urls.push(url.as_str());
147 }
148 write!(f, "{}", serde_json::to_string(self).unwrap())
149 }
150}
151
152////////////////////////////////////////////////////////////////////////////////////////////////////
153impl fmt::Display for UptimeStatus {
154 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
155 let mut urls: Vec<&str> = Vec::new();
156 for url in &self.urls {
157 urls.push(url.as_str());
158 }
159 write!(f, "{}", serde_json::to_string_pretty(self).unwrap())
160 }
161}
162
163//// PUBLIC FUNCTIONS //////////////////////////////////////////////////////////////////////////////
164/// ## Uptime monitor
165///
166/// This function continuously monitors the uptime of your host/network.
167///
168/// On change of status, an update will be logged at [INFO Level](log::Level::Info), containing
169/// information on your current status, including timestamps of the last up/down time and durations
170/// since.
171pub fn continuous_uptime_monitor(
172 success_ratio_target: u8,
173 urls: Vec<String>,
174 interval: u64,
175 timeout: u64,
176) {
177 if urls.len() == 0 {
178 error!("No URLs provided. There is nothing to monitor.");
179 return;
180 }
181
182 let interval = std::time::Duration::from_millis(interval);
183 let mut last_downtime: Option<SystemTime> = None;
184 let mut last_uptime: Option<SystemTime> = None;
185 let mut status = UptimeStatus::new(success_ratio_target, urls, timeout);
186 // we assume that the last status was up, so the binary shows the first status if its a
187 // failure.
188 let mut last_was_up: bool = true;
189 let mut last_ratio: u8 = status.success_ratio;
190 loop {
191 trace!(
192 ?status,
193 ?last_was_up,
194 "loop iteration for continuous uptime monitor"
195 );
196 if !status.success {
197 if last_was_up {
198 trace!("displaying status");
199 display_uptime_status("fail", last_uptime, last_downtime, &status)
200 }
201 last_downtime = Some(SystemTime::now());
202 last_was_up = false;
203 } else if status.success_ratio < 100 {
204 if status.success_ratio != last_ratio {
205 let msg = format!(
206 "uptime check: not all urls are reachable ({}%)",
207 status.success_ratio
208 );
209 display_uptime_status(&msg, last_uptime, last_downtime, &status)
210 }
211 last_uptime = Some(SystemTime::now());
212 last_was_up = true;
213 } else {
214 if !last_was_up {
215 display_uptime_status("success", last_uptime, last_downtime, &status)
216 }
217 last_uptime = Some(SystemTime::now());
218 last_was_up = true;
219 }
220
221 last_ratio = status.success_ratio;
222 std::thread::sleep(interval);
223 status.check();
224 }
225}
226
227//// PRIVATE FUNCTIONS /////////////////////////////////////////////////////////////////////////////
228/// Displays the current status for the [continuous uptime monitor](continuous_uptime_monitor)
229fn display_uptime_status(
230 msg: &str,
231 last_uptime: Option<SystemTime>,
232 last_downtime: Option<SystemTime>,
233 status: &UptimeStatus,
234) {
235 // I know it's weird that this has two spaces too much, but somehow just the tabs is missing
236 // two spaces.
237 info!("uptime check: {}", msg);
238 info!("last uptime: {}", match_format_time(last_uptime));
239 info!("last downtime: {}", match_format_time(last_downtime));
240 info!(
241 "since downtime: {}",
242 match_format_duration_since(last_downtime)
243 );
244 info!(
245 "since uptime: {}",
246 match_format_duration_since(last_uptime)
247 );
248 debug!("\n{}", status);
249 info!("{}", divider!());
250}
251
252////////////////////////////////////////////////////////////////////////////////////////////////////
253/// Returns "None" if the given [Option] is [None](Option::None). Otherwise, returns the time stamp
254/// formatted according to rfc3999.
255fn match_format_time(time: Option<SystemTime>) -> String {
256 match time {
257 Some(time) => format_rfc3339(time).to_string(),
258 None => String::from("None"),
259 }
260}
261
262////////////////////////////////////////////////////////////////////////////////////////////////////
263/// Returns "None" if the given [Option] is [None](Option::None). Otherwise, returns duration since
264/// that time in a human readable format.
265fn match_format_duration_since(time: Option<SystemTime>) -> String {
266 match time {
267 Some(time) => format_duration(
268 SystemTime::now()
269 .duration_since(time)
270 .expect("could not calculate elapsed time"),
271 )
272 .to_string(),
273 None => String::from("None"),
274 }
275}