use chrono::{DateTime, Local};
use deepsize::DeepSizeOf;
use tracing::{debug, error, trace};
use crate::errors::AnalysisError;
use crate::records::{display_group, Check, CheckType, IpType};
use crate::store::{Store, OUTAGE_TIME_SPAN};
use std::collections::HashMap;
use std::fmt::{Display, Write};
use std::os::unix::fs::MetadataExt;
use self::outage::Outage;
pub mod outage;
pub const TIME_FORMAT_HUMANS: &str = "%Y-%m-%d %H:%M:%S %Z";
pub type CheckGroup<'check> = Vec<&'check Check>;
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, PartialOrd, Ord, Default)]
pub enum IpAddrConstraint {
#[default]
Any,
V4,
V6,
}
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, PartialOrd, Ord, Default)]
pub struct CheckAccessConstraints {
pub failed_only: bool,
pub ip: IpAddrConstraint,
pub since_date: Option<DateTime<Local>>,
pub only_complete: bool,
}
impl IpAddrConstraint {
pub fn ip_matches(&self, ip: &std::net::IpAddr) -> bool {
if matches!(self, Self::Any) {
return true;
}
match ip {
std::net::IpAddr::V4(_) => matches!(self, Self::V4),
std::net::IpAddr::V6(_) => matches!(self, Self::V6),
}
}
pub fn ip_type_matches(&self, ip: &IpType) -> bool {
if matches!(self, Self::Any) {
return true;
}
match ip {
IpType::V4 => matches!(self, Self::V4),
IpType::V6 => matches!(self, Self::V6),
}
}
}
pub fn analyze(store: &Store, checks: &[&Check]) -> Result<String, AnalysisError> {
let mut f = String::new();
barrier(&mut f, "General")?;
generalized(checks, &mut f)?;
barrier(&mut f, "HTTP")?;
generic_type_analyze(checks, &mut f, CheckType::Http)?;
barrier(&mut f, "ICMP")?;
generic_type_analyze(checks, &mut f, CheckType::Icmp)?;
barrier(&mut f, "IPv4")?;
gereric_ip_analyze(checks, &mut f, IpType::V4)?;
barrier(&mut f, "IPv6")?;
gereric_ip_analyze(checks, &mut f, IpType::V6)?;
barrier(&mut f, "Outages")?;
outages(checks, &mut f)?;
barrier(&mut f, "Store Metadata")?;
store_meta(store, &mut f)?;
Ok(f)
}
pub fn get_checks(
store: &Store,
constraints: CheckAccessConstraints,
) -> Result<Vec<&Check>, AnalysisError> {
debug!("Getting checks with the following constraints: {constraints:#?}");
let checks: Vec<&Check> = store.checks().iter().collect();
let checks: Vec<&Check> = if constraints.only_complete && constraints.failed_only {
debug!("Processing outages because only complete outages should be considered");
let outages = Outage::make_outages(checks.as_ref());
fn is_in_outage(outages: &[Outage], check: &Check) -> bool {
outages
.binary_search_by(|outage| {
outage[0].timestamp_parsed().cmp(&check.timestamp_parsed())
})
.is_ok()
}
checks
.into_iter()
.filter(|c| is_in_outage(&outages, c))
.collect()
} else {
checks
};
let mut checks: Vec<&Check> = checks
.into_iter()
.filter(|c| {
(if constraints.failed_only {
!c.is_success()
} else {
true
}) && constraints.ip.ip_type_matches(&c.ip_type())
&& ({
if let Some(since_date) = constraints.since_date {
c.timestamp_parsed() >= since_date
} else {
true
}
})
})
.collect();
checks.sort();
Ok(checks)
}
pub fn fmt_timestamp(timestamp: impl Into<DateTime<Local>>) -> String {
let a: chrono::DateTime<chrono::Local> = timestamp.into();
format!("{}", a.format(TIME_FORMAT_HUMANS))
}
fn barrier(f: &mut String, title: &str) -> Result<(), AnalysisError> {
writeln!(f, "{:=<10}{:=<48}", "", format!(" {title} "))?;
Ok(())
}
fn key_value_write(
f: &mut String,
title: &str,
content: impl Display,
) -> Result<(), std::fmt::Error> {
writeln!(f, "{title:<24}: {content}")
}
fn outages(all: &[&Check], f: &mut String) -> Result<(), AnalysisError> {
let fails_exist = !all.iter().all(|c| c.is_success());
if !fails_exist || all.is_empty() {
writeln!(f, "None\n")?;
return Ok(());
}
let mut outages = Outage::make_outages(all);
writeln!(f, "Latest\n")?;
for (outage_idx, outage) in outages.iter().rev().enumerate() {
writeln!(f, "{outage_idx}:\t{}", &outage.short_report()?)?;
if outage_idx >= 9 {
writeln!(f, "\nshowing only the 10 latest outages...\n")?;
break;
}
}
writeln!(f, "\nMost severe\n")?;
outages.sort_by(Outage::cmp_severity);
for (outage_idx, outage) in outages.iter().rev().enumerate() {
writeln!(f, "{outage_idx}:\t{}", &outage.short_report()?)?;
if outage_idx >= 9 {
writeln!(f, "\nshowing only the 10 most severe outages...")?;
break;
}
}
writeln!(f)?;
Ok(())
}
pub fn outages_detailed(
all: &[&Check],
latest_outages: Option<usize>,
f: &mut String,
dump: bool,
) -> Result<(), AnalysisError> {
let fails_exist = !all.iter().all(|c| c.is_success());
if !fails_exist || all.is_empty() {
writeln!(f, "None\n")?;
return Ok(());
}
let mut fail_groups = fail_groups(all);
if let Some(latest) = latest_outages {
fail_groups.sort_by(|a, b| a.cmp(b).reverse());
fail_groups.truncate(latest);
fail_groups.sort();
}
for (outage_idx, group) in fail_groups.into_iter().enumerate() {
if group.is_empty() {
error!("empty outage group");
continue;
}
let outage = Outage::try_from(group).expect("fail group was empty");
writeln!(f, "{outage_idx}:\n{}", more_indent(&outage.to_string()))?;
if dump {
let mut buf = String::new();
display_group(outage.all(), &mut buf)?;
writeln!(f, "\tAll contained:\n{}", more_indent(&buf))?;
}
}
writeln!(f)?;
Ok(())
}
fn group_by_time<'check>(checks: &[&'check Check]) -> HashMap<i64, CheckGroup<'check>> {
let mut groups: HashMap<i64, CheckGroup<'check>> = HashMap::new();
for check in checks {
groups.entry(check.timestamp()).or_default().push(check);
}
groups
}
pub(crate) fn fail_groups<'check>(checks: &[&'check Check]) -> Vec<CheckGroup<'check>> {
trace!("calculating fail groups");
let by_time = group_by_time(checks);
let mut time_sorted_values: Vec<&Vec<&Check>> = by_time.values().collect();
time_sorted_values.sort();
let mut continuous_outage_groups: Vec<Vec<&Check>> = Vec::new();
let mut group_first_time = time_sorted_values[0][0].timestamp();
let mut group_current: Vec<&Check> = Vec::new();
let mut first;
for time_group in time_sorted_values {
#[cfg(debug_assertions)]
{
let t = time_group[0].timestamp();
debug_assert!(
time_group.iter().all(|c| c.timestamp() == t),
"time group does not share time"
)
}
if time_group.iter().all(|c| c.is_success()) {
if !group_current.is_empty() {
continuous_outage_groups.push(group_current.clone());
group_current.clear();
}
continue;
}
first = time_group[0];
if group_current.is_empty() {
group_first_time = first.timestamp();
}
if first.timestamp() - group_first_time > OUTAGE_TIME_SPAN {
continuous_outage_groups.push(group_current.clone());
group_current.clear();
group_first_time = first.timestamp(); }
group_current.extend(time_group);
}
if !group_current.is_empty() {
continuous_outage_groups.push(group_current);
}
continuous_outage_groups.sort();
continuous_outage_groups
}
fn analyze_check_type_set(
f: &mut String,
all: &[&Check],
successes: &[&Check],
) -> Result<(), AnalysisError> {
if all.is_empty() {
writeln!(f, "None\n")?;
return Ok(());
}
key_value_write(f, "checks", format!("{:08}", all.len()))?;
key_value_write(f, "checks ok", format!("{:08}", successes.len()))?;
key_value_write(
f,
"checks bad",
format!("{:08}", all.len() - successes.len()),
)?;
key_value_write(
f,
"success ratio",
format!(
"{:03.02}%",
success_ratio(all.len(), successes.len()) * 100.0
),
)?;
key_value_write(
f,
"first check at",
fmt_timestamp(all.first().unwrap().timestamp_parsed()),
)?;
key_value_write(
f,
"last check at",
fmt_timestamp(all.last().unwrap().timestamp_parsed()),
)?;
writeln!(f)?;
Ok(())
}
fn generalized(checks: &[&Check], f: &mut String) -> Result<(), AnalysisError> {
if checks.is_empty() {
writeln!(f, "no checks to analyze\n")?;
return Ok(());
}
let all: Vec<&Check> = checks.to_vec();
let successes: Vec<&Check> = checks.iter().copied().filter(|c| c.is_success()).collect();
analyze_check_type_set(f, &all, &successes)?;
Ok(())
}
fn gereric_ip_analyze(
checks: &[&Check],
f: &mut String,
ip_type: IpType,
) -> Result<(), AnalysisError> {
let all: Vec<&Check> = checks
.iter()
.copied()
.filter(|c| c.ip_type() == ip_type)
.collect();
let successes: Vec<&Check> = all.clone().into_iter().filter(|c| c.is_success()).collect();
analyze_check_type_set(f, &all, &successes)?;
Ok(())
}
fn generic_type_analyze(
checks: &[&Check],
f: &mut String,
check_type: CheckType,
) -> Result<(), AnalysisError> {
let all: Vec<&Check> = checks
.iter()
.copied()
.filter(|c| c.calc_type().unwrap_or(CheckType::Unknown) == check_type)
.collect();
let successes: Vec<&Check> = all.clone().into_iter().filter(|c| c.is_success()).collect();
analyze_check_type_set(f, &all, &successes)?;
Ok(())
}
fn store_meta(store: &Store, f: &mut String) -> Result<(), AnalysisError> {
let store_size_mem = store.deep_size_of();
let store_size_fs = std::fs::metadata(Store::path())?.size();
key_value_write(f, "Hash mem blake3", store.get_hash())?;
key_value_write(f, "Hash file sha256", store.get_hash_of_file()?)?;
key_value_write(f, "Store Version (mem)", store.version())?;
key_value_write(f, "Store Version (file)", Store::peek_file_version()?)?;
key_value_write(f, "Store Size (mem)", store_size_mem)?;
key_value_write(f, "Store Size (file)", store_size_fs)?;
key_value_write(
f,
"File to Mem Ratio",
store_size_fs as f64 / store_size_mem as f64,
)?;
Ok(())
}
#[inline]
fn success_ratio(all_checks: usize, subset: usize) -> f64 {
subset as f64 / all_checks as f64
}
#[inline]
fn more_indent(buf: &str) -> String {
format!("\t{}", buf.to_string().replace("\n", "\n\t"))
}
#[cfg(test)]
mod tests {
use chrono::{Timelike, Utc};
use tracing_test::traced_test;
use crate::analyze::Outage;
use crate::records::{Check, CheckFlag, TARGETS};
use super::{fail_groups, group_by_time};
#[rustfmt::skip]
fn basic_check_set() -> Vec<Check>{
let ip4 = TARGETS[0].parse().unwrap();
let ip6 = TARGETS[1].parse().unwrap();
let time = Utc::now().with_minute(0).unwrap();
let time = |min: u32| Utc::now().with_minute(time.minute()+min).unwrap();
let mut a = vec![
Check::new(time(0), CheckFlag::Success | CheckFlag::TypeHTTP, None, ip4),
Check::new(time(0), CheckFlag::Success | CheckFlag::TypeIcmp, None, ip4),
Check::new(time(0), CheckFlag::Success | CheckFlag::TypeHTTP, None, ip6),
Check::new(time(0), CheckFlag::Success | CheckFlag::TypeIcmp, None, ip6),
Check::new(time(2), CheckFlag::Unreachable | CheckFlag::TypeHTTP, None, ip4),
Check::new(time(2), CheckFlag::Unreachable | CheckFlag::TypeIcmp, None, ip4),
Check::new(time(2), CheckFlag::Unreachable | CheckFlag::TypeHTTP, None, ip6),
Check::new(time(2), CheckFlag::Unreachable | CheckFlag::TypeIcmp, None, ip6),
Check::new(time(3), CheckFlag::Unreachable | CheckFlag::TypeHTTP, None, ip4),
Check::new(time(3), CheckFlag::Unreachable | CheckFlag::TypeIcmp, None, ip4),
Check::new(time(3), CheckFlag::Unreachable | CheckFlag::TypeHTTP, None, ip6),
Check::new(time(3), CheckFlag::Unreachable | CheckFlag::TypeIcmp, None, ip6),
Check::new(time(4), CheckFlag::Success | CheckFlag::TypeHTTP, None, ip4),
Check::new(time(4), CheckFlag::Success | CheckFlag::TypeIcmp, None, ip4),
Check::new(time(4), CheckFlag::Success | CheckFlag::TypeHTTP, None, ip6),
Check::new(time(4), CheckFlag::Success | CheckFlag::TypeIcmp, None, ip6),
Check::new(time(5), CheckFlag::Unreachable | CheckFlag::TypeHTTP, None, ip4),
Check::new(time(5), CheckFlag::Unreachable | CheckFlag::TypeIcmp, None, ip4),
Check::new(time(5), CheckFlag::Unreachable | CheckFlag::TypeHTTP, None, ip6),
Check::new(time(5), CheckFlag::Unreachable | CheckFlag::TypeIcmp, None, ip6),
Check::new(time(50), CheckFlag::Unreachable | CheckFlag::TypeHTTP, None, ip4),
Check::new(time(50), CheckFlag::Unreachable | CheckFlag::TypeIcmp, None, ip4),
Check::new(time(50), CheckFlag::Unreachable | CheckFlag::TypeHTTP, None, ip6),
Check::new(time(50), CheckFlag::Unreachable | CheckFlag::TypeIcmp, None, ip6),
Check::new(time(51), CheckFlag::Unreachable | CheckFlag::TypeHTTP, None, ip4),
Check::new(time(51), CheckFlag::Unreachable | CheckFlag::TypeIcmp, None, ip4),
Check::new(time(51), CheckFlag::Unreachable | CheckFlag::TypeHTTP, None, ip6),
Check::new(time(51), CheckFlag::Unreachable | CheckFlag::TypeIcmp, None, ip6),
] ;
a.sort();
a
}
#[test]
#[traced_test]
fn test_fail_groups() {
let base_checks = basic_check_set();
let checks: Vec<&Check> = base_checks.iter().collect();
for _ in 0..40 {
let fg = fail_groups(&checks);
assert_eq!(fg.len(), 3);
assert_eq!(fg[0].len(), 8);
assert_eq!(fg[1].len(), 4);
assert_eq!(fg[2].len(), 8);
let _outages = [
Outage::try_from(fg[0].clone()).unwrap(),
Outage::try_from(fg[1].clone()).unwrap(),
];
}
}
#[test]
#[traced_test]
fn test_group_by_time() {
let base_checks = basic_check_set();
let checks: Vec<&Check> = base_checks.iter().collect();
let tg = group_by_time(&checks);
assert_eq!(tg.len(), 7);
for (k, v) in tg {
assert_eq!(v.len(), 4);
for c in v {
assert_eq!(k, c.timestamp())
}
}
}
}