Skip to content

Commit 576f3e0

Browse files
authored
Parse PSI cgroup v2 metrics (#1214)
* Parse PSI cgroup v2 metrics This commit introduces the parsing of v2 PSI metrics into lading. We special-case memory.pressure, cpu.pressure and io.pressure to be parsed. Signed-off-by: Brian L. Troutwine <[email protected]> * changelog Signed-off-by: Brian L. Troutwine <[email protected]> * tests Signed-off-by: Brian L. Troutwine <[email protected]> * debug -> warn Signed-off-by: Brian L. Troutwine <[email protected]> --------- Signed-off-by: Brian L. Troutwine <[email protected]>
1 parent 15c9e84 commit 576f3e0

File tree

2 files changed

+149
-1
lines changed

2 files changed

+149
-1
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
## Unreleased
8+
## Added
9+
- cgroup.v2 PSI metrics are now parsed.
10+
711
## [0.25.3]
812
## Changed
913
- Various dependencies updated, notably `hyper` is now 1.x.

lading/src/observer/linux/cgroup/v2.rs

+145-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ pub enum Error {
2020
ParseFloat(#[from] std::num::ParseFloatError),
2121
#[error("Cgroup v2 not found")]
2222
CgroupV2NotFound,
23+
#[error("Parsing PSI error: {0}")]
24+
ParsingPsi(String),
2325
}
2426

2527
/// Determines the cgroup v2 path for a given PID.
@@ -70,8 +72,21 @@ pub(crate) async fn poll(file_path: &Path, labels: &[(String, String)]) -> Resul
7072

7173
match fs::read_to_string(&file_path).await {
7274
Ok(content) => {
73-
let content = content.trim();
75+
if file_name == "memory.pressure"
76+
|| file_name == "io.pressure"
77+
|| file_name == "cpu.pressure"
78+
{
79+
if let Err(err) =
80+
parse_pressure(&content, &metric_prefix, labels)
81+
{
82+
warn!("[{path}] Failed to parse PSI contents: {err:?}",
83+
path = file_path.to_string_lossy()
84+
);
85+
}
86+
continue;
87+
}
7488

89+
let content = content.trim();
7590
// The format of cgroupv2 interface
7691
// files is defined here:
7792
// https://docs.kernel.org/admin-guide/cgroup-v2.html#interface-files
@@ -171,3 +186,132 @@ fn kv_pairs(
171186
}
172187
Ok(())
173188
}
189+
190+
fn parse_pressure(content: &str, prefix: &str, labels: &[(String, String)]) -> Result<(), Error> {
191+
for line in content.lines() {
192+
parse_pressure_line(line, prefix, |metric: String, value: f64| {
193+
gauge!(metric, labels).set(value);
194+
})?;
195+
}
196+
Ok(())
197+
}
198+
199+
fn parse_pressure_line<F>(line: &str, prefix: &str, mut f: F) -> Result<(), Error>
200+
where
201+
F: FnMut(String, f64),
202+
{
203+
// [some|full] avg10=FLOAT avg60=FLOAT avg300=FLOAT total=FLOAT
204+
let mut parts = line.split_whitespace();
205+
if let Some(category) = parts.next() {
206+
for field in parts {
207+
let Some((key, val)) = field.split_once('=') else {
208+
return Err(Error::ParsingPsi(format!("Invalid psi field: {field}")));
209+
};
210+
// It might be that total is an integer but for the sake of
211+
// simplicity we'll parse as f64. It has to become a float anyway
212+
// when we write it out as a metric.
213+
let value = val
214+
.parse::<f64>()
215+
.map_err(|err| Error::ParsingPsi(format!("{val} -> {err}")))?;
216+
217+
let metric_name = format!("{prefix}.{category}.{key}");
218+
f(metric_name, value);
219+
}
220+
} else {
221+
warn!("Unexpected blank category in psi file, skipping line: {line}");
222+
}
223+
Ok(())
224+
}
225+
226+
#[cfg(test)]
227+
mod tests {
228+
use super::parse_pressure_line;
229+
230+
#[test]
231+
fn parse_pressure_line_multiple_fields() {
232+
let line = "some avg10=0.42 avg60=1.0 total=42";
233+
let prefix = "cgroup.v2.memory.pressure";
234+
235+
let mut results = Vec::new();
236+
let res = parse_pressure_line(line, prefix, |metric, value| {
237+
results.push((metric, value));
238+
});
239+
240+
assert!(res.is_ok());
241+
assert_eq!(results.len(), 3);
242+
243+
assert_eq!(
244+
results[0],
245+
(String::from("cgroup.v2.memory.pressure.some.avg10"), 0.42)
246+
);
247+
assert_eq!(
248+
results[1],
249+
(String::from("cgroup.v2.memory.pressure.some.avg60"), 1.0)
250+
);
251+
assert_eq!(
252+
results[2],
253+
(String::from("cgroup.v2.memory.pressure.some.total"), 42.0)
254+
);
255+
}
256+
257+
#[test]
258+
fn parse_pressure_line_blank_line() {
259+
let line = "";
260+
let prefix = "cgroup.v2.memory.pressure";
261+
262+
let mut results = Vec::new();
263+
let res = parse_pressure_line(line, prefix, |metric, value| {
264+
results.push((metric, value));
265+
});
266+
267+
assert!(res.is_ok());
268+
assert!(results.is_empty());
269+
}
270+
271+
#[test]
272+
fn parse_pressure_line_incomplete() {
273+
let line = "some";
274+
let prefix = "cgroup.v2.memory.pressure";
275+
276+
let mut results = Vec::new();
277+
let res = parse_pressure_line(line, prefix, |metric, value| {
278+
results.push((metric, value));
279+
});
280+
281+
assert!(res.is_ok());
282+
assert!(results.is_empty());
283+
}
284+
285+
#[test]
286+
fn parse_pressure_line_malformed_field() {
287+
let line = "some avg10=0.0 avg60?";
288+
let prefix = "cgroup.v2.memory.pressure";
289+
290+
let mut results = Vec::new();
291+
let res = parse_pressure_line(line, prefix, |metric, value| {
292+
results.push((metric, value));
293+
});
294+
295+
// Intentionally grab as many fields as possible
296+
assert!(res.is_err());
297+
assert_eq!(results.len(), 1);
298+
assert_eq!(
299+
results[0],
300+
(String::from("cgroup.v2.memory.pressure.some.avg10"), 0.0)
301+
);
302+
}
303+
304+
#[test]
305+
fn parse_pressure_line_invalid_value() {
306+
let line = "some avg10=hello";
307+
let prefix = "cgroup.v2.memory.pressure";
308+
309+
let mut results = Vec::new();
310+
let res = parse_pressure_line(line, prefix, |metric, value| {
311+
results.push((metric, value));
312+
});
313+
314+
assert!(res.is_err());
315+
assert!(results.is_empty());
316+
}
317+
}

0 commit comments

Comments
 (0)