proc_sys_parser/
stat.rs

1/*!
2Read data from `/proc/stat` into the struct [`ProcStat`].
3
4The Documentation for `/proc/stat` is found here: <https://www.kernel.org/doc/Documentation/filesystems/proc.txt>.
5
6Please mind that the description of "steal" time in the kernel source describes 'involuntary wait time'.
7This is true, but involuntary waiting means virtualization makes the kernel (virtual machine) wait.
8This is implemented for PowerPC, S390 and X86, and for X86 for paravirtualization.
9
10The stat module converts the jiffies from `/proc/stat` from the cpu_total and cpu_individual [`CpuStat`]
11structs into milliseconds. It does that by taking the `CLK-TCK` (clock tick) sysconf variable set by
12`CONFIG_HZ`, and calculate the time in milliseconds from the cpu state jiffies value in the following way:
13
14```text
15(CPUSTATE_JIFFIES * 1000)        / CLK_TCK
16convert seconds to milliseconds    divide by ticks per second
17```
18Example usage of stat:
19```no_run
20use proc_sys_parser::{stat, stat::{ProcStat, CpuStat}};
21
22let proc_stat = stat::read();
23```
24Example output:
25```text
26ProcStat {
27    cpu_total: CpuStat { name: "cpu", user: 8570, nice: 0, system: 7530, idle: 1710040, iowait: 2780, irq: 0, softirq: 150, steal: 0, guest: 0, guest_nice: 0 },
28    cpu_individual: [CpuStat { name: "cpu0", user: 1800, nice: 0, system: 1450, idle: 283400, iowait: 460, irq: 0, softirq: 120, steal: 0, guest: 0, guest_nice: 0 },
29                     CpuStat { name: "cpu1", user: 1720, nice: 0, system: 1320, idle: 284780, iowait: 580, irq: 0, softirq: 0, steal: 0, guest: 0, guest_nice: 0 },
30                     CpuStat { name: "cpu2", user: 1060, nice: 0, system: 1220, idle: 285410, iowait: 510, irq: 0, softirq: 0, steal: 0, guest: 0, guest_nice: 0 },
31                     CpuStat { name: "cpu3", user: 890, nice: 0, system: 990, idle: 286130, iowait: 450, irq: 0, softirq: 0, steal: 0, guest: 0, guest_nice: 0 },
32                     CpuStat { name: "cpu4", user: 1400, nice: 0, system: 1280, idle: 285260, iowait: 310, irq: 0, softirq: 30, steal: 0, guest: 0, guest_nice: 0 },
33                     CpuStat { name: "cpu5", user: 1680, nice: 0, system: 1250, idle: 285020, iowait: 450, irq: 0, softirq: 0, steal: 0, guest: 0, guest_nice: 0 }],
34    interrupts: [184655, 0, 4500, 60546, 0, 0, 0, 2, 0, 0, 0, 70138, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 548, 0, 0, 0, 0, 0, 2, 0, 3410, 2927, 4739, 5542, 1595, 1913, 0, 0, 0, 79, 154, 208, 282, 43, 52, 0, 14842, 11679, 0, 0, 17, 0, 0, 0, 0, 0, 0, 0, 1437, 0, 0, 0, 0, 0, 0],
35    context_switches: 275716,
36    boot_time: 1702127060,
37    processes: 3472,
38    processes_running: 1,
39    processes_blocked: 0,
40    softirq: [99012, 30, 8368, 2, 24666, 11, 0, 208, 15031, 0, 50696]
41}
42```
43(edited for readability)
44
45If you want to change the path and/or file that is read for [`ProcStat`], which is `/proc/stat`, by
46default, use:
47```no_run
48use proc_sys_parser::{stat, stat::{ProcStat, CpuStat, Builder}};
49
50let proc_stat = Builder::new().path("/myproc").read();
51```
52
53*/
54use nix::unistd::{sysconf, SysconfVar};
55use std::fs::read_to_string;
56use crate::ProcSysParserError;
57use log::warn;
58
59
60/// Struct for holding cpu times in milliseconds
61#[derive(Debug, PartialEq, Default)]
62pub struct CpuStat {
63    /// cpu name. 'cpu' means total of all cpus, cpuN means individual cpu
64    pub name: String,
65    /// user time in milliseconds
66    pub user: u64,
67    /// user time reniced in milliseconds
68    pub nice: u64,
69    /// system/kernel time in milliseconds
70    pub system: u64,
71    /// idle time in milliseconds
72    pub idle: u64,
73    /// idle time in milliseconds attributed to performing IO
74    pub iowait: Option<u64>,
75    /// irq time in milliseconds
76    pub irq: Option<u64>,
77    /// softirq time in milliseconds
78    pub softirq: Option<u64>,
79    /// steal time in milliseconds
80    /// Introduced with kernel version 2.6.11
81    pub steal: Option<u64>,
82    /// guest user time in milliseconds
83    /// Introduced with kernel version 2.6.24
84    pub guest: Option<u64>,
85    /// guest user time reniced in milliseconds
86    /// Introduced with kernel version 2.6.24
87    pub guest_nice: Option<u64>,
88}
89
90/// Builder pattern for [`ProcStat`]
91#[derive(Default)]
92pub struct Builder {
93    pub proc_path : String,
94    pub proc_file : String,
95}
96
97impl Builder {
98    pub fn new() -> Builder {
99        Builder { 
100            proc_path: "/proc".to_string(),
101            proc_file: "stat".to_string(),
102        }
103    }
104
105    pub fn path(mut self, proc_path: &str) -> Builder {
106        self.proc_path = proc_path.to_string();
107        self
108    }
109    pub fn file(mut self, proc_file: &str) -> Builder {
110        self.proc_file = proc_file.to_string();
111        self
112    }
113    pub fn read(self) -> Result<ProcStat, ProcSysParserError> {
114        ProcStat::read_proc_stat(format!("{}/{}", &self.proc_path, &self.proc_file).as_str())
115    }
116}
117
118/// The main function for building a [`ProcStat`] struct with current data.
119/// This uses the Builder pattern, which allows settings such as the filename to specified.
120pub fn read() -> Result<ProcStat, ProcSysParserError> {
121   Builder::new().read()
122}
123
124/// Struct for holding `/proc/stat` statistics
125#[derive(Debug, PartialEq, Default)]
126pub struct ProcStat {
127    pub cpu_total: CpuStat,
128    pub cpu_individual: Vec<CpuStat>,
129    pub interrupts: Vec<u64>,
130    pub context_switches: u64,
131    pub boot_time: u64,
132    pub processes: u64,
133    pub processes_running: u64,
134    pub processes_blocked: u64,
135    pub softirq: Vec<u64>,
136}
137
138impl ProcStat {
139    pub fn new() -> ProcStat {
140        ProcStat::default() 
141    }
142    pub fn parse_proc_stat_output(proc_stat: &str,) -> Result<ProcStat, ProcSysParserError> {
143        let mut procstat = ProcStat::new();
144        for line in proc_stat.lines() {
145            match line {
146                line if line.starts_with("cpu ") => {
147                    procstat.cpu_total = CpuStat::generate_cpu_times(line)?;
148                },
149                line if line.starts_with("cpu") && line.chars().nth(3) != Some(' ') => {
150                    procstat.cpu_individual.push(CpuStat::generate_cpu_times(line)?);
151                },
152                line if line.starts_with("intr ") => {
153                    procstat.interrupts = ProcStat::generate_number_vector(line)?;
154                },
155                line if line.starts_with("ctxt ") => {
156                    procstat.context_switches = ProcStat::generate_number_unsigned(line)?;
157                },
158                line if line.starts_with("btime ") => {
159                    procstat.boot_time = ProcStat::generate_number_unsigned(line)?;
160                },
161                line if line.starts_with("processes ") => {
162                    procstat.processes = ProcStat::generate_number_unsigned(line)?;
163                },
164                line if line.starts_with("procs_running ") => {
165                    procstat.processes_running = ProcStat::generate_number_unsigned(line)?;
166                },
167                line if line.starts_with("procs_blocked ") => {
168                    procstat.processes_blocked = ProcStat::generate_number_unsigned(line)?;
169                },
170                line if line.starts_with("softirq ") => {
171                    procstat.softirq = ProcStat::generate_number_vector(line)?;
172                },
173                _  => warn!("stat: unknown entry found: {}", line),
174            }
175        }
176        Ok(procstat)
177    }
178    fn generate_number_vector(proc_stat_line: &str) -> Result<Vec<u64>, ProcSysParserError> {
179        proc_stat_line.split_whitespace()
180            .skip(1)
181            .map(|row| row.parse::<u64>().map_err(ProcSysParserError::ParseToIntegerError))
182            .collect::<Vec<_>>()
183            .into_iter()
184            .collect::<Result<Vec<_>, _>>()
185    }
186    fn generate_number_unsigned(proc_stat_line: &str) -> Result<u64, ProcSysParserError> {
187        proc_stat_line.split_whitespace()
188            .nth(1)
189            .ok_or(ProcSysParserError::IteratorItemError {item: "stat generate_number_unsigned".to_string() })?
190            .parse::<u64>().map_err(ProcSysParserError::ParseToIntegerError)
191    }
192    pub fn read_proc_stat(proc_stat_file: &str) -> Result<ProcStat, ProcSysParserError> {
193        let proc_stat_output = read_to_string(proc_stat_file)
194            .map_err(|error| ProcSysParserError::FileReadError { file: proc_stat_file.to_string(), error })?;
195        ProcStat::parse_proc_stat_output(&proc_stat_output)
196    }
197}
198
199impl CpuStat {
200    pub fn generate_cpu_times(proc_stat_cpu_line: &str) -> Result<CpuStat, ProcSysParserError> {
201        // Note: time in jiffies, must be divided by CLK_TCK to show time in seconds.
202        // CLK_TCK is set by CONFIG_HZ and is 100 on most enterprise linuxes.
203        let clock_time = sysconf(SysconfVar::CLK_TCK).unwrap_or(Some(100)).unwrap_or(100) as u64;
204
205        let parse_next_and_conversion_into_option_milliseconds = |result: Option<&str>, clock_time: u64 | -> Option<u64> {
206            match result {
207                None => None,
208                Some(value) => {
209                    match value.parse::<u64>() {
210                        Err(_) => None,
211                        Ok(number) => Some((number*1000_u64)/clock_time),
212                    }
213                },
214            }
215        };
216
217        let mut splitted = proc_stat_cpu_line.split_whitespace();
218        Ok(CpuStat {
219            name: splitted.next()
220                .ok_or(ProcSysParserError::IteratorItemError {item: "stat generate_cpu_times name".to_string() })?
221                .to_string(),
222            user: ((splitted.next()
223                .ok_or(ProcSysParserError::IteratorItemError {item: "stat generate_cpu_times user".to_string() })?
224                .parse::<u64>().map_err(ProcSysParserError::ParseToIntegerError)? *1000_u64)/clock_time),
225            nice: ((splitted.next()
226                .ok_or(ProcSysParserError::IteratorItemError {item: "stat generate_cpu_times nice".to_string() })?
227                .parse::<u64>().map_err(ProcSysParserError::ParseToIntegerError)? *1000_u64)/clock_time),
228            system: ((splitted.next()
229                .ok_or(ProcSysParserError::IteratorItemError {item: "stat generate_cpu_times system".to_string() })?
230                .parse::<u64>().map_err(ProcSysParserError::ParseToIntegerError)? *1000_u64)/clock_time),
231            idle: ((splitted.next()
232                .ok_or(ProcSysParserError::IteratorItemError {item: "stat generate_cpu_times idle".to_string() })?
233                .parse::<u64>().map_err(ProcSysParserError::ParseToIntegerError)? *1000_u64)/clock_time),
234            iowait: parse_next_and_conversion_into_option_milliseconds(splitted.next(), clock_time),
235            irq: parse_next_and_conversion_into_option_milliseconds(splitted.next(), clock_time),
236            softirq: parse_next_and_conversion_into_option_milliseconds(splitted.next(), clock_time),
237            steal: parse_next_and_conversion_into_option_milliseconds(splitted.next(), clock_time),
238            guest: parse_next_and_conversion_into_option_milliseconds(splitted.next(), clock_time),
239            guest_nice: parse_next_and_conversion_into_option_milliseconds(splitted.next(), clock_time),
240        })
241    }
242}
243
244#[cfg(test)]
245mod tests {
246    use std::fs::{write, create_dir_all, remove_dir_all};
247    use rand::{thread_rng, Rng};
248    use rand::distributions::Alphanumeric;
249    use super::*;
250
251    // cpu times are in jiffies, which are clock ticks.
252    // clock ticks are defined in the getconf value CLK_TCK.
253    // this crate dynamically obtains the CLK_TCK value.
254    // the common value of CLK_TCK is 100, which is a hard assumption here.
255    #[test]
256    fn parse_cpu_line() {
257        let cpu_line = "cpu  101521 47 66467 43586274 7651 0 1367 0 0 0";
258        let result = CpuStat::generate_cpu_times(&cpu_line).unwrap();
259        assert_eq!(result, CpuStat { name:"cpu".to_string(), user:1015210, nice:470, system:664670, idle:435862740, iowait:Some(76510), irq:Some(0), softirq:Some(13670), steal:Some(0), guest:Some(0), guest_nice:Some(0) });
260    }
261
262    // This mimics a (much) lower linux version which provides lesser statistics
263    // The statistics will be set to zero.
264    #[test]
265    fn parse_cpu_line_with_less_statistics() {
266        let cpu_line = "cpu  101521 47 66467 43586274";
267        let result = CpuStat::generate_cpu_times(&cpu_line).unwrap();
268        assert_eq!(result, CpuStat { name:"cpu".to_string(), user:1015210, nice:470, system:664670, idle:435862740, iowait:None, irq:None, softirq:None, steal:None, guest:None, guest_nice:None });
269    }
270
271
272    #[test]
273    fn parse_interrupt_line() {
274        let interrupt_line = "intr 21965856 0 520030 7300523 0 0 0 2 0 0 0 12267292 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 644 0 0 0 0 0 2 0 77822 81889 80164 70697 68349 79207 0 0 0 6172 6117 6131 5983 6483 6062 0 588204 437602 0 0 1202 0 0 0 0 0 0 0 0 0 0 0 355279 0 0";
275        let result = ProcStat::generate_number_vector(&interrupt_line).unwrap();
276        assert_eq!(result, vec![21965856, 0, 520030, 7300523, 0, 0, 0, 2, 0, 0, 0, 12267292, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 644, 0, 0, 0, 0, 0, 2, 0, 77822, 81889, 80164, 70697, 68349, 79207, 0, 0, 0, 6172, 6117, 6131, 5983, 6483, 6062, 0, 588204, 437602, 0, 0, 1202, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 355279, 0, 0]);
277    }
278
279    #[test]
280    fn parse_context_switches_line() {
281        let context_switches_line = "ctxt 36432936";
282        let result = ProcStat::generate_number_unsigned(context_switches_line).unwrap();
283        assert_eq!(result, 36432936);
284
285    }
286
287    #[test]
288    fn parse_full_proc_stat_file() {
289        let proc_stat = "cpu  101521 47 66467 43586274 7651 0 1367 0 0 0
290cpu0 16298 0 11590 7259262 1213 0 846 0 0 0
291cpu1 16272 0 11291 7265615 1289 0 110 0 0 0
292cpu2 16121 47 10986 7266358 1251 0 111 0 0 0
293cpu3 17786 0 11023 7264715 1350 0 116 0 0 0
294cpu4 17426 0 10736 7265491 1195 0 79 0 0 0
295cpu5 17616 0 10840 7264832 1351 0 103 0 0 0
296intr 21965856 0 520030 7300523 0 0 0 2 0 0 0 12267292 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 644 0 0 0 0 0 2 0 77822 81889 80164 70697 68349 79207 0 0 0 6172 6117 6131 5983 6483 6062 0 588204 437602 0 0 1202 0 0 0 0 0 0 0 0 0 0 0 355279 0 0
297ctxt 36432936
298btime 1701783048
299processes 345159
300procs_running 1
301procs_blocked 0
302softirq 7616206 32 1416021 213 1102885 11 0 1409 2270709 0 2824926";
303        let result = ProcStat::parse_proc_stat_output(proc_stat).unwrap();
304        assert_eq!(result, ProcStat { cpu_total: CpuStat { name: "cpu".to_string(), user: 1015210, nice: 470, system: 664670, idle: 435862740, iowait: Some(76510), irq: Some(0), softirq: Some(13670), steal: Some(0), guest: Some(0), guest_nice: Some(0) },
305            cpu_individual: vec![CpuStat { name: "cpu0".to_string(), user: 162980, nice: 0, system: 115900, idle: 72592620, iowait: Some(12130), irq: Some(0), softirq: Some(8460), steal: Some(0), guest: Some(0), guest_nice: Some(0) },
306                                 CpuStat { name: "cpu1".to_string(), user: 162720, nice: 0, system: 112910, idle: 72656150, iowait: Some(12890), irq: Some(0), softirq: Some(1100), steal: Some(0), guest: Some(0), guest_nice: Some(0) },
307                                 CpuStat { name: "cpu2".to_string(), user: 161210, nice: 470, system: 109860, idle: 72663580, iowait: Some(12510), irq: Some(0), softirq: Some(1110), steal: Some(0), guest: Some(0), guest_nice: Some(0) },
308                                 CpuStat { name: "cpu3".to_string(), user: 177860, nice: 0, system: 110230, idle: 72647150, iowait: Some(13500), irq: Some(0), softirq: Some(1160), steal: Some(0), guest: Some(0), guest_nice: Some(0) },
309                                 CpuStat { name: "cpu4".to_string(), user: 174260, nice: 0, system: 107360, idle: 72654910, iowait: Some(11950), irq: Some(0), softirq: Some(790), steal: Some(0), guest: Some(0), guest_nice: Some(0) },
310                                 CpuStat { name: "cpu5".to_string(), user: 176160, nice: 0, system: 108400, idle: 72648320, iowait: Some(13510), irq: Some(0), softirq: Some(1030), steal: Some(0), guest: Some(0), guest_nice: Some(0) }],
311            interrupts: vec![21965856, 0, 520030, 7300523, 0, 0, 0, 2, 0, 0, 0, 12267292, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 644, 0, 0, 0, 0, 0, 2, 0, 77822, 81889, 80164, 70697, 68349, 79207, 0, 0, 0, 6172, 6117, 6131, 5983, 6483, 6062, 0, 588204, 437602, 0, 0, 1202, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 355279, 0, 0],
312            context_switches: 36432936,
313            boot_time: 1701783048,
314            processes: 345159,
315            processes_running: 1,
316            processes_blocked: 0,
317            softirq: vec![7616206, 32, 1416021, 213, 1102885, 11, 0, 1409, 2270709, 0, 2824926],
318        });
319    }
320
321    #[test]
322    fn create_proc_stat_file_and_read()
323    {
324        let proc_stat = "cpu  1 1 1 1 1 0 1 0 0 0
325cpu0 1 1 1 1 1 0 1 0 0 0
326intr 100 0 1 1
327ctxt 100
328btime 100
329processes 10
330procs_running 1
331procs_blocked 0
332softirq 100 0 1 1";
333        let directory_suffix: String = thread_rng().sample_iter(&Alphanumeric).take(8).map(char::from).collect();
334        let test_path = format!("/tmp/test.{}", directory_suffix);
335        create_dir_all(test_path.clone()).expect("Error creating mock sysfs directories.");
336        
337        write(format!("{}/stat", test_path), proc_stat).expect(format!("Error writing to {}/stat", test_path).as_str());
338        let result = Builder::new().path(&test_path).read().unwrap();
339        remove_dir_all(test_path).unwrap();
340
341        assert_eq!(result, ProcStat { cpu_total: CpuStat { name: "cpu".to_string(), user: 10, nice: 10, system: 10, idle: 10, iowait: Some(10), irq: Some(0), softirq: Some(10), steal: Some(0), guest: Some(0), guest_nice: Some(0) },
342            cpu_individual: vec![CpuStat { name: "cpu0".to_string(),user: 10, nice: 10, system: 10, idle: 10, iowait: Some(10), irq: Some(0), softirq: Some(10), steal: Some(0), guest: Some(0), guest_nice: Some(0) }],
343            interrupts: vec![100, 0, 1, 1],
344            context_switches: 100,
345            boot_time: 100,
346            processes: 10,
347            processes_running: 1,
348            processes_blocked: 0,
349            softirq: vec![100, 0, 1, 1],
350        });
351    }
352}