use std::collections::HashMap;
use std::time::Instant;
const USER_HZ: f64 = 100.0;
const CPU_PCT_MAX: f32 = 6400.0;
pub fn parse_cgroup(content: &str) -> Option<String> {
for line in content.lines() {
let path = line.rsplit(':').next().unwrap_or(line);
for seg in path.split('/') {
if let Some(label) = recognize_segment(seg) {
return Some(label);
}
}
if let Some(name) = lxc_name(path) {
return Some(format!("lxc:{name}"));
}
if path.split('/').any(|s| s.starts_with("kubepods")) {
return Some("k8s:?".into());
}
}
None
}
fn recognize_segment(seg: &str) -> Option<String> {
let body = seg.strip_suffix(".scope").unwrap_or(seg);
if let Some(id) = body.strip_prefix("docker-") {
return short_hex(id).map(|h| format!("docker:{h}"));
}
if let Some(id) = body.strip_prefix("cri-containerd-") {
return short_hex(id).map(|h| format!("k8s:{h}"));
}
if let Some(id) = body.strip_prefix("crio-") {
return short_hex(id).map(|h| format!("k8s:{h}"));
}
if let Some(id) = body.strip_prefix("containerd-") {
return short_hex(id).map(|h| format!("k8s:{h}"));
}
if let Some(id) = body.strip_prefix("libpod-") {
return short_hex(id).map(|h| format!("podman:{h}"));
}
None
}
fn lxc_name(path: &str) -> Option<&str> {
let segs: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
for (i, seg) in segs.iter().enumerate() {
if *seg == "lxc" {
if let Some(name) = segs.get(i + 1) {
return non_empty(name);
}
}
if let Some(name) = seg.strip_prefix("lxc.payload.") {
return non_empty(name);
}
}
None
}
fn short_hex(id: &str) -> Option<String> {
if id.len() >= 12 && id.bytes().all(|b| b.is_ascii_hexdigit()) {
Some(id[..12].to_ascii_lowercase())
} else {
None
}
}
fn non_empty(s: &str) -> Option<&str> {
(!s.is_empty()).then_some(s)
}
pub fn container_of(pid: u32) -> Option<String> {
let content = std::fs::read_to_string(format!("/proc/{pid}/cgroup")).ok()?;
parse_cgroup(&content)
}
#[derive(Default)]
pub struct CpuTracker {
seen: HashMap<u32, (u64, Instant)>,
}
impl CpuTracker {
pub fn new() -> Self {
Self::default()
}
pub fn sample(&mut self, pid: u32) -> Option<f32> {
let content = std::fs::read_to_string(format!("/proc/{pid}/stat")).ok()?;
let ticks = parse_stat_ticks(&content)?;
let now = Instant::now();
let prev = self.seen.insert(pid, (ticks, now));
let (prev_ticks, prev_instant) = prev?;
let delta_ticks = ticks.checked_sub(prev_ticks)?;
let elapsed = now.duration_since(prev_instant).as_secs_f64();
if elapsed <= 0.0 {
return None;
}
let pct = (delta_ticks as f64 / USER_HZ / elapsed * 100.0) as f32;
Some(pct.clamp(0.0, CPU_PCT_MAX))
}
pub fn prune(&mut self, live_pids: &[u32]) {
self.seen.retain(|pid, _| live_pids.contains(pid));
}
}
pub fn parse_stat_ticks(stat: &str) -> Option<u64> {
let tail = stat.rsplit_once(')')?.1;
let mut fields = tail.split_whitespace();
let utime: u64 = fields.nth(11)?.parse().ok()?;
let stime: u64 = fields.next()?.parse().ok()?;
utime.checked_add(stime)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn docker_scope_yields_short_id() {
let c = "0::/system.slice/docker-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d.scope\n";
assert_eq!(parse_cgroup(c).as_deref(), Some("docker:1a2b3c4d5e6f"));
}
#[test]
fn cri_containerd_and_crio_map_to_k8s() {
let containerd = "0::/kubepods.slice/kubepods-burstable.slice/\
kubepods-burstable-pod123.slice/\
cri-containerd-abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789.scope\n";
assert_eq!(
parse_cgroup(containerd).as_deref(),
Some("k8s:abcdef012345")
);
let crio = "0::/kubepods.slice/kubepods-besteffort.slice/\
crio-fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210.scope\n";
assert_eq!(parse_cgroup(crio).as_deref(), Some("k8s:fedcba987654"));
}
#[test]
fn kubepods_without_recognizable_id_is_unknown_pod() {
let c = "0::/kubepods.slice/kubepods-besteffort.slice/kubepods-besteffort-pod9f8e.slice\n";
assert_eq!(parse_cgroup(c).as_deref(), Some("k8s:?"));
}
#[test]
fn libpod_scope_yields_podman() {
let c = "0::/machine.slice/\
libpod-0f1e2d3c4b5a69788796a5b4c3d2e1f00f1e2d3c4b5a69788796a5b4c3d2e1f0.scope\n";
assert_eq!(parse_cgroup(c).as_deref(), Some("podman:0f1e2d3c4b5a"));
}
#[test]
fn lxc_both_path_layouts_surface_the_name() {
let plain = "11:cpuset:/lxc/webserver\n10:devices:/lxc/webserver\n";
assert_eq!(parse_cgroup(plain).as_deref(), Some("lxc:webserver"));
let payload = "0::/lxc.payload.dbnode/system.slice/postgres.service\n";
assert_eq!(parse_cgroup(payload).as_deref(), Some("lxc:dbnode"));
}
#[test]
fn host_process_is_none() {
let c = "0::/user.slice/user-1000.slice/session-2.scope\n";
assert_eq!(parse_cgroup(c), None);
assert_eq!(parse_cgroup("0::/system.slice/sshd.service\n"), None);
}
#[test]
fn garbage_and_decoy_ids_are_none() {
assert_eq!(parse_cgroup(""), None);
assert_eq!(parse_cgroup("not a cgroup file at all"), None);
assert_eq!(
parse_cgroup("0::/system.slice/docker-notanid.scope\n"),
None
);
assert_eq!(
parse_cgroup("0::/system.slice/docker-deadbeef.scope\n"),
None
);
}
#[test]
fn stat_ticks_handles_comm_with_spaces_and_parens() {
let line = "1234 ((my proc) worker) S 1 1234 1234 0 -1 4194560 \
1000 0 5 0 111 22 0 0 20 0 1 0 9999 123456 789 ...\n";
assert_eq!(parse_stat_ticks(line), Some(133));
}
#[test]
fn stat_ticks_simple_comm() {
let line = "980 (python) R 1 980 980 0 -1 4194304 200 0 0 0 50 10 0 0 20 0 8 0 1000 0 0\n";
assert_eq!(parse_stat_ticks(line), Some(60));
}
#[test]
fn stat_ticks_rejects_truncated_or_garbage() {
assert_eq!(parse_stat_ticks("1234 (python S 1 1 1"), None);
assert_eq!(parse_stat_ticks("1234 (python) S 1 1"), None);
let bad = "1 (x) S 1 1 1 0 -1 0 0 0 0 0 xx 22 0 0 20 0 1 0 1 0 0\n";
assert_eq!(parse_stat_ticks(bad), None);
}
#[test]
fn cpu_tracker_first_sighting_has_no_baseline() {
let mut t = CpuTracker::new();
let me = std::process::id();
assert_eq!(
t.sample(me),
None,
"first sighting establishes baseline only"
);
}
#[test]
fn cpu_tracker_prune_drops_dead_pids() {
let mut t = CpuTracker::new();
t.seen.insert(111, (10, Instant::now()));
t.seen.insert(222, (20, Instant::now()));
t.seen.insert(333, (30, Instant::now()));
t.prune(&[222]);
assert!(!t.seen.contains_key(&111));
assert!(t.seen.contains_key(&222));
assert!(!t.seen.contains_key(&333));
}
}