use std::net::IpAddr;
use crate::model::{MetricState, UnavailableReason};
use crate::units::{Percent, Rate};
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum LinkState {
Up,
Down,
Dormant,
#[default]
Unknown,
}
impl LinkState {
#[must_use]
pub const fn symbol(self) -> char {
match self {
Self::Up => '+',
Self::Down => '-',
Self::Dormant => '.',
Self::Unknown => '?',
}
}
#[must_use]
pub const fn label(self) -> &'static str {
match self {
Self::Up => "up",
Self::Down => "down",
Self::Dormant => "dormant",
Self::Unknown => "unknown",
}
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum InterfaceKind {
Physical,
Loopback,
Virtual,
Tunnel,
#[default]
Unknown,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct InterfaceAddress {
pub ip: IpAddr,
pub prefix_len: Option<u8>,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct InterfaceErrors {
pub rx_errors: u64,
pub tx_errors: u64,
pub rx_dropped: u64,
pub tx_dropped: u64,
}
impl InterfaceErrors {
#[must_use]
pub const fn any(&self) -> bool {
self.rx_errors > 0 || self.tx_errors > 0 || self.rx_dropped > 0 || self.tx_dropped > 0
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct TrafficTotals {
pub rx_bytes: u64,
pub tx_bytes: u64,
pub rx_packets: u64,
pub tx_packets: u64,
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct NetworkSnapshot {
pub name: Box<str>,
pub kind: InterfaceKind,
pub state: MetricState<LinkState>,
pub addresses: Vec<InterfaceAddress>,
pub mac: Option<Box<str>>,
pub rx: MetricState<Rate>,
pub tx: MetricState<Rate>,
pub rx_packets: MetricState<Rate>,
pub tx_packets: MetricState<Rate>,
pub errors: MetricState<InterfaceErrors>,
pub link_speed_mbps: MetricState<u64>,
pub since_launch: TrafficTotals,
pub os_totals: MetricState<TrafficTotals>,
}
impl NetworkSnapshot {
#[must_use]
pub fn utilization(&self) -> MetricState<Percent> {
let Some(&speed_mbps) = self.link_speed_mbps.fresh() else {
return MetricState::TemporarilyUnavailable(UnavailableReason::LinkSpeedUnknown);
};
if speed_mbps == 0 {
return MetricState::TemporarilyUnavailable(UnavailableReason::LinkSpeedUnknown);
}
let (Some(rx), Some(tx)) = (self.rx.fresh(), self.tx.fresh()) else {
return MetricState::WarmingUp;
};
let capacity_bytes_per_second = speed_mbps as f64 * 1_000_000.0 / 8.0;
let busiest = rx.per_second().max(tx.per_second());
#[allow(clippy::cast_possible_truncation)]
let percent = ((busiest / capacity_bytes_per_second) * 100.0) as f32;
Percent::new(percent).map_or(
MetricState::TemporarilyUnavailable(UnavailableReason::ParseFailed),
MetricState::Available,
)
}
#[must_use]
pub fn warming_up(name: Box<str>, kind: InterfaceKind) -> Self {
Self {
name,
kind,
state: MetricState::WarmingUp,
addresses: Vec::new(),
mac: None,
rx: MetricState::WarmingUp,
tx: MetricState::WarmingUp,
rx_packets: MetricState::WarmingUp,
tx_packets: MetricState::WarmingUp,
errors: MetricState::WarmingUp,
link_speed_mbps: MetricState::WarmingUp,
since_launch: TrafficTotals::default(),
os_totals: MetricState::WarmingUp,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn interface() -> NetworkSnapshot {
NetworkSnapshot::warming_up("en0".into(), InterfaceKind::Physical)
}
#[test]
fn utilization_is_unavailable_without_a_known_link_speed() {
let mut nic = interface();
nic.rx = MetricState::Available(Rate::new(18_200_000.0).expect("valid"));
nic.tx = MetricState::Available(Rate::new(2_300_000.0).expect("valid"));
nic.link_speed_mbps = MetricState::Unsupported;
assert_eq!(
nic.utilization(),
MetricState::TemporarilyUnavailable(UnavailableReason::LinkSpeedUnknown),
"§7.4 forbids a utilization percentage without known capacity"
);
}
#[test]
fn a_zero_link_speed_is_treated_as_unknown_not_as_infinite_utilization() {
let mut nic = interface();
nic.rx = MetricState::Available(Rate::new(1_000.0).expect("valid"));
nic.tx = MetricState::Available(Rate::ZERO);
nic.link_speed_mbps = MetricState::Available(0);
assert_eq!(
nic.utilization(),
MetricState::TemporarilyUnavailable(UnavailableReason::LinkSpeedUnknown)
);
}
#[test]
fn utilization_uses_the_busier_direction_of_a_duplex_link() {
let mut nic = interface();
nic.rx = MetricState::Available(Rate::new(62_500_000.0).expect("valid"));
nic.tx = MetricState::Available(Rate::new(1_000.0).expect("valid"));
nic.link_speed_mbps = MetricState::Available(1_000);
let percent = *nic
.utilization()
.fresh()
.expect("speed and rates are known");
assert!((percent.value() - 50.0).abs() < 0.1, "got {percent}");
}
#[test]
fn utilization_warms_up_while_rates_are_still_unknown() {
let mut nic = interface();
nic.link_speed_mbps = MetricState::Available(1_000);
assert!(nic.utilization().is_warming_up());
}
#[test]
fn utilization_can_exceed_one_hundred_percent_rather_than_being_clamped() {
let mut nic = interface();
nic.rx = MetricState::Available(Rate::new(250_000_000.0).expect("valid"));
nic.tx = MetricState::Available(Rate::ZERO);
nic.link_speed_mbps = MetricState::Available(1_000);
let percent = *nic.utilization().fresh().expect("known");
assert!(percent.value() > 100.0, "got {percent}");
}
#[test]
fn link_state_symbols_are_distinguishable_without_color() {
let mut symbols: Vec<char> = [
LinkState::Up,
LinkState::Down,
LinkState::Dormant,
LinkState::Unknown,
]
.iter()
.map(|s| s.symbol())
.collect();
symbols.sort_unstable();
symbols.dedup();
assert_eq!(symbols.len(), 4);
}
#[test]
fn error_counters_report_whether_anything_is_wrong() {
assert!(!InterfaceErrors::default().any());
assert!(
InterfaceErrors {
rx_dropped: 1,
..InterfaceErrors::default()
}
.any()
);
}
}