Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions proc-stats/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions proc-stats/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,7 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
anyhow = "1.0.70"
regex = "1.7.3"
serde = { version = "1.0.156", features = ["derive"] }
serde_json = "1.0.94"
126 changes: 126 additions & 0 deletions proc-stats/src/common.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
use anyhow::Result;
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, fmt, fs, path::Path, str::FromStr};

/// A system resource as identified in <https://docs.kernel.org/accounting/psi.html> for use
/// in pressure statistics collection.
#[derive(Serialize, Deserialize)]
pub enum SysResource {
Cpu,
Memory,
Io,
}
impl fmt::Display for SysResource {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of these impls seem really verbose to me... is there a more elegant way to do this?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there are some crates that could help (e.g. strum), but it seems ok as-is to me for now. That being said, I am not sure you need either of the impl fmt::Display blocks; nothing broke when I commented them out. 🤔

fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
SysResource::Cpu => write!(f, "cpu"),
SysResource::Memory => write!(f, "memory"),
SysResource::Io => write!(f, "io"),
}
}
}
impl SysResource {
pub fn pressure_file(&self) -> &Path {
match self {
SysResource::Cpu => Path::new("/proc/pressure/cpu"),
SysResource::Memory => Path::new("/proc/pressure/memory"),
SysResource::Io => Path::new("/proc/pressure/io"),
}
}
}
#[derive(Serialize, Deserialize, Eq, Hash, PartialEq)]
pub enum SysPressureCategory {
Some,
Full,
}
#[derive(Debug)]
pub struct SysPressureParseError;

impl FromStr for SysPressureCategory {
type Err = SysPressureParseError;
fn from_str(input: &str) -> Result<SysPressureCategory, SysPressureParseError> {
match input {
"some" => Ok(SysPressureCategory::Some),
"full" => Ok(SysPressureCategory::Full),
_ => Err(SysPressureParseError),
}
}
}
impl fmt::Display for SysPressureCategory {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
SysPressureCategory::Full => write!(f, "full"),
SysPressureCategory::Some => write!(f, "some"),
}
}
}
#[derive(Serialize, Deserialize)]
pub struct SysPressureData {
avg10: f64,
avg60: f64,
avg300: f64,
total: u64,
}
/// This structure represents a ["Pressure Stall Information"](kernel-psi) reading against one
/// of the resources specified in [`crate::common::SysResource`](enum.SysResource.html).
///
/// [kernel-psi]: https://docs.kernel.org/accounting/psi.html
#[derive(Serialize, Deserialize)]
pub struct SysPressure {
resource: SysResource,
pressure: HashMap<SysPressureCategory, SysPressureData>,
}

impl SysPressure {
pub fn try_from(resource: SysResource) -> Result<Self> {
//let mut path = PathBuf::from("/proc/pressure/");
//path.push(resource.to_string());
let pressure: HashMap<SysPressureCategory, SysPressureData> =
fs::read_to_string(resource.pressure_file())
.expect("Failed to read {path}")
.trim()
.split('\n')
.filter_map(|line| {
// Example line:
// some avg10=0.04 avg60=0.08 avg300=0.12 total=10739245730
let re = Regex::new(
r"(?x)
(some|full)+\s # $1 Should map to a SysPressureCategory
avg10=([0-9]*\.[0-9]+|[0-9]+)\s # $2 a f64 for the 10 second average
avg60=([0-9]*\.[0-9]+|[0-9]+)\s # $3 a f64 for the 60 second average
avg300=([0-9]*\.[0-9]+|[0-9]+)\s # $4 a f64 for the 300 second average
total=([0-9]+) # $5 a u64 for the total
",
)
.unwrap();
let cap = re.captures(line);
match cap {
Some(c) => Some({
let cat =
SysPressureCategory::from_str(c.get(1).map_or("", |m| m.as_str()))
.expect("Failed to read pressure category");
let data = SysPressureData {
avg10: c
.get(2)
.map_or(0.00, |m| m.as_str().parse::<f64>().unwrap()),
avg60: c
.get(3)
.map_or(0.00, |m| m.as_str().parse::<f64>().unwrap()),
avg300: c
.get(4)
.map_or(0.00, |m| m.as_str().parse::<f64>().unwrap()),
total: c
.get(5)
.map_or(0_u64, |m| m.as_str().parse::<u64>().unwrap()),
};
(cat, data)
}),
None => todo!(),
}
})
.collect();
assert_eq!(pressure.len(), 2);
Ok(SysPressure { resource, pressure })
}
}
21 changes: 21 additions & 0 deletions proc-stats/src/cpu.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
use std::{collections::HashMap, fs};

use crate::common::{SysPressure, SysResource};

pub fn cpu_info() -> HashMap<String, String> {
fs::read_to_string("/proc/cpuinfo")
.expect("Failed to read /proc/cpuinfo")
.trim()
.split('\n')
.filter_map(|line| {
// Example line:
// vendor_id : GenuineIntel
line.split_once(':')
.map(|(name, value)| (name.trim().to_string(), value.trim().to_string()))
})
.collect()
}

pub fn cpu_pressure() -> SysPressure {
SysPressure::try_from(SysResource::Cpu).unwrap()
}
5 changes: 5 additions & 0 deletions proc-stats/src/io.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
use crate::common::{SysPressure, SysResource};

