check_jitter/
lib.rs

1use log::{debug, error, info};
2use nagios_range::Error as RangeError;
3use nagios_range::NagiosRange as ThresholdRange;
4use rand::Rng;
5use std::fmt;
6use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, ToSocketAddrs};
7use std::time::{Duration, Instant};
8use thiserror::Error;
9
10#[derive(Debug)]
11pub enum SocketType {
12    Datagram,
13    Raw,
14}
15
16impl fmt::Display for SocketType {
17    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
18        match self {
19            SocketType::Datagram => write!(f, "Datagram"),
20            SocketType::Raw => write!(f, "Raw"),
21        }
22    }
23}
24
25#[derive(Copy, Clone, Debug, PartialEq, Eq)]
26pub enum AggregationMethod {
27    Average,
28    Median,
29    Max,
30    Min,
31}
32
33impl std::str::FromStr for AggregationMethod {
34    type Err = String;
35
36    fn from_str(s: &str) -> Result<Self, Self::Err> {
37        match s.to_lowercase().as_str() {
38            "average" => Ok(AggregationMethod::Average),
39            "avg" => Ok(AggregationMethod::Average),
40            "mean" => Ok(AggregationMethod::Average),
41            "median" => Ok(AggregationMethod::Median),
42            "med" => Ok(AggregationMethod::Median),
43            "minimum" => Ok(AggregationMethod::Min),
44            "min" => Ok(AggregationMethod::Min),
45            "maximum" => Ok(AggregationMethod::Max),
46            "max" => Ok(AggregationMethod::Max),
47            _ => Err(format!("'{}' is not a valid aggregation method", s)),
48        }
49    }
50}
51
52impl fmt::Display for AggregationMethod {
53    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
54        match self {
55            AggregationMethod::Average => write!(f, "Average"),
56            AggregationMethod::Median => write!(f, "Median"),
57            AggregationMethod::Max => write!(f, "Max"),
58            AggregationMethod::Min => write!(f, "Min"),
59        }
60    }
61}
62
63#[derive(Debug)]
64pub struct PingErrorWrapper(ping::Error);
65
66impl PartialEq for PingErrorWrapper {
67    fn eq(&self, other: &Self) -> bool {
68        format!("{:?}", self.0) == format!("{:?}", other.0)
69    }
70}
71
72impl Eq for PingErrorWrapper {}
73
74impl fmt::Display for PingErrorWrapper {
75    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76        write!(f, "{:?}", self.0)
77    }
78}
79
80impl std::error::Error for PingErrorWrapper {}
81
82#[non_exhaustive]
83#[derive(Error, Debug, Eq, PartialEq)]
84pub enum CheckJitterError {
85    #[error("DNS Lookup failed for: {0}")]
86    DnsLookupFailed(String),
87
88    #[error("DNS resolution error for '{addr}': {error}")]
89    DnsResolutionError { addr: String, error: String },
90
91    #[error("The delta count is 0. Cannot calculate jitter.")]
92    EmptyDeltas,
93
94    #[error("At least 2 samples are required to calculate jitter, got {0}.")]
95    InsufficientSamples(u8),
96
97    #[error("Invalid IP: {0}")]
98    InvalidIP(String),
99
100    #[error("Ping failed because of insufficient permissions")]
101    PermissionDenied,
102
103    #[error("Ping failed with error: {0}")]
104    PingError(PingErrorWrapper),
105
106    #[error("Ping failed with IO error: {0}")]
107    PingIoError(String),
108
109    #[error("Ping timed out after: {0}ms")]
110    Timeout(String),
111
112    #[error("Unable to parse hostname: {0}")]
113    UrlParseError(url::ParseError),
114}
115
116impl From<std::io::Error> for CheckJitterError {
117    fn from(err: std::io::Error) -> Self {
118        match err.kind() {
119            std::io::ErrorKind::PermissionDenied => CheckJitterError::PermissionDenied,
120            _ => CheckJitterError::PingIoError(err.to_string()),
121        }
122    }
123}
124
125#[derive(Clone, Debug, PartialEq)]
126pub struct Thresholds {
127    pub warning: Option<ThresholdRange>,
128    pub critical: Option<ThresholdRange>,
129}
130
131#[non_exhaustive]
132#[derive(Debug, PartialEq)]
133pub enum UnknownVariant {
134    Error(CheckJitterError),
135    FailedToInitLogger(String),
136    InvalidAddr(String),
137    InvalidMinMaxInterval(u64, u64),
138    ClapError(String),
139    NoThresholds,
140    RangeParseError(String, RangeError),
141    Timeout(Duration),
142}
143
144#[derive(Debug, PartialEq)]
145pub enum Status<'a> {
146    Ok(AggregationMethod, f64, &'a Thresholds),
147    Warning(AggregationMethod, f64, &'a Thresholds),
148    Critical(AggregationMethod, f64, &'a Thresholds),
149    Unknown(UnknownVariant),
150}
151
152fn display_string(label: &str, status: &str, uom: &str, f: f64, t: &Thresholds) -> String {
153    let min: f64 = 0.0;
154    match (t.warning, t.critical) {
155        (Some(w), Some(c)) => {
156            format!("{status} - {label}: {f}{uom}|'{label}'={f}{uom};{w};{c};{min}")
157        }
158        (Some(w), None) => format!("{status} - {label}: {f}{uom}|'{label}'={f}{uom};{w};;{min}"),
159        (None, Some(c)) => format!("{status} - {label}: {f}{uom}|'{label}'={f}{uom};;{c};{min}"),
160        (None, None) => format!("{status} - {label}: {f}{uom}|'{label}'={f}{uom};;;{min}"),
161    }
162}
163
164#[cfg(test)]
165mod display_string_tests {
166    use super::*;
167    use pretty_assertions::assert_eq;
168
169    #[test]
170    fn test_with_both_thresholds() {
171        let thresholds = Thresholds {
172            warning: Some(ThresholdRange::from("0:0.5").unwrap()),
173            critical: Some(ThresholdRange::from("0:1").unwrap()),
174        };
175
176        let expected = "OK - Average Jitter: 0.1ms|'Average Jitter'=0.1ms;0:0.5;0:1;0";
177        let actual = display_string("Average Jitter", "OK", "ms", 0.1, &thresholds);
178
179        assert_eq!(actual, expected);
180    }
181
182    #[test]
183    fn test_with_only_warning() {
184        let thresholds = Thresholds {
185            warning: Some(ThresholdRange::from("0:0.5").unwrap()),
186            critical: None,
187        };
188
189        let expected = "OK - Average Jitter: 0.1ms|'Average Jitter'=0.1ms;0:0.5;;0";
190        let actual = display_string("Average Jitter", "OK", "ms", 0.1, &thresholds);
191
192        assert_eq!(actual, expected);
193    }
194
195    #[test]
196    fn test_with_only_critical() {
197        let thresholds = Thresholds {
198            warning: None,
199            critical: Some(ThresholdRange::from("0:0.5").unwrap()),
200        };
201
202        let expected = "OK - Average Jitter: 0.1ms|'Average Jitter'=0.1ms;;0:0.5;0";
203        let actual = display_string("Average Jitter", "OK", "ms", 0.1, &thresholds);
204
205        assert_eq!(actual, expected);
206    }
207
208    #[test]
209    fn test_with_no_thresholds() {
210        let thresholds = Thresholds {
211            warning: None,
212            critical: None,
213        };
214
215        let expected = "OK - Average Jitter: 0.1ms|'Average Jitter'=0.1ms;;;0";
216        let actual = display_string("Average Jitter", "OK", "ms", 0.1, &thresholds);
217
218        assert_eq!(actual, expected);
219    }
220}
221
222impl fmt::Display for Status<'_> {
223    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
224        let uom = "ms";
225        let label = match self {
226            Status::Ok(AggregationMethod::Average, _, _) => "Average Jitter",
227            Status::Ok(AggregationMethod::Median, _, _) => "Median Jitter",
228            Status::Ok(AggregationMethod::Max, _, _) => "Max Jitter",
229            Status::Ok(AggregationMethod::Min, _, _) => "Min Jitter",
230            Status::Warning(AggregationMethod::Average, _, _) => "Average Jitter",
231            Status::Warning(AggregationMethod::Median, _, _) => "Median Jitter",
232            Status::Warning(AggregationMethod::Max, _, _) => "Max Jitter",
233            Status::Warning(AggregationMethod::Min, _, _) => "Min Jitter",
234            Status::Critical(AggregationMethod::Average, _, _) => "Average Jitter",
235            Status::Critical(AggregationMethod::Median, _, _) => "Median Jitter",
236            Status::Critical(AggregationMethod::Max, _, _) => "Max Jitter",
237            Status::Critical(AggregationMethod::Min, _, _) => "Min Jitter",
238            Status::Unknown(_) => "Unknown",
239        };
240
241        match self {
242            Status::Ok(_, n, t) => {
243                write!(f, "{}", display_string(label, "OK", uom, *n, t))
244            }
245            Status::Warning(_, n, t) => {
246                write!(f, "{}", display_string(label, "WARNING", uom, *n, t))
247            }
248            Status::Critical(_, n, t) => {
249                write!(f, "{}", display_string(label, "CRITICAL", uom, *n, t))
250            }
251            Status::Unknown(UnknownVariant::Error(e)) => {
252                write!(f, "UNKNOWN - An error occurred: '{}'", e)
253            }
254            Status::Unknown(UnknownVariant::FailedToInitLogger(s)) => {
255                write!(
256                    f,
257                    "UNKNOWN - Failed to initialize logger with error: '{}'",
258                    s
259                )
260            }
261            Status::Unknown(UnknownVariant::InvalidAddr(s)) => {
262                write!(f, "UNKNOWN - Invalid address or hostname: {}", s)
263            }
264            Status::Unknown(UnknownVariant::InvalidMinMaxInterval(min, max)) => {
265                write!(
266                    f,
267                    "UNKNOWN - Invalid min/max interval: min: {}, max: {}",
268                    min, max
269                )
270            }
271            Status::Unknown(UnknownVariant::ClapError(s)) => {
272                let trimmed = s.trim_end();
273                let without_leading_error = trimmed.trim_start_matches("error: ");
274                write!(
275                    f,
276                    "UNKNOWN - Command line parsing produced an error: {}",
277                    without_leading_error,
278                )
279            }
280            Status::Unknown(UnknownVariant::NoThresholds) => {
281                write!(
282                    f,
283                    "UNKNOWN - No thresholds provided. Provide at least one threshold."
284                )
285            }
286            Status::Unknown(UnknownVariant::RangeParseError(s, e)) => {
287                write!(
288                    f,
289                    "UNKNOWN - Unable to parse range '{}' with error: {}",
290                    s, e
291                )
292            }
293            Status::Unknown(UnknownVariant::Timeout(d)) => {
294                write!(f, "UNKNOWN - Ping timeout occurred after {:?}", d)
295            }
296        }
297    }
298}
299
300#[cfg(test)]
301mod status_display_tests {
302    use super::*;
303    use pretty_assertions::assert_eq;
304
305    #[test]
306    fn test_with_ok() {
307        let t = Thresholds {
308            warning: Some(ThresholdRange::from("0:0.5").unwrap()),
309            critical: Some(ThresholdRange::from("0:1").unwrap()),
310        };
311        let status = Status::Ok(AggregationMethod::Average, 0.1, &t);
312        let expected = "OK - Average Jitter: 0.1ms|'Average Jitter'=0.1ms;0:0.5;0:1;0";
313        let actual = format!("{}", status);
314
315        assert_eq!(actual, expected);
316    }
317
318    #[test]
319    // The expected value is the same as the previous test, even if the str given to initiate
320    // the ThresholdRange is different.
321    fn test_with_ok_simple_thresholds() {
322        let t = Thresholds {
323            warning: Some(ThresholdRange::from("0.5").unwrap()),
324            critical: Some(ThresholdRange::from("1").unwrap()),
325        };
326        let status = Status::Ok(AggregationMethod::Median, 0.1, &t);
327        let expected = "OK - Median Jitter: 0.1ms|'Median Jitter'=0.1ms;0:0.5;0:1;0";
328        let actual = format!("{}", status);
329
330        assert_eq!(actual, expected);
331    }
332
333    #[test]
334    fn test_with_warning() {
335        let t = Thresholds {
336            warning: Some(ThresholdRange::from("0:0.5").unwrap()),
337            critical: Some(ThresholdRange::from("0:1").unwrap()),
338        };
339        let status = Status::Warning(AggregationMethod::Average, 0.1, &t);
340        let expected = "WARNING - Average Jitter: 0.1ms|'Average Jitter'=0.1ms;0:0.5;0:1;0";
341        let actual = format!("{}", status);
342
343        assert_eq!(actual, expected);
344    }
345
346    #[test]
347    fn test_with_critical() {
348        let t = Thresholds {
349            warning: Some(ThresholdRange::from("0:0.5").unwrap()),
350            critical: Some(ThresholdRange::from("0:1").unwrap()),
351        };
352        let status = Status::Critical(AggregationMethod::Max, 0.1, &t);
353        let expected = "CRITICAL - Max Jitter: 0.1ms|'Max Jitter'=0.1ms;0:0.5;0:1;0";
354        let actual = format!("{}", status);
355
356        assert_eq!(actual, expected);
357    }
358
359    #[test]
360    fn test_with_error() {
361        let status = Status::Unknown(UnknownVariant::Error(CheckJitterError::DnsLookupFailed(
362            "example.com".to_string(),
363        )));
364
365        let expected = "UNKNOWN - An error occurred: 'DNS Lookup failed for: example.com'";
366        let actual = format!("{}", status);
367
368        assert_eq!(actual, expected);
369    }
370}
371
372impl Status<'_> {
373    pub fn to_int(&self) -> i32 {
374        match self {
375            Status::Ok(_, _, _) => 0,
376            Status::Warning(_, _, _) => 1,
377            Status::Critical(_, _, _) => 2,
378            Status::Unknown(_) => 3,
379        }
380    }
381}
382
383fn abs_diff_duration(a: Duration, b: Duration) -> Duration {
384    if a > b {
385        a - b
386    } else {
387        b - a
388    }
389}
390
391#[cfg(test)]
392mod abs_diff_duration_tests {
393    use super::*;
394    use pretty_assertions::assert_eq;
395
396    #[test]
397    fn test_with_small_a() {
398        let a = Duration::from_nanos(100_000_000);
399        let b = Duration::from_nanos(100_100_000);
400        let expected = Duration::from_nanos(100_000);
401        let actual = abs_diff_duration(a, b);
402
403        assert_eq!(actual, expected);
404    }
405
406    #[test]
407    fn test_with_small_b() {
408        let a = Duration::from_nanos(100_100_000);
409        let b = Duration::from_nanos(100_000_000);
410        let expected = Duration::from_nanos(100_000);
411        let actual = abs_diff_duration(a, b);
412
413        assert_eq!(actual, expected);
414    }
415
416    #[test]
417    fn test_with_equal_values() {
418        let a = Duration::from_nanos(100_000_000);
419        let b = Duration::from_nanos(100_000_000);
420        let expected = Duration::from_nanos(0);
421        let actual = abs_diff_duration(a, b);
422
423        assert_eq!(actual, expected);
424    }
425}
426
427fn generate_intervals(count: u8, min_interval: u64, max_interval: u64) -> Vec<Duration> {
428    if min_interval > max_interval {
429        debug!(
430            "Invalid min and max interval: min: {}, max: {}. No random intervals will be generated.",
431            min_interval, max_interval
432        );
433        return Vec::new();
434    }
435
436    if max_interval == 0 && min_interval == 0 {
437        debug!("Min and max interval are both 0. No random intervals will be generated.");
438        return Vec::new();
439    }
440
441    let mut intervals = Vec::with_capacity(count as usize);
442
443    if max_interval == min_interval {
444        debug!(
445            "Min and max interval are equal: {}ms. Intervals will not be randomized.",
446            min_interval
447        );
448        for _ in 0..count {
449            intervals.push(Duration::from_millis(min_interval));
450        }
451
452        debug!("Random intervals: {:?}", intervals);
453
454        return intervals;
455    }
456
457    debug!(
458        "Generating {} random intervals between {}ms and {}ms...",
459        count, min_interval, max_interval
460    );
461
462    for _ in 0..count {
463        let interval = rand::thread_rng().gen_range(min_interval..=max_interval);
464        intervals.push(Duration::from_millis(interval));
465    }
466
467    debug!("Random intervals: {:?}", intervals);
468
469    intervals
470}
471
472#[cfg(test)]
473mod generate_intervals_tests {
474    use super::*;
475    use pretty_assertions::assert_eq;
476
477    #[test]
478    fn test_with_min_max() {
479        let count = 10;
480        let min_interval = 10;
481        let max_interval = 100;
482        let intervals = generate_intervals(count, min_interval, max_interval);
483
484        assert_eq!(intervals.len(), count as usize);
485        for i in intervals {
486            assert!(i >= Duration::from_millis(min_interval));
487            assert!(i <= Duration::from_millis(max_interval));
488        }
489    }
490
491    #[test]
492    fn test_with_min_max_equal() {
493        let count = 10;
494        let min_interval = 10;
495        let max_interval = 10;
496        let intervals = generate_intervals(count, min_interval, max_interval);
497
498        assert_eq!(intervals.len(), count as usize);
499        for i in intervals {
500            assert_eq!(i, Duration::from_millis(min_interval));
501        }
502    }
503
504    #[test]
505    fn test_with_min_max_zero() {
506        let count = 10;
507        let min_interval = 0;
508        let max_interval = 0;
509        let intervals = generate_intervals(count, min_interval, max_interval);
510
511        assert_eq!(intervals, Vec::<Duration>::new());
512        assert!(intervals.is_empty());
513    }
514
515    #[test]
516    fn test_with_min_max_swapped() {
517        let count = 10;
518        let min_interval = 100;
519        let max_interval = 10;
520        let intervals = generate_intervals(count, min_interval, max_interval);
521
522        assert_eq!(intervals, Vec::<Duration>::new());
523        assert!(intervals.is_empty());
524    }
525    #[test]
526    fn test_with_zero_count() {
527        let count = 0;
528        let min_interval = 10;
529        let max_interval = 100;
530        let intervals = generate_intervals(count, min_interval, max_interval);
531
532        assert_eq!(intervals, Vec::<Duration>::new());
533        assert!(intervals.is_empty());
534    }
535
536    #[test]
537    fn test_with_large_range() {
538        let count = 10;
539        let min_interval = 1;
540        let max_interval = 1_000_000;
541        let intervals = generate_intervals(count, min_interval, max_interval);
542
543        assert_eq!(intervals.len(), count as usize);
544        for i in intervals {
545            assert!(i >= Duration::from_millis(min_interval));
546            assert!(i <= Duration::from_millis(max_interval));
547        }
548    }
549
550    #[test]
551    fn test_with_single_interval() {
552        let count = 1;
553        let min_interval = 10;
554        let max_interval = 100;
555        let intervals = generate_intervals(count, min_interval, max_interval);
556
557        assert_eq!(intervals.len(), 1);
558        assert!(intervals[0] >= Duration::from_millis(min_interval));
559        assert!(intervals[0] <= Duration::from_millis(max_interval));
560    }
561
562    #[test]
563    fn test_with_very_large_intervals() {
564        let count = 10;
565        let min_interval = u64::MAX - 1_000;
566        let max_interval = u64::MAX;
567        let intervals = generate_intervals(count, min_interval, max_interval);
568
569        assert_eq!(intervals.len(), count as usize);
570        for i in intervals {
571            assert!(i >= Duration::from_millis(min_interval));
572            assert!(i <= Duration::from_millis(max_interval));
573        }
574    }
575}
576
577fn default_resolver(addr: &str) -> Result<Vec<IpAddr>, CheckJitterError> {
578    let addr_with_port = format!("{}:0", addr);
579    match addr_with_port.to_socket_addrs() {
580        Ok(addrs_iter) => {
581            let addrs: Vec<IpAddr> = addrs_iter.map(|sockaddr| sockaddr.ip()).collect();
582            if addrs.is_empty() {
583                Err(CheckJitterError::DnsLookupFailed(addr.to_string()))
584            } else {
585                Ok(addrs)
586            }
587        }
588        Err(e) => Err(CheckJitterError::DnsResolutionError {
589            addr: addr.to_string(),
590            error: e.to_string(),
591        }),
592    }
593}
594
595fn parse_addr_with_resolver<F>(addr: &str, resolver: F) -> Result<Vec<IpAddr>, CheckJitterError>
596where
597    F: Fn(&str) -> Result<Vec<IpAddr>, CheckJitterError>,
598{
599    if let Ok(ipv4) = addr.parse::<Ipv4Addr>() {
600        return Ok(vec![IpAddr::V4(ipv4)]);
601    }
602    if let Ok(ipv6) = addr.parse::<Ipv6Addr>() {
603        return Ok(vec![IpAddr::V6(ipv6)]);
604    }
605
606    // If the address is not an IP address, perform DNS lookup using the provided resolver.
607    resolver(addr)
608}
609
610fn parse_addr(addr: &str) -> Result<Vec<IpAddr>, CheckJitterError> {
611    parse_addr_with_resolver(addr, default_resolver)
612}
613
614#[cfg(test)]
615mod parse_addr_tests {
616    use super::*;
617
618    fn mock_resolver(addr: &str) -> Result<Vec<IpAddr>, CheckJitterError> {
619        match addr {
620            "localhost" => Ok(vec![IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))]),
621            "ipv6-localhost" => Ok(vec![IpAddr::V6(Ipv6Addr::LOCALHOST)]),
622            "multi.example.com" => Ok(vec![
623                IpAddr::V4(Ipv4Addr::new(192, 0, 2, 1)),
624                IpAddr::V4(Ipv4Addr::new(192, 0, 2, 2)),
625                IpAddr::V4(Ipv4Addr::new(192, 0, 2, 3)),
626            ]),
627            "unresolved.example.com" => Err(CheckJitterError::DnsLookupFailed(addr.to_string())),
628            "error.example.com" => Err(CheckJitterError::DnsResolutionError {
629                addr: addr.to_string(),
630                error: "mock error".to_string(),
631            }),
632            _ => Err(CheckJitterError::DnsResolutionError {
633                addr: addr.to_string(),
634                error: "unknown host".to_string(),
635            }),
636        }
637    }
638
639    #[test]
640    fn test_valid_ipv4_address() {
641        let addr = "192.168.1.1";
642        let result = parse_addr_with_resolver(addr, mock_resolver);
643        assert_eq!(result, Ok(vec![IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1))]));
644    }
645
646    #[test]
647    fn test_valid_ipv6_address() {
648        let addr = "::1";
649        let result = parse_addr_with_resolver(addr, mock_resolver);
650        assert_eq!(result, Ok(vec![IpAddr::V6(Ipv6Addr::LOCALHOST)]));
651    }
652
653    #[test]
654    fn test_invalid_ip_address() {
655        let addr = "999.999.999.999";
656        let result = parse_addr_with_resolver(addr, mock_resolver);
657        assert_eq!(
658            result,
659            Err(CheckJitterError::DnsResolutionError {
660                addr: addr.to_string(),
661                error: "unknown host".to_string(),
662            })
663        );
664    }
665
666    #[test]
667    fn test_valid_hostname() {
668        let addr = "localhost";
669        let result = parse_addr_with_resolver(addr, mock_resolver);
670        assert_eq!(result, Ok(vec![IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))]));
671    }
672
673    #[test]
674    fn test_valid_ipv6_hostname() {
675        let addr = "ipv6-localhost";
676        let result = parse_addr_with_resolver(addr, mock_resolver);
677        assert_eq!(result, Ok(vec![IpAddr::V6(Ipv6Addr::LOCALHOST)]));
678    }
679
680    #[test]
681    fn test_unresolved_hostname() {
682        let addr = "unresolved.example.com";
683        let result = parse_addr_with_resolver(addr, mock_resolver);
684        assert_eq!(
685            result,
686            Err(CheckJitterError::DnsLookupFailed(addr.to_string()))
687        );
688    }
689
690    #[test]
691    fn test_dns_resolution_error() {
692        let addr = "error.example.com";
693        let result = parse_addr_with_resolver(addr, mock_resolver);
694        assert_eq!(
695            result,
696            Err(CheckJitterError::DnsResolutionError {
697                addr: addr.to_string(),
698                error: "mock error".to_string(),
699            })
700        );
701    }
702
703    #[test]
704    fn test_unknown_hostname() {
705        let addr = "unknown.example.com";
706        let result = parse_addr_with_resolver(addr, mock_resolver);
707        assert_eq!(
708            result,
709            Err(CheckJitterError::DnsResolutionError {
710                addr: addr.to_string(),
711                error: "unknown host".to_string(),
712            })
713        );
714    }
715
716    #[test]
717    fn test_hostname_with_multiple_ips() {
718        let addr = "multi.example.com";
719        let result = parse_addr_with_resolver(addr, mock_resolver);
720        assert_eq!(
721            result,
722            Ok(vec![
723                IpAddr::V4(Ipv4Addr::new(192, 0, 2, 1)),
724                IpAddr::V4(Ipv4Addr::new(192, 0, 2, 2)),
725                IpAddr::V4(Ipv4Addr::new(192, 0, 2, 3)),
726            ])
727        )
728    }
729}
730
731fn run_samples(
732    ip: IpAddr,
733    socket_type: SocketType,
734    samples: u8,
735    timeout: Duration,
736    intervals: Vec<Duration>,
737) -> Result<Vec<Duration>, CheckJitterError> {
738    let ping_function = match socket_type {
739        SocketType::Datagram => ping::dgramsock::ping,
740        SocketType::Raw => ping::rawsock::ping,
741    };
742
743    let mut durations = Vec::<Duration>::with_capacity(samples as usize);
744    let mut intervals_iter = intervals.into_iter();
745    let mut next_ping_time = Instant::now();
746
747    for i in 0..samples {
748        let now = Instant::now();
749
750        if now < next_ping_time {
751            let sleep_duration = next_ping_time - now;
752            std::thread::sleep(sleep_duration);
753        }
754
755        let start_time = Instant::now();
756
757        match ping_function(ip, Some(timeout), None, None, None, None) {
758            Ok(_) => {
759                let end_time = Instant::now();
760                let duration = end_time - start_time;
761                durations.push(duration);
762                debug!("Ping round {}, duration: {:?}", i + 1, duration);
763
764                if let Some(interval) = intervals_iter.next() {
765                    next_ping_time = end_time + interval;
766                } else {
767                    next_ping_time = end_time;
768                }
769            }
770            Err(e) => {
771                if let ping::Error::IoError { error } = &e {
772                    match error.kind() {
773                        std::io::ErrorKind::PermissionDenied => {
774                            return Err(CheckJitterError::PermissionDenied);
775                        }
776                        std::io::ErrorKind::WouldBlock => {
777                            return Err(CheckJitterError::Timeout(timeout.as_millis().to_string()));
778                        }
779                        _ => {
780                            return Err(CheckJitterError::PingIoError(error.to_string()));
781                        }
782                    }
783                }
784                return Err(CheckJitterError::PingError(PingErrorWrapper(e)));
785            }
786        };
787    }
788
789    debug!("Ping durations: {:?}", durations);
790    Ok(durations)
791}
792
793fn get_durations(
794    addr: &str,
795    socket_type: SocketType,
796    samples: u8,
797    timeout: Duration,
798    min_interval: u64,
799    max_interval: u64,
800) -> Result<Vec<Duration>, CheckJitterError> {
801    // NOTE: Only the first IP address from the list of resolved addresses will be used.
802    // TODO: This may change in the future if we decide to ping all resolved addresses by default
803    //       or provide an option to do so.
804    let ip = match parse_addr(addr)?.first() {
805        Some(ip) => *ip,
806        None => return Err(CheckJitterError::DnsLookupFailed(addr.to_string())),
807    };
808
809    if samples < 2 {
810        return Err(CheckJitterError::InsufficientSamples(samples));
811    }
812
813    let intervals = generate_intervals(samples - 1, min_interval, max_interval);
814    run_samples(ip, socket_type, samples, timeout, intervals)
815}
816
817fn calculate_deltas(durations: &[Duration]) -> Result<Vec<Duration>, CheckJitterError> {
818    if durations.len() < 2 {
819        return Err(CheckJitterError::InsufficientSamples(durations.len() as u8));
820    }
821
822    let deltas = durations
823        .windows(2)
824        .map(|w| abs_diff_duration(w[0], w[1]))
825        .collect();
826
827    debug!("Deltas: {:?}", deltas);
828
829    Ok(deltas)
830}
831
832#[cfg(test)]
833mod calculate_deltas_tests {
834    use super::*;
835    use pretty_assertions::assert_eq;
836
837    #[test]
838    fn test_with_zero_durations() {
839        let durations = &[];
840
841        let result = calculate_deltas(durations);
842
843        assert_eq!(result, Err(CheckJitterError::InsufficientSamples(0)));
844    }
845
846    #[test]
847    fn test_with_one_duration() {
848        let durations = &[Duration::from_nanos(100_000_000)];
849
850        let result = calculate_deltas(durations);
851
852        assert_eq!(result, Err(CheckJitterError::InsufficientSamples(1)));
853    }
854
855    #[test]
856    fn test_with_two_durations() {
857        let durations = &[
858            Duration::from_nanos(100_000_000),
859            Duration::from_nanos(100_100_000),
860        ];
861
862        let expected_deltas = vec![Duration::from_nanos(100_000)];
863
864        let deltas = calculate_deltas(durations).unwrap();
865
866        assert_eq!(deltas, expected_deltas);
867    }
868
869    #[test]
870    fn test_with_simple_durations() {
871        let durations = &[
872            Duration::from_nanos(100_000_000),
873            Duration::from_nanos(100_100_000),
874            Duration::from_nanos(100_200_000),
875            Duration::from_nanos(100_300_000),
876        ];
877
878        let expected_deltas = &[
879            Duration::from_nanos(100_000),
880            Duration::from_nanos(100_000),
881            Duration::from_nanos(100_000),
882        ];
883
884        let deltas = calculate_deltas(durations).unwrap();
885
886        assert_eq!(deltas, expected_deltas);
887    }
888
889    #[test]
890    fn test_with_irregular_durations() {
891        let durations = &[
892            Duration::from_nanos(100_000_000),
893            Duration::from_nanos(100_101_200),
894            Duration::from_nanos(101_200_030),
895            Duration::from_nanos(100_310_900),
896        ];
897
898        let expected_deltas = &[
899            Duration::from_nanos(101_200),
900            Duration::from_nanos(1_098_830),
901            Duration::from_nanos(889_130),
902        ];
903
904        let deltas = calculate_deltas(durations).unwrap();
905        assert_eq!(deltas, expected_deltas);
906    }
907}
908
909fn calculate_avg_jitter(deltas: Vec<Duration>) -> f64 {
910    let total_jitter = deltas.iter().sum::<Duration>();
911    debug!("Sum of deltas: {:?}", total_jitter);
912
913    let avg_jitter = total_jitter / deltas.len() as u32;
914    debug!("Average jitter duration: {:?}", avg_jitter);
915
916    let average_float = avg_jitter.as_secs_f64() * 1_000.0;
917    debug!("Average jitter as f64: {:?}", average_float);
918
919    average_float
920}
921
922fn calculate_median_jitter(deltas: Vec<Duration>) -> f64 {
923    let mut sorted_deltas = deltas.clone();
924    sorted_deltas.sort();
925    debug!("Sorted deltas: {:?}", sorted_deltas);
926
927    let len = sorted_deltas.len();
928    debug!("Number of deltas: {}", len);
929
930    let median_float: f64 = if len % 2 == 0 {
931        let mid = len / 2;
932        let mid_1 = mid - 1;
933        let mid_2 = mid;
934        let dur_1 = sorted_deltas[mid_1].as_secs_f64() * 1_000.0;
935        let dur_2 = sorted_deltas[mid_2].as_secs_f64() * 1_000.0;
936        (dur_1 + dur_2) / 2.0
937    } else {
938        let mid = len / 2;
939        sorted_deltas[mid].as_secs_f64() * 1_000.0
940    };
941    debug!("Median jitter as f64: {:?}", median_float);
942
943    median_float
944}
945
946fn calculate_max_jitter(deltas: Vec<Duration>) -> Result<f64, CheckJitterError> {
947    let max = deltas.iter().max().ok_or(CheckJitterError::EmptyDeltas)?;
948    debug!("Max jitter: {:?}", max);
949    let max_float = max.as_secs_f64() * 1_000.0;
950    debug!("Max jitter as f64: {:?}", max_float);
951
952    Ok(max_float)
953}
954
955fn calculate_min_jitter(deltas: Vec<Duration>) -> Result<f64, CheckJitterError> {
956    let min = deltas.iter().min().ok_or(CheckJitterError::EmptyDeltas)?;
957    debug!("Min jitter: {:?}", min);
958    let min_float = min.as_secs_f64() * 1_000.0;
959    debug!("Min jitter as f64: {:?}", min_float);
960
961    Ok(min_float)
962}
963
964/// Round the jitter to the specified precision.
965pub fn round_jitter(j: f64, precision: u8) -> f64 {
966    let factor = 10f64.powi(precision as i32);
967    let rounded_jitter = (j * factor).round() / factor;
968    debug!("jitter as rounded f64: {:?}", rounded_jitter);
969
970    rounded_jitter
971}
972
973#[cfg(test)]
974mod calculate_rounded_jitter_tests {
975    use super::*;
976    use pretty_assertions::assert_eq;
977
978    #[test]
979    fn test_with_simple_durations() {
980        let simple_durations = vec![
981            Duration::from_nanos(100_000_000),
982            Duration::from_nanos(100_100_000),
983            Duration::from_nanos(100_200_000),
984            Duration::from_nanos(100_300_000),
985            Duration::from_nanos(100_400_000),
986            Duration::from_nanos(100_500_000),
987            Duration::from_nanos(100_600_000),
988            Duration::from_nanos(100_700_000),
989            Duration::from_nanos(100_800_000),
990            Duration::from_nanos(100_900_000),
991        ];
992
993        let expected_average_jitter = 0.1;
994        let expected_median_jitter = 0.1;
995        let expected_max_jitter = 0.1;
996        let expected_min_jitter = 0.1;
997        let deltas = calculate_deltas(&simple_durations).unwrap();
998        println!("{:#?}", deltas.clone());
999        let average_jitter = calculate_avg_jitter(deltas.clone());
1000        let median_jitter = calculate_median_jitter(deltas.clone());
1001        let max_jitter = calculate_max_jitter(deltas.clone()).unwrap();
1002        let min_jitter = calculate_min_jitter(deltas).unwrap();
1003        let rounded_average_jitter = round_jitter(average_jitter, 3);
1004        let rounded_median_jitter = round_jitter(median_jitter, 3);
1005        let rounded_max_jitter = round_jitter(max_jitter, 3);
1006        let rounded_min_jitter = round_jitter(min_jitter, 3);
1007
1008        assert_eq!(rounded_average_jitter, expected_average_jitter);
1009        assert_eq!(rounded_median_jitter, expected_median_jitter);
1010        assert_eq!(rounded_max_jitter, expected_max_jitter);
1011        assert_eq!(rounded_min_jitter, expected_min_jitter);
1012    }
1013
1014    #[test]
1015    fn test_with_even_number_of_irregular_durations() {
1016        let irregular_durations = vec![
1017            Duration::from_nanos(270_279_792),
1018            Duration::from_nanos(270_400_049),
1019            Duration::from_nanos(270_242_514),
1020            Duration::from_nanos(269_988_869),
1021            Duration::from_nanos(270_157_314),
1022            Duration::from_nanos(270_096_136),
1023            Duration::from_nanos(270_105_637),
1024            Duration::from_nanos(270_003_857),
1025            Duration::from_nanos(270_192_099),
1026            Duration::from_nanos(270_035_557),
1027        ];
1028
1029        let expected_average_jitter = 0.135_236;
1030        let expected_median_jitter = 0.156_542;
1031        let expected_max_jitter = 0.253_645;
1032        let expected_min_jitter = 0.009_501;
1033        let deltas = calculate_deltas(&irregular_durations).unwrap();
1034        println!("{:#?}", deltas.clone());
1035        let average_jitter = calculate_avg_jitter(deltas.clone());
1036        let median_jitter = calculate_median_jitter(deltas.clone());
1037        let max_jitter = calculate_max_jitter(deltas.clone()).unwrap();
1038        let min_jitter = calculate_min_jitter(deltas).unwrap();
1039        let rounded_average_jitter = round_jitter(average_jitter, 6);
1040        let rounded_median_jitter = round_jitter(median_jitter, 6);
1041        let rounded_max_jitter = round_jitter(max_jitter, 6);
1042        let rounded_min_jitter = round_jitter(min_jitter, 6);
1043
1044        assert_eq!(rounded_average_jitter, expected_average_jitter);
1045        assert_eq!(rounded_median_jitter, expected_median_jitter);
1046        assert_eq!(rounded_max_jitter, expected_max_jitter);
1047        assert_eq!(rounded_min_jitter, expected_min_jitter);
1048    }
1049
1050    #[test]
1051    fn test_with_uneven_number_of_irregular_durations() {
1052        let irregular_durations = vec![
1053            Duration::from_nanos(270_279_792),
1054            Duration::from_nanos(270_400_049),
1055            Duration::from_nanos(270_242_514),
1056            Duration::from_nanos(269_988_869),
1057            Duration::from_nanos(270_157_314),
1058            Duration::from_nanos(270_096_136),
1059            Duration::from_nanos(270_105_637),
1060            Duration::from_nanos(270_003_857),
1061            Duration::from_nanos(270_192_099),
1062        ];
1063
1064        let expected_average_jitter = 0.132_572;
1065        let expected_median_jitter = 0.138_896;
1066        let expected_max_jitter = 0.253_645;
1067        let expected_min_jitter = 0.009_501;
1068        let deltas = calculate_deltas(&irregular_durations).unwrap();
1069        println!("{:#?}", deltas.clone());
1070        let average_jitter = calculate_avg_jitter(deltas.clone());
1071        let median_jitter = calculate_median_jitter(deltas.clone());
1072        let max_jitter = calculate_max_jitter(deltas.clone()).unwrap();
1073        let min_jitter = calculate_min_jitter(deltas).unwrap();
1074        let rounded_average_jitter = round_jitter(average_jitter, 6);
1075        let rounded_median_jitter = round_jitter(median_jitter, 6);
1076        let rounded_max_jitter = round_jitter(max_jitter, 6);
1077        let rounded_min_jitter = round_jitter(min_jitter, 6);
1078
1079        assert_eq!(rounded_average_jitter, expected_average_jitter);
1080        assert_eq!(rounded_median_jitter, expected_median_jitter);
1081        assert_eq!(rounded_max_jitter, expected_max_jitter);
1082        assert_eq!(rounded_min_jitter, expected_min_jitter);
1083    }
1084}
1085
1086/// Get and calculate the aggregated jitter to an IP address or hostname.
1087///
1088/// This function will perform a DNS lookup if a hostname is provided and then use that IP address
1089/// to ping the target. The function will then calculate the aggregated value based on the
1090/// aggregation method passed as an argument. This value will then be rounded to the specified
1091/// decimal.
1092///
1093/// Note that opening a raw socket requires root privileges on Unix-like systems.
1094///
1095/// # Arguments
1096/// * `aggr_method` - The aggregation method to use.
1097/// * `addr` - The IP address or hostname to ping.
1098/// * `socket_type` - The type of socket to use for the ping.
1099/// * `samples` - The number of samples (pings) to take.
1100/// * `timeout` - The timeout for each ping.
1101/// * `min_interval` - The minimum interval between pings in milliseconds.
1102/// * `max_interval` - The maximum interval between pings in milliseconds.
1103///
1104/// # Returns
1105/// The aggregated jitter in milliseconds as a floating point number rounded to the
1106/// specified decimal.
1107///
1108/// # Example
1109/// ```rust,no_run
1110/// // This example will not run because it requires root privileges.
1111/// use check_jitter::{get_jitter, CheckJitterError, AggregationMethod, SocketType};
1112/// use std::time::Duration;
1113///
1114/// let jitter = get_jitter(
1115///     AggregationMethod::Average, // aggr_method
1116///     "192.168.1.1",              // addr
1117///     SocketType::Raw,            // socket_type
1118///     10,                         // samples
1119///     Duration::from_secs(1),     // timeout
1120///     10,                         // min_interval
1121///     100).unwrap();              // max_interval
1122/// println!("Average jitter: {}ms", jitter);
1123/// ```
1124pub fn get_jitter(
1125    aggr_method: AggregationMethod,
1126    addr: &str,
1127    socket_type: SocketType,
1128    samples: u8,
1129    timeout: Duration,
1130    min_interval: u64,
1131    max_interval: u64,
1132) -> Result<f64, CheckJitterError> {
1133    let durations = get_durations(
1134        addr,
1135        socket_type,
1136        samples,
1137        timeout,
1138        min_interval,
1139        max_interval,
1140    )?;
1141    let deltas = calculate_deltas(&durations)?;
1142    match aggr_method {
1143        AggregationMethod::Average => Ok(calculate_avg_jitter(deltas)),
1144        AggregationMethod::Median => Ok(calculate_median_jitter(deltas)),
1145        AggregationMethod::Max => calculate_max_jitter(deltas),
1146        AggregationMethod::Min => calculate_min_jitter(deltas),
1147    }
1148}
1149
1150/// Evaluate the jitter against the thresholds and return the appropriate status.
1151///
1152/// This function will evaluate the jitter against the provided thresholds and return the
1153/// appropriate status. It will match against the critical threshold first and then the warning
1154/// threshold, returning the first match or `Status::Ok` if no thresholds are matched.
1155///
1156/// # Arguments
1157/// * `jitter` - The jitter to evaluate as a 64 bit floating point number.
1158/// * `thresholds` - A reference to the `Thresholds` to evaluate against.
1159///
1160/// # Returns
1161/// The `Status` of the jitter against the thresholds.
1162///
1163/// # Example
1164/// ```rust
1165/// use check_jitter::{evaluate_thresholds, AggregationMethod, Thresholds, Status};
1166/// use nagios_range::NagiosRange as ThresholdRange;
1167/// use std::time::Duration;
1168///
1169/// let jitter = 0.1;
1170/// let thresholds = Thresholds {
1171///     warning: Some(ThresholdRange::from("0:0.5").unwrap()),
1172///     critical: Some(ThresholdRange::from("0:1").unwrap()),
1173/// };
1174///
1175/// let status = evaluate_thresholds(AggregationMethod::Average, jitter, &thresholds);
1176///
1177/// match status {
1178///     Status::Ok(_, _, _) => println!("Jitter is OK"),
1179///     Status::Warning(_, _, _) => println!("Jitter is warning"),
1180///     Status::Critical(_, _, _) => println!("Jitter is critical"),
1181///     Status::Unknown(_) => println!("Unknown status"),
1182/// }
1183/// ```
1184pub fn evaluate_thresholds(
1185    aggr_method: AggregationMethod,
1186    value: f64,
1187    thresholds: &Thresholds,
1188) -> Status {
1189    info!("Evaluating jitter: {:?}", value);
1190    if let Some(c) = thresholds.critical {
1191        info!("Checking critical threshold: {:?}", c);
1192        if c.check(value) {
1193            info!("Jitter is critical: {:?}", value);
1194            return Status::Critical(aggr_method, value, thresholds);
1195        } else {
1196            info!("Jitter is not critical: {:?}", value);
1197        }
1198    } else {
1199        info!("No critical threshold provided");
1200    }
1201
1202    if let Some(w) = thresholds.warning {
1203        info!("Checking warning threshold: {:?}", w);
1204        if w.check(value) {
1205            info!("Jitter is warning: {:?}", value);
1206            return Status::Warning(aggr_method, value, thresholds);
1207        } else {
1208            info!("Jitter is not warning: {:?}", value);
1209        }
1210    } else {
1211        info!("No warning threshold provided");
1212    }
1213
1214    Status::Ok(aggr_method, value, thresholds)
1215}