@@ -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