hyperi_rustlib/metrics/
container.rs1use std::fs;
12use std::path::Path;
13
14#[derive(Debug, Clone)]
16pub struct ContainerMetrics {
17 namespace: String,
18 cgroup_version: CgroupVersion,
19}
20
21#[derive(Debug, Clone, Copy)]
22enum CgroupVersion {
23 V1,
24 V2,
25 Unknown,
26}
27
28impl ContainerMetrics {
29 #[must_use]
31 pub fn new(namespace: &str) -> Self {
32 let cgroup_version = detect_cgroup_version();
33
34 let this = Self {
35 namespace: namespace.to_string(),
36 cgroup_version,
37 };
38
39 this.register_metrics();
40 this
41 }
42
43 fn register_metrics(&self) {
45 let ns = &self.namespace;
46
47 metrics::describe_gauge!(
48 format!("{ns}_container_memory_limit_bytes"),
49 "Container memory limit in bytes".to_string()
50 );
51 metrics::describe_gauge!(
52 format!("{ns}_container_memory_usage_bytes"),
53 "Container memory usage in bytes".to_string()
54 );
55 metrics::describe_gauge!(
56 format!("{ns}_container_cpu_limit_cores"),
57 "Container CPU limit in cores".to_string()
58 );
59 }
60
61 pub fn update(&self) {
63 let ns = &self.namespace;
64
65 if let Some(limit) = self.read_memory_limit() {
67 metrics::gauge!(format!("{ns}_container_memory_limit_bytes")).set(limit as f64);
68 }
69
70 if let Some(usage) = self.read_memory_usage() {
72 metrics::gauge!(format!("{ns}_container_memory_usage_bytes")).set(usage as f64);
73 }
74
75 if let Some(cores) = self.read_cpu_limit() {
77 metrics::gauge!(format!("{ns}_container_cpu_limit_cores")).set(cores);
78 }
79 }
80
81 fn read_memory_limit(&self) -> Option<u64> {
83 match self.cgroup_version {
84 CgroupVersion::V2 => {
85 read_cgroup_value("/sys/fs/cgroup/memory.max")
87 }
88 CgroupVersion::V1 => {
89 read_cgroup_value("/sys/fs/cgroup/memory/memory.limit_in_bytes")
91 }
92 CgroupVersion::Unknown => None,
93 }
94 }
95
96 fn read_memory_usage(&self) -> Option<u64> {
98 match self.cgroup_version {
99 CgroupVersion::V2 => {
100 read_cgroup_value("/sys/fs/cgroup/memory.current")
102 }
103 CgroupVersion::V1 => {
104 read_cgroup_value("/sys/fs/cgroup/memory/memory.usage_in_bytes")
106 }
107 CgroupVersion::Unknown => None,
108 }
109 }
110
111 fn read_cpu_limit(&self) -> Option<f64> {
113 match self.cgroup_version {
114 CgroupVersion::V2 => {
115 let content = fs::read_to_string("/sys/fs/cgroup/cpu.max").ok()?;
117 parse_cpu_max_v2(&content)
118 }
119 CgroupVersion::V1 => {
120 let quota = read_cgroup_value("/sys/fs/cgroup/cpu/cpu.cfs_quota_us")?;
122 let period = read_cgroup_value("/sys/fs/cgroup/cpu/cpu.cfs_period_us")?;
123
124 if quota == u64::MAX || period == 0 {
125 None
126 } else {
127 Some(quota as f64 / period as f64)
128 }
129 }
130 CgroupVersion::Unknown => None,
131 }
132 }
133}
134
135#[cfg(all(feature = "scaling", feature = "expression"))]
140pub(crate) fn cpu_limit_cores() -> Option<f64> {
141 match detect_cgroup_version() {
142 CgroupVersion::V2 => {
143 let content = fs::read_to_string("/sys/fs/cgroup/cpu.max").ok()?;
144 parse_cpu_max_v2(&content)
145 }
146 CgroupVersion::V1 => {
147 let quota = read_cgroup_value("/sys/fs/cgroup/cpu/cpu.cfs_quota_us")?;
148 let period = read_cgroup_value("/sys/fs/cgroup/cpu/cpu.cfs_period_us")?;
149 if quota == u64::MAX || period == 0 {
150 None
151 } else {
152 Some(quota as f64 / period as f64)
153 }
154 }
155 CgroupVersion::Unknown => None,
156 }
157}
158
159fn detect_cgroup_version() -> CgroupVersion {
161 if Path::new("/sys/fs/cgroup/cgroup.controllers").exists() {
163 return CgroupVersion::V2;
164 }
165
166 if Path::new("/sys/fs/cgroup/memory/memory.limit_in_bytes").exists() {
168 return CgroupVersion::V1;
169 }
170
171 CgroupVersion::Unknown
172}
173
174fn read_cgroup_value(path: &str) -> Option<u64> {
176 let content = fs::read_to_string(path).ok()?;
177 let trimmed = content.trim();
178
179 if trimmed == "max" {
181 return Some(u64::MAX);
182 }
183
184 trimmed.parse().ok()
185}
186
187fn parse_cpu_max_v2(content: &str) -> Option<f64> {
189 let parts: Vec<&str> = content.split_whitespace().collect();
190 if parts.len() != 2 {
191 return None;
192 }
193
194 let quota = parts[0];
195 let period: u64 = parts[1].parse().ok()?;
196
197 if quota == "max" || period == 0 {
198 return None;
199 }
200
201 let quota_us: u64 = quota.parse().ok()?;
202 Some(quota_us as f64 / period as f64)
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208
209 #[test]
210 fn test_container_metrics_new() {
211 let cm = ContainerMetrics::new("test");
212 assert_eq!(cm.namespace, "test");
213 }
214
215 #[test]
216 fn test_parse_cpu_max_v2() {
217 assert_eq!(parse_cpu_max_v2("100000 100000"), Some(1.0));
218 assert_eq!(parse_cpu_max_v2("50000 100000"), Some(0.5));
219 assert_eq!(parse_cpu_max_v2("max 100000"), None);
220 assert_eq!(parse_cpu_max_v2("invalid"), None);
221 }
222
223 #[test]
224 fn test_container_metrics_update() {
225 let cm = ContainerMetrics::new("test");
226 cm.update();
228 }
229}