pub fn io_pressure() -> SysPressure {
SysPressure::try_from(SysResource::Io).unwrap()
}
136 changes: 48 additions & 88 deletions proc-stats/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,110 +1,70 @@
use std::{collections::HashMap, fs};

use serde::Serialize;
use serde_json::Map;

fn mem_info() -> HashMap<String, String> {
fs::read_to_string("/proc/meminfo")
.expect("Failed to read /proc/meminfo")
.trim()
.split('\n')
.filter_map(|line| {
// Example line:
// MemTotal: 985768 kB
line.split_once(':')
.map(|(name, value)| (name.to_string(), value.trim().to_string()))
})
.collect()
}
mod common;
mod cpu;
mod io;
mod network;
use network::network_interface_stats;
mod memory;
use memory::{mem_info, mem_pressure};
mod sys;

#[derive(Debug, Serialize)]
struct NetworkInterfaceStats {
rx_bytes: u64,
rx_packets: u64,
rx_errors: u64,
rx_dropped_or_missed: u64,
rx_fifo_errors: u64,
rx_frame_errors: u64, // rx_length_errors + rx_over_errors + rx_crc_errors + rx_frame_errors
rx_compressed: u64,
multicast: u64,
tx_bytes: u64,
tx_packets: u64,
tx_errors: u64,
tx_dropped: u64,
tx_fifo_errors: u64,
collisions: u64,
tx_carrier_errors: u64, // carrier_errors + tx_aborted_errors + tx_window_errors + tx_heartbeat_errors
tx_compressed: u64,
}
use crate::cpu::{cpu_info, cpu_pressure};
use crate::io::io_pressure;
use crate::sys::{detect_platform, uptime};

fn network_interface_stats() -> HashMap<String, NetworkInterfaceStats> {
// The contents of `/proc/net/dev` is fairly hideous:
// $ cat /proc/net/dev
// Inter-| Receive | Transmit
// face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed
// lo: 476 4 0 0 0 0 0 0 476 4 0 0 0 0 0 0
// eth0: 201837803305 164530931 0 80597 0 0 0 0 17291765307 97844243 0 0 0 0 0 0
//
// ...and so on.
//
// Hat tip to this Stack Overflow answer https://stackoverflow.com/a/4943975 for pointing out
// where in the Linux source code these fields came from; the code has moved since that answer
// was written and is now here:
// https://github.com/torvalds/linux/blob/38e04b3/net/core/net-procfs.c#L77-L99

fs::read_to_string("/proc/net/dev")
.expect("Failed to read /proc/net/dev")
.trim()
.split('\n')
.filter_map(|line| {
line.split_once(':').map(|(interface_name, stats)| {
let stats = stats
.split_whitespace()
.map(|stat| stat.parse().unwrap())
.collect::<Vec<u64>>();
assert_eq!(stats.len(), 16);

(
interface_name.trim().to_string(),
NetworkInterfaceStats {
rx_bytes: stats[0],
rx_packets: stats[1],
rx_errors: stats[2],
rx_dropped_or_missed: stats[3],
rx_fifo_errors: stats[4],
rx_frame_errors: stats[5],
rx_compressed: stats[6],
multicast: stats[7],
tx_bytes: stats[8],
tx_packets: stats[9],
tx_errors: stats[10],
tx_dropped: stats[11],
tx_fifo_errors: stats[12],
collisions: stats[13],
tx_carrier_errors: stats[14],
tx_compressed: stats[15],
},
)
})
})
.collect()
}
// /proc/cgroups?
// /proc/crypto? lists available ciphers
// /proc/devices?
// /proc/diskstats?
// /proc/driver/*? e.g. rtc
// /proc/ioports?
// /proc/modules?

fn main() {
let cpu_info_json = serde_json::to_string(&cpu_info()).unwrap();
let cpu_pressure_json = serde_json::to_string(&cpu_pressure()).unwrap();
let io_pressure_json = serde_json::to_string(&io_pressure()).unwrap();
let mem_info_json = serde_json::to_string(&mem_info()).unwrap();
let mem_pressure_json = serde_json::to_string(&mem_pressure()).unwrap();
let network_interface_stats_json = serde_json::to_string(&network_interface_stats()).unwrap();
let sys_uptime_json = serde_json::to_string(&uptime()).unwrap();
let sys_platform_json = serde_json::to_string(&detect_platform()).unwrap();

let mut output = serde_json::Value::Object(Map::new());
output.as_object_mut().map(|output| {
output.insert(
String::from("cpu_info"),
serde_json::from_str(&cpu_info_json).unwrap(),
);
output.insert(
String::from("cpu_pressure"),
serde_json::from_str(&cpu_pressure_json).unwrap(),
);
output.insert(
String::from("io_pressure"),
serde_json::from_str(&io_pressure_json).unwrap(),
);
output.insert(
String::from("mem_info"),
serde_json::from_str(&mem_info_json).unwrap(),
);
output.insert(
String::from("mem_pressure"),
serde_json::from_str(&mem_pressure_json).unwrap(),
);
output.insert(
String::from("network_interface_stats"),
serde_json::from_str(&network_interface_stats_json).unwrap(),
);
output
output.insert(
String::from("sys_uptime"),
serde_json::from_str(&sys_uptime_json).unwrap(),
);
output.insert(
String::from("sys_platform"),
serde_json::from_str(&sys_platform_json).unwrap(),
);
});

println!("{}", serde_json::to_string_pretty(&output).unwrap());
Expand Down
Loading