1use serde::{Deserialize, Deserializer, Serialize};
30use serde_json::Value;
31
32use crate::isolation::isolation_log::shell_quote;
33
34pub const CGROUP_SAMPLE_INTERVAL_SECONDS: u64 = 1;
36
37pub mod cgroup_shell_vars {
39 pub const SAMPLE: &str = "__start_command_cgroup";
41 pub const SAMPLER: &str = "__start_command_cgroup_sampler";
42 pub const FILE: &str = "__start_command_cgroup_file";
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum OomScope {
48 Unknown,
50 ContainerLimit,
52 HostOrParent,
54}
55
56impl OomScope {
57 pub fn as_str(&self) -> &'static str {
58 match self {
59 OomScope::Unknown => "unknown",
60 OomScope::ContainerLimit => "container-limit",
61 OomScope::HostOrParent => "host-or-parent",
62 }
63 }
64
65 pub fn note(&self) -> &'static str {
66 match self {
67 OomScope::Unknown => "OOM kill scope unknown",
68 OomScope::ContainerLimit => "the container hit its own memory limit",
69 OomScope::HostOrParent => "a host-wide or parent cgroup OOM killed processes here",
70 }
71 }
72}
73
74#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
76#[serde(rename_all = "camelCase")]
77pub struct CgroupMemory {
78 pub limit_bytes: Option<u64>,
79 pub peak_bytes: Option<u64>,
80 pub oom_events: Option<u64>,
81 pub oom_kills: Option<u64>,
82}
83
84pub fn build_cgroup_functions_snippet() -> String {
98 [
99 concat!(
100 "__start_command_cgroup_dir() { __scd_id=$(docker inspect -f '{{.Id}}' \"$1\" 2>/dev/null); ",
101 "[ -n \"$__scd_id\" ] || return 1; ",
102 "__scd_pid=$(docker inspect -f '{{.State.Pid}}' \"$1\" 2>/dev/null); __scd_rel=''; ",
103 "case \"$__scd_pid\" in ''|0|*[!0-9]*) ;; ",
104 "*) __scd_rel=$(sed -n 's/^0:://p' \"${START_COMMAND_PROC_ROOT:-/proc}/$__scd_pid/cgroup\" 2>/dev/null);; esac; ",
105 "__scd_root=${START_COMMAND_CGROUP_ROOT:-/sys/fs/cgroup}; ",
106 "for __scd_dir in ${__scd_rel:+\"$__scd_root$__scd_rel\"} ",
107 "\"$__scd_root/system.slice/docker-$__scd_id.scope\" \"$__scd_root/docker/$__scd_id\"; do ",
108 "case \"$__scd_dir\" in *\"$__scd_id\"*) if [ -r \"$__scd_dir/memory.events\" ]; then ",
109 "printf '%s' \"$__scd_dir\"; return 0; fi;; esac; done; return 1; }",
110 ),
111 concat!(
112 "__start_command_cgroup_read() { __scr_events=$(cat \"$1/memory.events\" 2>/dev/null) || return 1; ",
113 "__scr_oom=$(printf '%s\\n' \"$__scr_events\" | sed -n 's/^oom //p'); ",
114 "__scr_kill=$(printf '%s\\n' \"$__scr_events\" | sed -n 's/^oom_kill //p'); ",
115 "__scr_max=$(cat \"$1/memory.max\" 2>/dev/null); __scr_peak=$(cat \"$1/memory.peak\" 2>/dev/null); ",
116 "printf '%s %s %s %s %s\\n' \"${__scr_max:--}\" \"${__scr_peak:--}\" ",
117 "\"${__scr_oom:--}\" \"${__scr_kill:--}\" \"$1\"; }",
118 ),
119 ]
120 .join("; ")
121}
122
123pub fn build_cgroup_sampler_start_snippet(container_name: &str) -> String {
127 use cgroup_shell_vars::{FILE, SAMPLER};
128 [
129 build_cgroup_functions_snippet(),
130 format!("{}=\"${{TMPDIR:-/tmp}}/start-command-cgroup.$$\"", FILE),
131 format!("rm -f \"${f}\" \"${f}.tmp\"", f = FILE),
132 format!(
133 "( __scs_dir=$(__start_command_cgroup_dir {name}) || exit 0; \
134 while __scs_line=$(__start_command_cgroup_read \"$__scs_dir\"); do \
135 printf '%s\\n' \"$__scs_line\" > \"${f}.tmp\" && mv -f \"${f}.tmp\" \"${f}\"; \
136 sleep {interval}; done ) >/dev/null 2>&1 & {sampler}=$!",
137 name = shell_quote(container_name),
138 f = FILE,
139 interval = CGROUP_SAMPLE_INTERVAL_SECONDS,
140 sampler = SAMPLER,
141 ),
142 ]
143 .join("; ")
144}
145
146pub fn build_cgroup_sampler_stop_snippet() -> String {
151 use cgroup_shell_vars::{FILE, SAMPLE, SAMPLER};
152 [
153 format!("kill \"${}\" 2>/dev/null", SAMPLER),
154 format!("wait \"${}\" 2>/dev/null", SAMPLER),
155 format!("{}=$(cat \"${}\" 2>/dev/null)", SAMPLE, FILE),
156 format!("__scs_dir=$(printf '%s' \"${}\" | cut -s -d ' ' -f 5-)", SAMPLE),
157 format!(
158 "if [ -n \"$__scs_dir\" ] && __scs_line=$(__start_command_cgroup_read \"$__scs_dir\"); then {}=$__scs_line; fi",
159 SAMPLE
160 ),
161 format!("rm -f \"${f}\" \"${f}.tmp\"", f = FILE),
162 format!(
163 "{s}=$(printf '%s' \"${s}\" | cut -d ' ' -f 1-4)",
164 s = SAMPLE
165 ),
166 ]
167 .join("; ")
168}
169
170pub fn build_cgroup_memory_log_snippet(quoted_log_path: &str) -> String {
173 let sample = cgroup_shell_vars::SAMPLE;
174 format!(
176 "__start_command_cgroup_log() {{ __scm_note=''; \
177 if [ \"$4\" -gt 0 ] 2>/dev/null; then __scm_note=' ({unknown})'; fi; \
178 printf 'Memory: memory.max=%s memory.peak=%s oom=%s oom_kill=%s%s\\n' \
179 \"$1\" \"$2\" \"$3\" \"$4\" \"$__scm_note\"; }}; \
180 if [ -n \"${sample}\" ]; then __start_command_cgroup_log ${sample} >> {log}; fi",
181 unknown = OomScope::Unknown.note(),
182 sample = sample,
183 log = quoted_log_path,
184 )
185}
186
187fn parse_counter(text: &str) -> Option<u64> {
188 if !text.is_empty() && text.chars().all(|c| c.is_ascii_digit()) {
189 text.parse().ok()
190 } else {
191 None
192 }
193}
194
195pub fn parse_cgroup_memory_sample(text: &str) -> Option<CgroupMemory> {
198 let fields: Vec<&str> = text.split_whitespace().collect();
199 if fields.len() < 4 {
200 return None;
201 }
202 let counters = CgroupMemory {
203 limit_bytes: parse_counter(fields[0]),
204 peak_bytes: parse_counter(fields[1]),
205 oom_events: parse_counter(fields[2]),
206 oom_kills: parse_counter(fields[3]),
207 };
208 if counters.oom_events.is_none() && counters.oom_kills.is_none() {
209 return None;
211 }
212 Some(counters)
213}
214
215pub fn format_cgroup_memory_log_line(text: &str) -> Option<String> {
218 let counters = parse_cgroup_memory_sample(text)?;
219 let fields: Vec<&str> = text.split_whitespace().collect();
220 let note = describe_cgroup_oom_scope(&counters)
221 .map(|scope| format!(" ({})", scope.note()))
222 .unwrap_or_default();
223 Some(format!(
224 "Memory: memory.max={} memory.peak={} oom={} oom_kill={}{}",
225 fields[0], fields[1], fields[2], fields[3], note
226 ))
227}
228
229pub fn describe_cgroup_oom_scope(memory: &CgroupMemory) -> Option<OomScope> {
234 memory.oom_kills.filter(|kills| *kills > 0)?;
235 Some(OomScope::Unknown)
236}
237
238fn format_bytes(bytes: Option<u64>) -> String {
239 let Some(bytes) = bytes else {
240 return "unknown".to_string();
241 };
242 const UNITS: [&str; 5] = ["B", "KiB", "MiB", "GiB", "TiB"];
243 let mut value = bytes as f64;
244 let mut unit = 0;
245 while value >= 1024.0 && unit < UNITS.len() - 1 {
246 value /= 1024.0;
247 unit += 1;
248 }
249 if unit == 0 {
250 format!("{} B", bytes)
251 } else {
252 format!("{:.1} {}", value, UNITS[unit])
253 }
254}
255
256pub fn format_cgroup_memory(memory: &CgroupMemory) -> String {
259 let limit = match memory.limit_bytes {
260 Some(_) => format!("{} limit", format_bytes(memory.limit_bytes)),
261 None => "no limit".to_string(),
262 };
263 let count = |value: Option<u64>| value.map_or_else(|| "unknown".to_string(), |v| v.to_string());
264 let note = describe_cgroup_oom_scope(memory)
265 .map(|scope| format!(" ({})", scope.note()))
266 .unwrap_or_default();
267 format!(
268 "peak {} of {}, oom {}, oom_kill {}{}",
269 format_bytes(memory.peak_bytes),
270 limit,
271 count(memory.oom_events),
272 count(memory.oom_kills),
273 note
274 )
275}
276
277fn counter_from_value(value: Option<&Value>) -> Option<u64> {
278 match value? {
279 Value::Number(number) => number.as_u64().or_else(|| {
280 number
281 .as_f64()
282 .filter(|v| v.fract() == 0.0 && *v >= 0.0 && *v <= 9_007_199_254_740_991.0)
283 .map(|v| v as u64)
284 }),
285 Value::String(text) => parse_counter(text.trim()),
286 _ => None,
287 }
288}
289
290pub fn normalize_cgroup_memory(value: &Value) -> Option<CgroupMemory> {
293 let object = value.as_object()?;
294 Some(CgroupMemory {
295 limit_bytes: counter_from_value(object.get("limitBytes")),
296 peak_bytes: counter_from_value(object.get("peakBytes")),
297 oom_events: counter_from_value(object.get("oomEvents")),
298 oom_kills: counter_from_value(object.get("oomKills")),
299 })
300}
301
302pub fn deserialize_cgroup_memory<'de, D>(deserializer: D) -> Result<Option<CgroupMemory>, D::Error>
304where
305 D: Deserializer<'de>,
306{
307 let value = Option::<Value>::deserialize(deserializer)?;
308 Ok(value.as_ref().and_then(normalize_cgroup_memory))
309}
310
311#[cfg(test)]
312mod tests {
313 use super::*;
314
315 #[test]
316 fn parses_counters_and_treats_max_as_no_limit() {
317 assert_eq!(
318 parse_cgroup_memory_sample("max 1024 0 2"),
319 Some(CgroupMemory {
320 limit_bytes: None,
321 peak_bytes: Some(1024),
322 oom_events: Some(0),
323 oom_kills: Some(2),
324 })
325 );
326 assert_eq!(parse_cgroup_memory_sample("max - - -"), None);
327 assert_eq!(parse_cgroup_memory_sample("1 2 3"), None);
328 }
329
330 #[test]
331 fn normalizes_floats_strings_and_nulls() {
332 let value = serde_json::json!({
333 "limitBytes": 268435456.0,
334 "peakBytes": "1024",
335 "oomEvents": null,
336 "oomKills": -1
337 });
338 assert_eq!(
339 normalize_cgroup_memory(&value),
340 Some(CgroupMemory {
341 limit_bytes: Some(268_435_456),
342 peak_bytes: Some(1024),
343 oom_events: None,
344 oom_kills: None,
345 })
346 );
347 assert_eq!(normalize_cgroup_memory(&Value::Null), None);
348 }
349}