This commit is contained in:
gru committed 2026-10-03 00:27:09 +01:00
commit 5e23e9af02
240 files changed
+99957

No files matched your search

+180
View File
@@ -0,0 +1,180 @@
use super::{now_ms, platform_name, NetworkInterfaceView, View};
use std::{collections::BTreeMap, process::Command};
#[derive(Default)]
pub(crate) struct Counters {
cpu_total: Option<u64>,
cpu_idle: Option<u64>,
process_cpu_micros: Option<u64>,
}
fn command_output(program: &str, args: &[&str]) -> Option<String> {
let output = Command::new(program).args(args).output().ok()?;
output.status.success().then(|| String::from_utf8_lossy(&output.stdout).trim().to_owned())
}
fn sysctl(name: &str) -> Option<String> {
command_output("sysctl", &["-n", name])
}
fn sysctl_u64(names: &[&str]) -> Option<u64> {
names.iter().find_map(|name| sysctl(name)?.split_whitespace().next()?.parse().ok())
}
fn cpu_times() -> Option<(u64, u64)> {
let values: Vec<u64> = sysctl("kern.cp_time")?
.split_whitespace()
.filter_map(|value| value.parse().ok())
.collect();
if values.len() < 5 {
return None;
}
Some((values.iter().sum(), values[4]))
}
fn process_cpu_micros() -> Option<u64> {
let mut usage = std::mem::MaybeUninit::<libc::rusage>::zeroed();
// SAFETY: getrusage initializes the supplied rusage structure on success.
let rc = unsafe { libc::getrusage(libc::RUSAGE_SELF, usage.as_mut_ptr()) };
if rc != 0 {
return None;
}
// SAFETY: rc == 0 guarantees the structure was initialized by libc.
let usage = unsafe { usage.assume_init() };
let user = u64::try_from(usage.ru_utime.tv_sec).ok()?.saturating_mul(1_000_000)
+ u64::try_from(usage.ru_utime.tv_usec).ok()?;
let system = u64::try_from(usage.ru_stime.tv_sec).ok()?.saturating_mul(1_000_000)
+ u64::try_from(usage.ru_stime.tv_usec).ok()?;
Some(user.saturating_add(system))
}
fn load_average() -> Option<[f64; 3]> {
let raw = sysctl("vm.loadavg")?;
let values: Vec<f64> = raw
.trim_matches(|c: char| c == '{' || c == '}' || c.is_whitespace())
.split_whitespace()
.filter_map(|value| value.parse().ok())
.collect();
(values.len() >= 3).then(|| [values[0], values[1], values[2]])
}
fn process_rss_bytes() -> Option<u64> {
let pid = std::process::id().to_string();
command_output("ps", &["-o", "rss=", "-p", &pid])?
.split_whitespace()
.next()?
.parse::<u64>()
.ok()
.map(|kib| kib.saturating_mul(1024))
}
fn network_interfaces() -> Vec<NetworkInterfaceView> {
let Some(text) = command_output("netstat", &["-ibn"]) else {
return Vec::new();
};
let mut lines = text.lines();
let Some(header) = lines.next() else {
return Vec::new();
};
let columns: Vec<_> = header.split_whitespace().collect();
let name_index = columns.iter().position(|value| value.eq_ignore_ascii_case("Name"));
let ibytes_index = columns.iter().position(|value| value.eq_ignore_ascii_case("Ibytes"));
let obytes_index = columns.iter().position(|value| value.eq_ignore_ascii_case("Obytes"));
let (Some(name_index), Some(ibytes_index), Some(obytes_index)) =
(name_index, ibytes_index, obytes_index)
else {
return Vec::new();
};
let mut interfaces: BTreeMap<String, (u64, u64)> = BTreeMap::new();
for line in lines {
let fields: Vec<_> = line.split_whitespace().collect();
if fields.len() <= name_index || fields.len() <= ibytes_index || fields.len() <= obytes_index {
continue;
}
let name = fields[name_index].trim_end_matches('*');
if name.is_empty() || name == "lo0" {
continue;
}
let (Ok(rx), Ok(tx)) = (fields[ibytes_index].parse::<u64>(), fields[obytes_index].parse::<u64>()) else {
continue;
};
let entry = interfaces.entry(name.to_owned()).or_default();
entry.0 = entry.0.max(rx);
entry.1 = entry.1.max(tx);
}
interfaces
.into_iter()
.take(64)
.map(|(name, (rx_bytes, tx_bytes))| NetworkInterfaceView {
name,
rx_bytes,
tx_bytes,
rx_bytes_per_second: None,
tx_bytes_per_second: None,
})
.collect()
}
pub(crate) fn sample(previous: Option<(View, Counters)>) -> (View, Counters) {
let (cpu_total, cpu_idle) = cpu_times().map_or((None, None), |(total, idle)| (Some(total), Some(idle)));
let counters = Counters {
cpu_total,
cpu_idle,
process_cpu_micros: process_cpu_micros(),
};
let mut view = View {
source: "bsd_native",
platform: platform_name(),
sampled_at_ms: now_ms(),
logical_cpus: sysctl_u64(&["hw.ncpu", "hw.ncpuonline"])
.and_then(|value| usize::try_from(value).ok())
.or_else(|| std::thread::available_parallelism().ok().map(usize::from)),
memory_total_bytes: sysctl_u64(&["hw.physmem64", "hw.physmem"]),
process_rss_bytes: process_rss_bytes(),
load_average: load_average(),
network_interfaces: network_interfaces(),
..Default::default()
};
if !view.network_interfaces.is_empty() {
view.network_rx_bytes = Some(view.network_interfaces.iter().map(|item| item.rx_bytes).sum());
view.network_tx_bytes = Some(view.network_interfaces.iter().map(|item| item.tx_bytes).sum());
}
if let Some((old, old_counters)) = previous {
let system_total = counters
.cpu_total
.zip(old_counters.cpu_total)
.and_then(|(new, old)| new.checked_sub(old))
.filter(|value| *value > 0);
if let Some(total) = system_total {
view.cpu_percent = counters
.cpu_idle
.zip(old_counters.cpu_idle)
.and_then(|(new, old)| new.checked_sub(old))
.map(|idle| (100.0 * (1.0 - idle as f64 / total as f64)).clamp(0.0, 100.0));
}
let seconds = view.sampled_at_ms.saturating_sub(old.sampled_at_ms) as f64 / 1000.0;
if seconds > 0.0 {
view.process_cpu_percent = counters
.process_cpu_micros
.zip(old_counters.process_cpu_micros)
.and_then(|(new, old)| new.checked_sub(old))
.map(|micros| micros as f64 / (seconds * 10_000.0));
view.network_rx_bytes_per_second = view
.network_rx_bytes
.zip(old.network_rx_bytes)
.map(|(new, old)| new.saturating_sub(old) as f64 / seconds);
view.network_tx_bytes_per_second = view
.network_tx_bytes
.zip(old.network_tx_bytes)
.map(|(new, old)| new.saturating_sub(old) as f64 / seconds);
for interface in &mut view.network_interfaces {
if let Some(previous) = old.network_interfaces.iter().find(|old| old.name == interface.name) {
interface.rx_bytes_per_second = Some(interface.rx_bytes.saturating_sub(previous.rx_bytes) as f64 / seconds);
interface.tx_bytes_per_second = Some(interface.tx_bytes.saturating_sub(previous.tx_bytes) as f64 / seconds);
}
}
}
}
// Keep unknown/unsupported BSD metrics as None instead of substituting a different semantic.
(view, counters)
}
+84
View File
@@ -0,0 +1,84 @@
use super::View;
use std::fs;
fn key_value(text: &str, name: &str) -> Option<u64> {
text.lines().find_map(|line| {
let (key, rest) = line.split_once(':')?;
(key == name)
.then(|| rest.split_whitespace().next()?.parse().ok())
.flatten()
})
}
pub(crate) fn augment(view: &mut View) {
if let Ok(stat) = fs::read_to_string("/proc/stat") {
view.logical_cpus = Some(
stat.lines()
.filter(|line| line.starts_with("cpu") && line.as_bytes().get(3).is_some_and(u8::is_ascii_digit))
.count(),
)
.filter(|count| *count > 0);
}
// Linux-specific augmentation preserves fields and units that existed before
// the cross-platform library adapter. Generic CPU/load/network/process
// metrics are deliberately collected by the shared API in mod.rs.
if let Ok(mem) = fs::read_to_string("/proc/meminfo") {
view.memory_total_bytes = key_value(&mem, "MemTotal").map(|n| n.saturating_mul(1024));
view.memory_used_bytes = view
.memory_total_bytes
.zip(key_value(&mem, "MemAvailable"))
.map(|(total, available)| total.saturating_sub(available.saturating_mul(1024)));
}
if let Ok(status) = fs::read_to_string("/proc/self/status") {
view.process_rss_bytes = key_value(&status, "VmRSS").map(|n| n.saturating_mul(1024));
view.process_rss_anon_bytes = key_value(&status, "RssAnon").map(|n| n.saturating_mul(1024));
view.process_rss_file_bytes = key_value(&status, "RssFile").map(|n| n.saturating_mul(1024));
view.process_rss_shmem_bytes = key_value(&status, "RssShmem").map(|n| n.saturating_mul(1024));
view.process_data_bytes = key_value(&status, "VmData").map(|n| n.saturating_mul(1024));
view.process_swap_bytes = key_value(&status, "VmSwap").map(|n| n.saturating_mul(1024));
view.process_threads = key_value(&status, "Threads");
}
view.open_fds = fs::read_dir("/proc/self/fd")
.ok()
.map(|entries| entries.filter_map(Result::ok).count())
.or(view.open_fds);
if let Ok(io) = fs::read_to_string("/proc/self/io") {
view.process_read_bytes = key_value(&io, "read_bytes");
view.process_write_bytes = key_value(&io, "write_bytes");
}
if let Some(group) = fs::read_to_string("/proc/self/cgroup")
.ok()
.and_then(|text| {
text.lines()
.find_map(|line| line.strip_prefix("0::").map(str::to_owned))
})
.filter(|path| {
path.starts_with('/') && !path.split('/').any(|part| part == ".." || part == ".")
})
{
let directory = std::path::Path::new("/sys/fs/cgroup").join(group.trim_start_matches('/'));
view.cgroup_memory_current_bytes = fs::read_to_string(directory.join("memory.current"))
.ok()
.and_then(|s| s.trim().parse().ok());
view.cgroup_memory_max_bytes = fs::read_to_string(directory.join("memory.max"))
.ok()
.and_then(|s| s.trim().parse().ok());
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn proc_values_and_unavailable_are_distinct() {
assert_eq!(
key_value(
"VmRSS:\t123 kB\nRssAnon:\t100 kB\nRssFile:\t20 kB\nRssShmem:\t3 kB\nThreads:\t2\n",
"VmRSS"
),
Some(123)
);
assert_eq!(key_value("", "VmRSS"), None);
}
}
+510
View File
@@ -0,0 +1,510 @@
//! Cross-platform system/process statistics used by low-frequency diagnostics.
//!
//! Generic host metrics come from `systemstat`, network inventory/counters use
//! `sysinfo`, and generic process metrics use `metrics-process` where supported.
//! `systemstat` is also a library-level network fallback on targets where
//! `sysinfo` has no native network backend. Platform-specific
//! code is limited to additive metrics that the common libraries do not expose.
//! Missing metrics stay `None`; callers must not synthesize values.
use serde::Serialize;
#[derive(Debug, Clone, Serialize)]
pub struct NetworkInterfaceView {
pub name: String,
pub rx_bytes: u64,
pub tx_bytes: u64,
pub rx_bytes_per_second: Option<f64>,
pub tx_bytes_per_second: Option<f64>,
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct View {
pub source: &'static str,
pub platform: &'static str,
pub sampled_at_ms: u64,
pub cpu_percent: Option<f64>,
pub process_cpu_percent: Option<f64>,
pub logical_cpus: Option<usize>,
pub memory_total_bytes: Option<u64>,
pub memory_used_bytes: Option<u64>,
pub system_swap_total_bytes: Option<u64>,
pub system_swap_used_bytes: Option<u64>,
pub system_uptime_seconds: Option<u64>,
pub process_rss_bytes: Option<u64>,
pub process_virtual_memory_bytes: Option<u64>,
pub process_virtual_memory_max_bytes: Option<u64>,
pub process_start_time_seconds: Option<u64>,
/// Resident anonymous memory (heap/stacks/private anonymous mappings).
pub process_rss_anon_bytes: Option<u64>,
/// Resident file-backed mappings.
pub process_rss_file_bytes: Option<u64>,
/// Resident shared-memory mappings.
pub process_rss_shmem_bytes: Option<u64>,
/// Data/heap virtual address space reported by the native platform source.
pub process_data_bytes: Option<u64>,
/// Process swap currently charged by the kernel.
pub process_swap_bytes: Option<u64>,
pub open_fds: Option<usize>,
pub max_fds: Option<u64>,
pub process_threads: Option<u64>,
pub load_average: Option<[f64; 3]>,
pub network_rx_bytes: Option<u64>,
pub network_tx_bytes: Option<u64>,
pub network_rx_bytes_per_second: Option<f64>,
pub network_tx_bytes_per_second: Option<f64>,
pub network_interfaces: Vec<NetworkInterfaceView>,
pub process_read_bytes: Option<u64>,
pub process_write_bytes: Option<u64>,
pub cgroup_memory_current_bytes: Option<u64>,
pub cgroup_memory_max_bytes: Option<u64>,
}
#[derive(Default)]
pub(crate) struct Counters {
process_cpu_seconds: Option<f64>,
}
pub(crate) fn platform_name() -> &'static str {
#[cfg(target_os = "linux")]
{
return "Linux";
}
#[cfg(target_os = "freebsd")]
{
return "FreeBSD";
}
#[cfg(target_os = "openbsd")]
{
return "OpenBSD";
}
#[cfg(target_os = "netbsd")]
{
return "NetBSD";
}
#[cfg(target_os = "dragonfly")]
{
return "DragonFly BSD";
}
#[cfg(target_os = "macos")]
{
return "macOS";
}
#[cfg(target_os = "windows")]
{
return "Windows";
}
#[cfg(all(
target_family = "unix",
not(any(
target_os = "linux",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly",
target_os = "macos"
))
))]
{
return "Unix";
}
#[cfg(all(not(target_family = "unix"), not(target_os = "windows")))]
{
"Unsupported"
}
}
fn source_name() -> &'static str {
#[cfg(target_os = "linux")]
{
return "systemstat+sysinfo+metrics_process+linux_procfs";
}
#[cfg(any(
target_os = "freebsd",
target_os = "openbsd",
target_os = "macos",
target_os = "windows"
))]
{
return "systemstat+sysinfo+metrics_process";
}
#[cfg(any(target_os = "netbsd", target_os = "dragonfly"))]
{
return "systemstat+sysinfo+metrics_process(dummy)";
}
#[cfg(not(any(
target_os = "linux",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly",
target_os = "macos",
target_os = "windows"
)))]
{
"unsupported"
}
}
#[cfg(any(
target_os = "linux",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly",
target_os = "macos",
target_os = "windows"
))]
fn collect_host(view: &mut View) {
use systemstat::{Platform, System};
let system = System::new();
let cpu = system.cpu_load_aggregate().ok();
if let Ok(memory) = system.memory() {
let total = memory.total.as_u64();
let free = memory.free.as_u64();
view.memory_total_bytes = Some(total);
view.memory_used_bytes = Some(total.saturating_sub(free));
}
if let Ok(swap) = system.swap() {
let total = swap.total.as_u64();
let free = swap.free.as_u64();
view.system_swap_total_bytes = Some(total);
view.system_swap_used_bytes = Some(total.saturating_sub(free));
}
if let Ok(uptime) = system.uptime() {
view.system_uptime_seconds = Some(uptime.as_secs());
}
if let Ok(load) = system.load_average() {
view.load_average = Some([load.one as f64, load.five as f64, load.fifteen as f64]);
}
if let Some(cpu) = cpu {
std::thread::sleep(std::time::Duration::from_secs(1));
if let Ok(cpu) = cpu.done() {
view.cpu_percent = Some((100.0 * (1.0 - cpu.idle as f64)).clamp(0.0, 100.0));
}
}
}
fn is_loopback_interface(name: &str) -> bool {
if name == "lo" {
return true;
}
name.strip_prefix("lo")
.is_some_and(|suffix| !suffix.is_empty() && suffix.bytes().all(|byte| byte.is_ascii_digit()))
}
fn normalize_network_interfaces<I>(interfaces: I) -> Vec<NetworkInterfaceView>
where
I: IntoIterator<Item = (String, u64, u64)>,
{
let mut merged = std::collections::BTreeMap::<String, (u64, u64)>::new();
for (name, rx_bytes, tx_bytes) in interfaces {
let name = name.trim();
if name.is_empty() || is_loopback_interface(name) {
continue;
}
let entry = merged.entry(name.to_owned()).or_default();
entry.0 = entry.0.max(rx_bytes);
entry.1 = entry.1.max(tx_bytes);
}
merged
.into_iter()
.map(|(name, (rx_bytes, tx_bytes))| NetworkInterfaceView {
name,
rx_bytes,
tx_bytes,
rx_bytes_per_second: None,
tx_bytes_per_second: None,
})
.collect()
}
fn collect_network_sysinfo() -> Vec<NetworkInterfaceView> {
let networks = sysinfo::Networks::new_with_refreshed_list();
normalize_network_interfaces(networks.iter().map(|(name, data)| {
(
name.clone(),
data.total_received(),
data.total_transmitted(),
)
}))
}
#[cfg(any(
target_os = "linux",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly",
target_os = "macos",
target_os = "windows"
))]
fn collect_network_systemstat() -> Vec<NetworkInterfaceView> {
use systemstat::{Platform, System};
let system = System::new();
let Ok(networks) = system.networks() else {
return Vec::new();
};
let mut raw = Vec::new();
for (key, interface) in networks {
let display_name = if interface.name.trim().is_empty() {
key.as_str()
} else {
interface.name.as_str()
};
let mut stats = system.network_stats(&key).ok();
if stats.is_none() && display_name != key.as_str() {
stats = system.network_stats(display_name).ok();
}
if let Some(stats) = stats {
raw.push((
display_name.to_owned(),
stats.rx_bytes.as_u64(),
stats.tx_bytes.as_u64(),
));
}
}
normalize_network_interfaces(raw)
}
#[cfg(not(any(
target_os = "linux",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly",
target_os = "macos",
target_os = "windows"
)))]
fn collect_network_systemstat() -> Vec<NetworkInterfaceView> {
Vec::new()
}
fn merge_network_interfaces(
primary: Vec<NetworkInterfaceView>,
secondary: Vec<NetworkInterfaceView>,
) -> Vec<NetworkInterfaceView> {
// Both libraries expose cumulative byte counters. Merge by interface name
// and keep the larger counter so a partial/zero reading from one backend
// cannot hide a usable reading from the other.
normalize_network_interfaces(primary.into_iter().chain(secondary).map(|item| {
(item.name, item.rx_bytes, item.tx_bytes)
}))
}
fn collect_network(view: &mut View) {
// `sysinfo::Networks` is the primary interface/traffic API. `systemstat`
// supplements it and is the library-level fallback on targets where
// sysinfo has no native network backend. Combining both inventories also
// avoids dropping an interface when either backend is only partially
// available. No shell commands or per-platform network parser are used.
let interfaces = merge_network_interfaces(
collect_network_sysinfo(),
collect_network_systemstat(),
);
if interfaces.is_empty() {
return;
}
view.network_rx_bytes = Some(
interfaces
.iter()
.fold(0u64, |total, item| total.saturating_add(item.rx_bytes)),
);
view.network_tx_bytes = Some(
interfaces
.iter()
.fold(0u64, |total, item| total.saturating_add(item.tx_bytes)),
);
view.network_interfaces = interfaces;
}
#[cfg(not(any(
target_os = "linux",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly",
target_os = "macos",
target_os = "windows"
)))]
fn collect_host(_view: &mut View) {}
fn collect_process(view: &mut View, counters: &mut Counters) {
let metrics = metrics_process::collector::collect();
counters.process_cpu_seconds = metrics.cpu_seconds_total;
view.process_rss_bytes = metrics.resident_memory_bytes;
view.process_virtual_memory_bytes = metrics.virtual_memory_bytes;
view.process_virtual_memory_max_bytes = metrics.virtual_memory_max_bytes;
view.process_start_time_seconds = metrics.start_time_seconds;
view.open_fds = metrics.open_fds.and_then(|value| usize::try_from(value).ok());
view.max_fds = metrics.max_fds;
view.process_threads = metrics.threads;
}
fn calculate_rates(view: &mut View, previous: &View) {
let seconds = view.sampled_at_ms.saturating_sub(previous.sampled_at_ms) as f64 / 1000.0;
if seconds <= 0.0 {
return;
}
view.network_rx_bytes_per_second = view
.network_rx_bytes
.zip(previous.network_rx_bytes)
.map(|(current, old)| current.saturating_sub(old) as f64 / seconds);
view.network_tx_bytes_per_second = view
.network_tx_bytes
.zip(previous.network_tx_bytes)
.map(|(current, old)| current.saturating_sub(old) as f64 / seconds);
for interface in &mut view.network_interfaces {
if let Some(old) = previous
.network_interfaces
.iter()
.find(|candidate| candidate.name == interface.name)
{
interface.rx_bytes_per_second =
Some(interface.rx_bytes.saturating_sub(old.rx_bytes) as f64 / seconds);
interface.tx_bytes_per_second =
Some(interface.tx_bytes.saturating_sub(old.tx_bytes) as f64 / seconds);
}
}
}
pub(crate) fn sample(previous: Option<(View, Counters)>) -> (View, Counters) {
let mut view = View {
source: source_name(),
platform: platform_name(),
logical_cpus: std::thread::available_parallelism().ok().map(usize::from),
..Default::default()
};
let mut counters = Counters::default();
collect_host(&mut view);
collect_process(&mut view, &mut counters);
collect_network(&mut view);
// Preserve the existing detailed Linux fields and Linux memory-used semantics;
// these are additive details not provided by the common libraries.
#[cfg(target_os = "linux")]
linux::augment(&mut view);
view.sampled_at_ms = now_ms();
if let Some((previous_view, previous_counters)) = previous {
let seconds = view.sampled_at_ms.saturating_sub(previous_view.sampled_at_ms) as f64 / 1000.0;
if seconds > 0.0 {
view.process_cpu_percent = counters
.process_cpu_seconds
.zip(previous_counters.process_cpu_seconds)
.and_then(|(current, old)| (current >= old).then_some((current - old) * 100.0 / seconds));
}
calculate_rates(&mut view, &previous_view);
}
(view, counters)
}
pub(crate) fn now_ms() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as u64
}
#[cfg(target_os = "linux")]
mod linux;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn platform_is_never_empty() {
assert!(!platform_name().is_empty());
}
#[test]
fn network_normalization_filters_loopback_and_keeps_all_real_interfaces() {
let raw = vec![
("lo".to_owned(), 1, 2),
("lo0".to_owned(), 3, 4),
("lo1".to_owned(), 5, 6),
("em0".to_owned(), 100, 200),
("eth0".to_owned(), 300, 400),
("em0".to_owned(), 90, 250),
];
let interfaces = normalize_network_interfaces(raw);
assert_eq!(interfaces.len(), 2);
assert_eq!(interfaces[0].name, "em0");
assert_eq!(interfaces[0].rx_bytes, 100);
assert_eq!(interfaces[0].tx_bytes, 250);
assert_eq!(interfaces[1].name, "eth0");
}
#[test]
fn network_merge_supplements_partial_primary_inventory() {
let primary = normalize_network_interfaces(vec![
("em0".to_owned(), 0, 20),
("wg0".to_owned(), 50, 60),
]);
let secondary = normalize_network_interfaces(vec![
("em0".to_owned(), 100, 10),
("em1".to_owned(), 200, 300),
]);
let interfaces = merge_network_interfaces(primary, secondary);
assert_eq!(interfaces.len(), 3);
assert_eq!(interfaces[0].name, "em0");
assert_eq!(interfaces[0].rx_bytes, 100);
assert_eq!(interfaces[0].tx_bytes, 20);
assert_eq!(interfaces[1].name, "em1");
assert_eq!(interfaces[2].name, "wg0");
}
#[test]
fn rate_calculation_tracks_each_interface() {
let mut current = View {
sampled_at_ms: 3_000,
network_rx_bytes: Some(400),
network_tx_bytes: Some(800),
network_interfaces: vec![NetworkInterfaceView {
name: "em0".to_owned(),
rx_bytes: 300,
tx_bytes: 600,
rx_bytes_per_second: None,
tx_bytes_per_second: None,
}],
..Default::default()
};
let previous = View {
sampled_at_ms: 1_000,
network_rx_bytes: Some(200),
network_tx_bytes: Some(400),
network_interfaces: vec![NetworkInterfaceView {
name: "em0".to_owned(),
rx_bytes: 100,
tx_bytes: 200,
rx_bytes_per_second: None,
tx_bytes_per_second: None,
}],
..Default::default()
};
calculate_rates(&mut current, &previous);
assert_eq!(current.network_rx_bytes_per_second, Some(100.0));
assert_eq!(current.network_tx_bytes_per_second, Some(200.0));
assert_eq!(current.network_interfaces[0].rx_bytes_per_second, Some(100.0));
assert_eq!(current.network_interfaces[0].tx_bytes_per_second, Some(200.0));
}
#[test]
fn sample_reports_platform_and_timestamp() {
let (view, _) = sample(None);
assert_eq!(view.platform, platform_name());
assert_eq!(view.source, source_name());
assert!(view.sampled_at_ms > 0);
if let Some(cpu) = view.cpu_percent {
assert!((0.0..=100.0).contains(&cpu));
}
}
}