1use std::net::IpAddr;
14
15use crate::model::{MetricState, UnavailableReason};
16use crate::units::{Percent, Rate};
17
18#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
20#[cfg_attr(feature = "serde", derive(serde::Serialize))]
21#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
22pub enum LinkState {
23 Up,
25 Down,
27 Dormant,
29 #[default]
31 Unknown,
32}
33
34impl LinkState {
35 #[must_use]
37 pub const fn symbol(self) -> char {
38 match self {
39 Self::Up => '+',
40 Self::Down => '-',
41 Self::Dormant => '.',
42 Self::Unknown => '?',
43 }
44 }
45
46 #[must_use]
48 pub const fn label(self) -> &'static str {
49 match self {
50 Self::Up => "up",
51 Self::Down => "down",
52 Self::Dormant => "dormant",
53 Self::Unknown => "unknown",
54 }
55 }
56}
57
58#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
60#[cfg_attr(feature = "serde", derive(serde::Serialize))]
61#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
62pub enum InterfaceKind {
63 Physical,
65 Loopback,
67 Virtual,
69 Tunnel,
71 #[default]
73 Unknown,
74}
75
76#[derive(Clone, Copy, Debug, Eq, PartialEq)]
78#[cfg_attr(feature = "serde", derive(serde::Serialize))]
79pub struct InterfaceAddress {
80 pub ip: IpAddr,
82 pub prefix_len: Option<u8>,
84}
85
86#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
88#[cfg_attr(feature = "serde", derive(serde::Serialize))]
89pub struct InterfaceErrors {
90 pub rx_errors: u64,
92 pub tx_errors: u64,
94 pub rx_dropped: u64,
96 pub tx_dropped: u64,
98}
99
100impl InterfaceErrors {
101 #[must_use]
103 pub const fn any(&self) -> bool {
104 self.rx_errors > 0 || self.tx_errors > 0 || self.rx_dropped > 0 || self.tx_dropped > 0
105 }
106}
107
108#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
110#[cfg_attr(feature = "serde", derive(serde::Serialize))]
111pub struct TrafficTotals {
112 pub rx_bytes: u64,
114 pub tx_bytes: u64,
116 pub rx_packets: u64,
118 pub tx_packets: u64,
120}
121
122#[derive(Clone, Debug, PartialEq)]
124#[cfg_attr(feature = "serde", derive(serde::Serialize))]
125pub struct NetworkSnapshot {
126 pub name: Box<str>,
128 pub kind: InterfaceKind,
130 pub state: MetricState<LinkState>,
132 pub addresses: Vec<InterfaceAddress>,
134 pub mac: Option<Box<str>>,
136 pub rx: MetricState<Rate>,
138 pub tx: MetricState<Rate>,
140 pub rx_packets: MetricState<Rate>,
142 pub tx_packets: MetricState<Rate>,
144 pub errors: MetricState<InterfaceErrors>,
146 pub link_speed_mbps: MetricState<u64>,
148 pub since_launch: TrafficTotals,
153 pub os_totals: MetricState<TrafficTotals>,
155}
156
157impl NetworkSnapshot {
158 #[must_use]
165 pub fn utilization(&self) -> MetricState<Percent> {
166 let Some(&speed_mbps) = self.link_speed_mbps.fresh() else {
167 return MetricState::TemporarilyUnavailable(UnavailableReason::LinkSpeedUnknown);
168 };
169 if speed_mbps == 0 {
170 return MetricState::TemporarilyUnavailable(UnavailableReason::LinkSpeedUnknown);
171 }
172 let (Some(rx), Some(tx)) = (self.rx.fresh(), self.tx.fresh()) else {
173 return MetricState::WarmingUp;
174 };
175 let capacity_bytes_per_second = speed_mbps as f64 * 1_000_000.0 / 8.0;
177 let busiest = rx.per_second().max(tx.per_second());
178 #[allow(clippy::cast_possible_truncation)]
181 let percent = ((busiest / capacity_bytes_per_second) * 100.0) as f32;
182 Percent::new(percent).map_or(
183 MetricState::TemporarilyUnavailable(UnavailableReason::ParseFailed),
184 MetricState::Available,
185 )
186 }
187
188 #[must_use]
190 pub fn warming_up(name: Box<str>, kind: InterfaceKind) -> Self {
191 Self {
192 name,
193 kind,
194 state: MetricState::WarmingUp,
195 addresses: Vec::new(),
196 mac: None,
197 rx: MetricState::WarmingUp,
198 tx: MetricState::WarmingUp,
199 rx_packets: MetricState::WarmingUp,
200 tx_packets: MetricState::WarmingUp,
201 errors: MetricState::WarmingUp,
202 link_speed_mbps: MetricState::WarmingUp,
203 since_launch: TrafficTotals::default(),
204 os_totals: MetricState::WarmingUp,
205 }
206 }
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212
213 fn interface() -> NetworkSnapshot {
214 NetworkSnapshot::warming_up("en0".into(), InterfaceKind::Physical)
215 }
216
217 #[test]
218 fn utilization_is_unavailable_without_a_known_link_speed() {
219 let mut nic = interface();
220 nic.rx = MetricState::Available(Rate::new(18_200_000.0).expect("valid"));
221 nic.tx = MetricState::Available(Rate::new(2_300_000.0).expect("valid"));
222 nic.link_speed_mbps = MetricState::Unsupported;
223
224 assert_eq!(
225 nic.utilization(),
226 MetricState::TemporarilyUnavailable(UnavailableReason::LinkSpeedUnknown),
227 "§7.4 forbids a utilization percentage without known capacity"
228 );
229 }
230
231 #[test]
232 fn a_zero_link_speed_is_treated_as_unknown_not_as_infinite_utilization() {
233 let mut nic = interface();
234 nic.rx = MetricState::Available(Rate::new(1_000.0).expect("valid"));
235 nic.tx = MetricState::Available(Rate::ZERO);
236 nic.link_speed_mbps = MetricState::Available(0);
237 assert_eq!(
238 nic.utilization(),
239 MetricState::TemporarilyUnavailable(UnavailableReason::LinkSpeedUnknown)
240 );
241 }
242
243 #[test]
244 fn utilization_uses_the_busier_direction_of_a_duplex_link() {
245 let mut nic = interface();
246 nic.rx = MetricState::Available(Rate::new(62_500_000.0).expect("valid"));
248 nic.tx = MetricState::Available(Rate::new(1_000.0).expect("valid"));
249 nic.link_speed_mbps = MetricState::Available(1_000);
250
251 let percent = *nic
252 .utilization()
253 .fresh()
254 .expect("speed and rates are known");
255 assert!((percent.value() - 50.0).abs() < 0.1, "got {percent}");
256 }
257
258 #[test]
259 fn utilization_warms_up_while_rates_are_still_unknown() {
260 let mut nic = interface();
261 nic.link_speed_mbps = MetricState::Available(1_000);
262 assert!(nic.utilization().is_warming_up());
263 }
264
265 #[test]
266 fn utilization_can_exceed_one_hundred_percent_rather_than_being_clamped() {
267 let mut nic = interface();
270 nic.rx = MetricState::Available(Rate::new(250_000_000.0).expect("valid"));
271 nic.tx = MetricState::Available(Rate::ZERO);
272 nic.link_speed_mbps = MetricState::Available(1_000);
273 let percent = *nic.utilization().fresh().expect("known");
274 assert!(percent.value() > 100.0, "got {percent}");
275 }
276
277 #[test]
278 fn link_state_symbols_are_distinguishable_without_color() {
279 let mut symbols: Vec<char> = [
280 LinkState::Up,
281 LinkState::Down,
282 LinkState::Dormant,
283 LinkState::Unknown,
284 ]
285 .iter()
286 .map(|s| s.symbol())
287 .collect();
288 symbols.sort_unstable();
289 symbols.dedup();
290 assert_eq!(symbols.len(), 4);
291 }
292
293 #[test]
294 fn error_counters_report_whether_anything_is_wrong() {
295 assert!(!InterfaceErrors::default().any());
296 assert!(
297 InterfaceErrors {
298 rx_dropped: 1,
299 ..InterfaceErrors::default()
300 }
301 .any()
302 );
303 }
304}