use anyhow::Result;
use std::fs;
#[derive(Debug, Clone, Default)]
#[allow(dead_code)]
pub struct NfsMount {
pub server: String,
pub path: String,
pub mount_point: String,
pub fs_type: String,
}
#[derive(Debug, Clone, Default)]
pub struct NfsStats {
pub mount: NfsMount,
pub read_ops: u64,
pub write_ops: u64,
pub read_bytes: u64,
pub write_bytes: u64,
}
#[derive(Debug, Clone, Default)]
#[allow(dead_code)]
pub struct NfsStatsDelta {
pub mount_point: String,
pub read_ops_sec: f64,
pub write_ops_sec: f64,
pub read_bytes_sec: f64,
pub write_bytes_sec: f64,
}
pub struct NfsCollector {
last_stats: Vec<NfsStats>,
last_time: std::time::Instant,
}
impl Default for NfsCollector {
fn default() -> Self {
Self::new()
}
}
impl NfsCollector {
pub fn new() -> Self {
Self {
last_stats: Vec::new(),
last_time: std::time::Instant::now(),
}
}
pub fn read_nfs_mounts() -> Result<Vec<NfsMount>> {
let content = fs::read_to_string("/proc/mounts")?;
let mut mounts = Vec::new();
for line in content.lines() {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() < 3 {
continue;
}
let fs_type = parts[2];
if fs_type.starts_with("nfs") {
let device = parts[0];
let mount_point = parts[1];
let device_parts: Vec<&str> = device.splitn(2, ':').collect();
if device_parts.len() == 2 {
mounts.push(NfsMount {
server: device_parts[0].to_string(),
path: device_parts[1].to_string(),
mount_point: mount_point.to_string(),
fs_type: fs_type.to_string(),
});
}
}
}
Ok(mounts)
}
pub fn read_nfs_stats(mounts: Vec<NfsMount>) -> Result<Vec<NfsStats>> {
let mut stats = Vec::new();
let nfs_content = fs::read_to_string("/proc/net/rpc/nfs").unwrap_or_default();
let mut total_read_ops: u64 = 0;
let mut total_write_ops: u64 = 0;
for line in nfs_content.lines() {
if line.starts_with("proc4 ") {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() > 25 {
total_read_ops = parts.get(8).and_then(|s| s.parse().ok()).unwrap_or(0);
total_write_ops = parts.get(9).and_then(|s| s.parse().ok()).unwrap_or(0);
}
}
}
let mount_count = mounts.len() as u64;
let per_mount_read = if mount_count > 0 {
total_read_ops / mount_count
} else {
0
};
let per_mount_write = if mount_count > 0 {
total_write_ops / mount_count
} else {
0
};
let avg_read_size: u64 = 64 * 1024; let avg_write_size: u64 = 64 * 1024;
for mount in mounts {
stats.push(NfsStats {
mount: mount.clone(),
read_ops: per_mount_read,
write_ops: per_mount_write,
read_bytes: per_mount_read * avg_read_size,
write_bytes: per_mount_write * avg_write_size,
});
}
Ok(stats)
}
pub fn collect(&mut self) -> Result<(Vec<NfsStats>, Vec<NfsStatsDelta>)> {
let mounts = Self::read_nfs_mounts()?;
let current_stats = Self::read_nfs_stats(mounts)?;
let now = std::time::Instant::now();
let elapsed = now.duration_since(self.last_time).as_secs_f64();
let is_first_run = self.last_stats.is_empty();
let mut deltas = Vec::new();
for current in ¤t_stats {
let (read_ops_sec, write_ops_sec, read_bytes_sec, write_bytes_sec) =
if is_first_run || elapsed <= 0.0 {
(0.0, 0.0, 0.0, 0.0)
} else if let Some(last) = self
.last_stats
.iter()
.find(|s| s.mount.mount_point == current.mount.mount_point)
{
(
(current.read_ops.saturating_sub(last.read_ops)) as f64 / elapsed,
(current.write_ops.saturating_sub(last.write_ops)) as f64 / elapsed,
(current.read_bytes.saturating_sub(last.read_bytes)) as f64 / elapsed,
(current.write_bytes.saturating_sub(last.write_bytes)) as f64 / elapsed,
)
} else {
(0.0, 0.0, 0.0, 0.0)
};
deltas.push(NfsStatsDelta {
mount_point: current.mount.mount_point.clone(),
read_ops_sec,
write_ops_sec,
read_bytes_sec,
write_bytes_sec,
});
}
self.last_stats = current_stats.clone();
self.last_time = now;
Ok((current_stats, deltas))
}
}
pub fn format_bytes(bytes: f64) -> String {
const KB: f64 = 1024.0;
const MB: f64 = KB * 1024.0;
const GB: f64 = MB * 1024.0;
if bytes >= GB {
format!("{:.2} GB", bytes / GB)
} else if bytes >= MB {
format!("{:.2} MB", bytes / MB)
} else if bytes >= KB {
format!("{:.2} KB", bytes / KB)
} else {
format!("{:.0} B", bytes)
}
}
pub fn format_bytes_per_sec(bytes_sec: f64) -> String {
format!("{}/s", format_bytes(bytes_sec))
}
pub fn format_ops_per_sec(ops_sec: f64) -> String {
format!("{:.1} op/s", ops_sec)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_bytes_per_sec() {
assert_eq!(format_bytes_per_sec(1024.0), "1.00 KB/s");
assert_eq!(format_bytes_per_sec(1024.0 * 1024.0), "1.00 MB/s");
}
#[test]
fn test_format_ops_per_sec() {
assert_eq!(format_ops_per_sec(10.5), "10.5 op/s");
assert_eq!(format_ops_per_sec(0.0), "0.0 op/s");
}
}