1#![allow(dead_code)]
16
17use std::fs::read_to_string;
18use std::path::Path;
19
20#[cfg(target_os = "linux")]
21use nix::sys::{statfs, statfs::statfs};
22use prometheus::core::{Collector, Desc};
23use prometheus::proto::MetricFamily;
24use prometheus::{IntGauge, Opts};
25
26const CGROUP_UNIFIED_MOUNTPOINT: &str = "/sys/fs/cgroup";
27
28const MEMORY_MAX_FILE_CGROUP_V2: &str = "memory.max";
29const MEMORY_MAX_FILE_CGROUP_V1: &str = "memory.limit_in_bytes";
30const MEMORY_USAGE_FILE_CGROUP_V2: &str = "memory.current";
31const CPU_MAX_FILE_CGROUP_V2: &str = "cpu.max";
32const CPU_QUOTA_FILE_CGROUP_V1: &str = "cpu.cfs_quota_us";
33const CPU_PERIOD_FILE_CGROUP_V1: &str = "cpu.cfs_period_us";
34const CPU_USAGE_FILE_CGROUP_V2: &str = "cpu.stat";
35
36const MAX_VALUE_CGROUP_V2: &str = "max";
38
39const MAX_MEMORY_IN_BYTES: i64 = 1125899906842624; pub fn get_memory_limit_from_cgroups() -> Option<i64> {
48 #[cfg(target_os = "linux")]
49 {
50 let memory_max_file = if is_cgroup_v2()? {
51 MEMORY_MAX_FILE_CGROUP_V2
53 } else {
54 MEMORY_MAX_FILE_CGROUP_V1
56 };
57
58 let memory_limit =
60 read_value_from_file(Path::new(CGROUP_UNIFIED_MOUNTPOINT).join(memory_max_file))?;
61
62 if memory_limit > MAX_MEMORY_IN_BYTES {
64 return None;
65 }
66 Some(memory_limit)
67 }
68
69 #[cfg(not(target_os = "linux"))]
70 None
71}
72
73pub fn get_memory_usage_from_cgroups() -> Option<i64> {
77 #[cfg(target_os = "linux")]
78 {
79 if is_cgroup_v2()? {
80 let usage = read_value_from_file(
81 Path::new(CGROUP_UNIFIED_MOUNTPOINT).join(MEMORY_USAGE_FILE_CGROUP_V2),
82 )?;
83 Some(usage)
84 } else {
85 None
86 }
87 }
88
89 #[cfg(not(target_os = "linux"))]
90 None
91}
92
93pub fn get_cpu_limit_from_cgroups() -> Option<i64> {
98 #[cfg(target_os = "linux")]
99 if is_cgroup_v2()? {
100 get_cgroup_v2_cpu_limit(Path::new(CGROUP_UNIFIED_MOUNTPOINT).join(CPU_MAX_FILE_CGROUP_V2))
102 } else {
103 let quota = read_value_from_file(
105 Path::new(CGROUP_UNIFIED_MOUNTPOINT).join(CPU_QUOTA_FILE_CGROUP_V1),
106 )?;
107
108 let period = read_value_from_file(
109 Path::new(CGROUP_UNIFIED_MOUNTPOINT).join(CPU_PERIOD_FILE_CGROUP_V1),
110 )?;
111
112 Some(quota * 1000 / period)
114 }
115
116 #[cfg(not(target_os = "linux"))]
117 None
118}
119
120pub fn get_cpu_usage_from_cgroups() -> Option<i64> {
124 if !Path::new(CGROUP_UNIFIED_MOUNTPOINT)
128 .join(MEMORY_USAGE_FILE_CGROUP_V2)
129 .exists()
130 {
131 return None;
132 }
133
134 let content =
136 read_to_string(Path::new(CGROUP_UNIFIED_MOUNTPOINT).join(CPU_USAGE_FILE_CGROUP_V2)).ok()?;
137
138 let first_line = content.lines().next()?;
140 let fields = first_line.split(' ').collect::<Vec<&str>>();
141 if fields.len() != 2 {
142 return None;
143 }
144
145 fields[1].trim().parse::<i64>().ok()
146}
147
148pub(crate) fn calculate_cpu_usage(
152 current_cpu_usage_usecs: i64,
153 last_cpu_usage_usecs: i64,
154 interval_milliseconds: i64,
155) -> i64 {
156 let diff = current_cpu_usage_usecs - last_cpu_usage_usecs;
157 if diff > 0 && interval_milliseconds > 0 {
158 ((diff as f64 / interval_milliseconds as f64).round() as i64).max(1)
159 } else {
160 0
161 }
162}
163
164fn is_cgroup_v2() -> Option<bool> {
168 #[cfg(target_os = "linux")]
169 {
170 let path = Path::new(CGROUP_UNIFIED_MOUNTPOINT);
171 let fs_stat = statfs(path).ok()?;
172 Some(fs_stat.filesystem_type() == statfs::CGROUP2_SUPER_MAGIC)
173 }
174
175 #[cfg(not(target_os = "linux"))]
176 None
177}
178
179fn read_value_from_file<P: AsRef<Path>>(path: P) -> Option<i64> {
180 let content = read_to_string(&path).ok()?;
181
182 if content.starts_with(MAX_VALUE_CGROUP_V2) {
184 return None;
185 }
186
187 content.trim().parse::<i64>().ok()
188}
189
190fn get_cgroup_v2_cpu_limit<P: AsRef<Path>>(path: P) -> Option<i64> {
191 let content = read_to_string(&path).ok()?;
192
193 let fields = content.trim().split(' ').collect::<Vec<&str>>();
194 if fields.len() != 2 {
195 return None;
196 }
197
198 let quota = fields[0].trim();
200 if quota == MAX_VALUE_CGROUP_V2 {
201 return None;
202 }
203
204 let quota = quota.parse::<i64>().ok()?;
205
206 let period = fields[1].trim();
207 let period = period.parse::<i64>().ok()?;
208
209 Some(quota * 1000 / period)
211}
212
213#[derive(Debug)]
215pub struct CgroupsMetricsCollector {
216 descs: Vec<Desc>,
217 memory_usage: IntGauge,
218 cpu_usage: IntGauge,
219}
220
221impl Default for CgroupsMetricsCollector {
222 fn default() -> Self {
223 let mut descs = vec![];
224 let cpu_usage = IntGauge::with_opts(Opts::new(
225 "greptime_cgroups_cpu_usage_microseconds",
226 "the current cpu usage in microseconds that collected from cgroups filesystem",
227 ))
228 .unwrap();
229 descs.extend(cpu_usage.desc().into_iter().cloned());
230
231 let memory_usage = IntGauge::with_opts(Opts::new(
232 "greptime_cgroups_memory_usage_bytes",
233 "the current memory usage that collected from cgroups filesystem",
234 ))
235 .unwrap();
236 descs.extend(memory_usage.desc().into_iter().cloned());
237
238 Self {
239 descs,
240 memory_usage,
241 cpu_usage,
242 }
243 }
244}
245
246impl Collector for CgroupsMetricsCollector {
247 fn desc(&self) -> Vec<&Desc> {
248 self.descs.iter().collect()
249 }
250
251 fn collect(&self) -> Vec<MetricFamily> {
252 if let Some(cpu_usage) = get_cpu_usage_from_cgroups() {
253 self.cpu_usage.set(cpu_usage);
254 }
255
256 if let Some(memory_usage) = get_memory_usage_from_cgroups() {
257 self.memory_usage.set(memory_usage);
258 }
259
260 let mut mfs = Vec::with_capacity(self.descs.len());
261 mfs.extend(self.cpu_usage.collect());
262 mfs.extend(self.memory_usage.collect());
263 mfs
264 }
265}
266
267#[cfg(test)]
268mod tests {
269 use super::*;
270
271 #[test]
272 fn test_read_value_from_file() {
273 assert_eq!(
274 read_value_from_file(Path::new("testdata").join("memory.max")).unwrap(),
275 100000
276 );
277 assert_eq!(
278 read_value_from_file(Path::new("testdata").join("memory.max.unlimited")),
279 None
280 );
281 assert_eq!(read_value_from_file(Path::new("non_existent_file")), None);
282 }
283
284 #[test]
285 fn test_get_cgroup_v2_cpu_limit() {
286 assert_eq!(
287 get_cgroup_v2_cpu_limit(Path::new("testdata").join("cpu.max")).unwrap(),
288 1500
289 );
290 assert_eq!(
291 get_cgroup_v2_cpu_limit(Path::new("testdata").join("cpu.max.unlimited")),
292 None
293 );
294 assert_eq!(
295 get_cgroup_v2_cpu_limit(Path::new("non_existent_file")),
296 None
297 );
298 }
299}