1use anyhow::anyhow;
2use prometheus_client::collector::Collector;
3use prometheus_client::encoding::DescriptorEncoder;
4use prometheus_client::metrics::counter::ConstCounter;
5use prometheus_client::metrics::gauge::ConstGauge;
6use regex::Regex;
7use serde::Serialize;
8use std::fmt::Error;
9
10lazy_static::lazy_static! {
11 static ref CGROUP_V2_PATH_RE: Regex = Regex::new(r#"(?m)^0::(/.*)$"#).unwrap();
12 static ref MEMORY_CURRENT_PATH: String = get_memory_current_path().unwrap_or_else(|_| String::new());
13 static ref MEMORY_STAT_PATH: String = get_memory_stat_path().unwrap_or_else(|_| String::new());
14}
15
16fn get_cgroupv2_path() -> anyhow::Result<String> {
17 let cgroup_path = String::from_utf8(std::fs::read("/proc/self/cgroup")?)?;
18
19 CGROUP_V2_PATH_RE
20 .captures(&cgroup_path)
21 .map(|x| format!("/sys/fs/cgroup{}", x.get(1).unwrap().as_str()))
22 .ok_or_else(|| anyhow::anyhow!("Failed to parse cgroup path"))
23}
24
25fn get_memory_current_path() -> anyhow::Result<String> {
26 let path = get_cgroupv2_path()?;
27 Ok(format!("{}/memory.current", path))
28}
29
30fn get_memory_stat_path() -> anyhow::Result<String> {
31 let path = get_cgroupv2_path()?;
32 Ok(format!("{}/memory.stat", path))
33}
34
35pub fn get_memory() -> anyhow::Result<u64> {
36 let content = std::fs::read_to_string(&*MEMORY_CURRENT_PATH)?;
37 content
38 .trim()
39 .parse::<u64>()
40 .map_err(|e| anyhow::anyhow!("Failed to parse memory value: {}", e))
41}
42
43pub fn get_stats() -> anyhow::Result<MemoryStat> {
44 let content = std::fs::read_to_string(&*MEMORY_STAT_PATH)?;
45 let mut r = MemoryStat::parse(content.trim())
46 .map_err(|e| anyhow::anyhow!("Failed to parse memory stat: {}", e))?;
47 r.usage = get_memory()?;
48 r.working_set = r.usage.saturating_sub(r.inactive_file);
49 Ok(r)
50}
51
52#[derive(Default, Debug, Clone, Serialize)]
54pub struct MemoryStat {
55 pub usage: u64,
56 pub working_set: u64,
58 pub anon: u64,
60 pub inactive_anon: u64,
61 pub active_anon: u64,
62 pub file: u64,
64 pub file_mapped: u64,
65 pub active_file: u64,
66 pub inactive_file: u64,
67 pub shmem: u64,
68 pub kernel: u64,
70 pub kernel_stack: u64,
71 pub pagetables: u64,
72 pub percpu: u64,
73 pub sock: u64,
74 pub slab: u64,
75 pub slab_reclaimable: u64,
76 pub slab_unreclaimable: u64,
77 pub pgfault: u64,
78 pub pgmajfault: u64,
79 pub workingset_refault_anon: u64,
80 pub workingset_refault_file: u64,
81 pub workingset_activate_anon: u64,
82 pub workingset_activate_file: u64,
83 pub workingset_restore_anon: u64,
84 pub workingset_restore_file: u64,
85}
86
87impl MemoryStat {
88 fn parse(content: &str) -> anyhow::Result<Self> {
89 let mut res = MemoryStat::default();
90
91 for line in content.lines() {
92 let mut parts = line.split_whitespace();
93 let key = parts
94 .next()
95 .ok_or_else(|| anyhow!("Invalid line: '{}' (no key)", line))?;
96 let value = parts
97 .next()
98 .ok_or_else(|| anyhow!("Invalid line: '{}' (no value)", line))?
99 .parse::<u64>()
100 .map_err(|_| anyhow!("Invalid line: '{}' (invalid value)", line))?;
101 if parts.next().is_some() {
102 return Err(anyhow!("Invalid line: '{}' (too many parts)", line));
103 }
104
105 match key {
106 "anon" => res.anon = value,
107 "inactive_anon" => res.inactive_anon = value,
108 "active_anon" => res.active_anon = value,
109 "file" => res.file = value,
110 "file_mapped" => res.file_mapped = value,
111 "active_file" => res.active_file = value,
112 "inactive_file" => res.inactive_file = value,
113 "shmem" => res.shmem = value,
114 "kernel" => res.kernel = value,
115 "kernel_stack" => res.kernel_stack = value,
116 "pagetables" => res.pagetables = value,
117 "percpu" => res.percpu = value,
118 "sock" => res.sock = value,
119 "slab" => res.slab = value,
120 "slab_reclaimable" => res.slab_reclaimable = value,
121 "slab_unreclaimable" => res.slab_unreclaimable = value,
122 "pgfault" => res.pgfault = value,
123 "pgmajfault" => res.pgmajfault = value,
124 "workingset_refault_anon" => res.workingset_refault_anon = value,
125 "workingset_refault_file" => res.workingset_refault_file = value,
126 "workingset_activate_anon" => res.workingset_activate_anon = value,
127 "workingset_activate_file" => res.workingset_activate_file = value,
128 "workingset_restore_anon" => res.workingset_restore_anon = value,
129 "workingset_restore_file" => res.workingset_restore_file = value,
130 _ => {},
132 }
133 }
134
135 Ok(res)
136 }
137}
138
139#[derive(Debug, Clone)]
140pub struct PrometheusCollector {}
141
142impl PrometheusCollector {
143 pub fn register(registry: &mut prometheus_client::registry::Registry) {
144 registry.register_collector(Box::new(Self {}))
145 }
146}
147
148impl Collector for PrometheusCollector {
149 fn encode(&self, mut encoder: DescriptorEncoder) -> Result<(), Error> {
150 use prometheus_client::encoding::EncodeMetric;
151 let Ok(s) = get_stats() else {
152 return Ok(());
153 };
154
155 fn encode_gauge(
156 encoder: &mut DescriptorEncoder,
157 value: u64,
158 name: &'static str,
159 help: &str,
160 ) -> Result<(), Error> {
161 let metric = ConstGauge::new(value);
162 let metric_encoder = encoder.encode_descriptor(name, help, None, metric.metric_type())?;
163 metric.encode(metric_encoder)?;
164 Ok(())
165 }
166
167 fn encode_counter(
168 encoder: &mut DescriptorEncoder,
169 value: u64,
170 name: &'static str,
171 help: &str,
172 ) -> Result<(), Error> {
173 let metric = ConstCounter::new(value);
174 let metric_encoder = encoder.encode_descriptor(name, help, None, metric.metric_type())?;
175 metric.encode(metric_encoder)?;
176 Ok(())
177 }
178
179 encode_gauge(
180 &mut encoder,
181 s.usage,
182 "cgroup_usage",
183 "current memory usage",
184 )?;
185 encode_gauge(
186 &mut encoder,
187 s.working_set,
188 "cgroup_working_set",
189 "current working set",
190 )?;
191 encode_gauge(
192 &mut encoder,
193 s.anon,
194 "cgroup_anon",
195 "current anonymous usage",
196 )?;
197 encode_gauge(
198 &mut encoder,
199 s.inactive_anon,
200 "cgroup_inactive_anon",
201 "current inactive anonymous usage",
202 )?;
203 encode_gauge(
204 &mut encoder,
205 s.active_anon,
206 "cgroup_active_anon",
207 "current active anonymous usage",
208 )?;
209 encode_gauge(&mut encoder, s.file, "cgroup_file", "current file usage")?;
210 encode_gauge(
211 &mut encoder,
212 s.file_mapped,
213 "cgroup_file_mapped",
214 "current mapped file usage",
215 )?;
216 encode_gauge(
217 &mut encoder,
218 s.active_file,
219 "cgroup_active_file",
220 "current active file usage",
221 )?;
222 encode_gauge(
223 &mut encoder,
224 s.inactive_file,
225 "cgroup_inactive_file",
226 "current inactive file usage",
227 )?;
228 encode_gauge(&mut encoder, s.shmem, "cgroup_shmem", "current shmem usage")?;
229 encode_gauge(
230 &mut encoder,
231 s.kernel,
232 "cgroup_kernel",
233 "current kernel usage",
234 )?;
235 encode_gauge(
236 &mut encoder,
237 s.kernel_stack,
238 "cgroup_kernel_stack",
239 "current kernel stack usage",
240 )?;
241 encode_gauge(
242 &mut encoder,
243 s.pagetables,
244 "cgroup_pagetables",
245 "current pagetables usage",
246 )?;
247 encode_gauge(
248 &mut encoder,
249 s.percpu,
250 "cgroup_percpu",
251 "current percpu usage",
252 )?;
253 encode_gauge(
254 &mut encoder,
255 s.sock,
256 "cgroup_sock",
257 "current socket memory usage",
258 )?;
259 encode_gauge(&mut encoder, s.slab, "cgroup_slab", "current slab usage")?;
260 encode_gauge(
261 &mut encoder,
262 s.slab_reclaimable,
263 "cgroup_slab_reclaimable",
264 "current reclaimable slab usage",
265 )?;
266 encode_gauge(
267 &mut encoder,
268 s.slab_unreclaimable,
269 "cgroup_slab_unreclaimable",
270 "current unreclaimable slab usage",
271 )?;
272 encode_counter(
273 &mut encoder,
274 s.pgfault,
275 "cgroup_pgfault_total",
276 "cgroup page faults",
277 )?;
278 encode_counter(
279 &mut encoder,
280 s.pgmajfault,
281 "cgroup_pgmajfault_total",
282 "cgroup major page faults",
283 )?;
284 encode_counter(
285 &mut encoder,
286 s.workingset_refault_anon,
287 "cgroup_workingset_refault_anon_total",
288 "anonymous workingset refaults",
289 )?;
290 encode_counter(
291 &mut encoder,
292 s.workingset_refault_file,
293 "cgroup_workingset_refault_file_total",
294 "file workingset refaults",
295 )?;
296 encode_counter(
297 &mut encoder,
298 s.workingset_activate_anon,
299 "cgroup_workingset_activate_anon_total",
300 "anonymous workingset activations",
301 )?;
302 encode_counter(
303 &mut encoder,
304 s.workingset_activate_file,
305 "cgroup_workingset_activate_file_total",
306 "file workingset activations",
307 )?;
308 encode_counter(
309 &mut encoder,
310 s.workingset_restore_anon,
311 "cgroup_workingset_restore_anon_total",
312 "anonymous workingset restores",
313 )?;
314 encode_counter(
315 &mut encoder,
316 s.workingset_restore_file,
317 "cgroup_workingset_restore_file_total",
318 "file workingset restores",
319 )?;
320 Ok(())
321 }
322}
323
324#[cfg(test)]
325mod tests {
326 use super::MemoryStat;
327
328 #[test]
329 fn parse_memory_stat_includes_non_heap_signals() {
330 let input = "\
331anon 4096
332inactive_anon 1024
333active_anon 3072
334file 8192
335file_mapped 2048
336active_file 4096
337inactive_file 4096
338shmem 512
339kernel 1024
340kernel_stack 128
341pagetables 256
342percpu 64
343sock 32
344slab 768
345slab_reclaimable 512
346slab_unreclaimable 256
347pgfault 100
348pgmajfault 3
349workingset_refault_anon 7
350workingset_refault_file 11
351workingset_activate_anon 5
352workingset_activate_file 13
353workingset_restore_anon 2
354workingset_restore_file 17";
355
356 let stats = MemoryStat::parse(input).expect("memory.stat should parse");
357 assert_eq!(stats.anon, 4096);
358 assert_eq!(stats.file_mapped, 2048);
359 assert_eq!(stats.shmem, 512);
360 assert_eq!(stats.slab_reclaimable, 512);
361 assert_eq!(stats.slab_unreclaimable, 256);
362 assert_eq!(stats.pgfault, 100);
363 assert_eq!(stats.pgmajfault, 3);
364 assert_eq!(stats.workingset_refault_file, 11);
365 assert_eq!(stats.workingset_restore_file, 17);
366 }
367}