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}