v0.24.5
This commit is contained in:
commit
5e23e9af02
240 files changed
+99957
No files matched your search
@@ -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)
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user