1use chrono::{DateTime, Utc};
24
25use crate::exit_reason::{describe_exit_code, SIGNAL_NAMES, UNKNOWN_EXIT_CODE};
26use crate::isolation::isolation_log::shell_quote;
27
28pub const DOCKER_ZERO_TIME: &str = "0001-01-01T00:00:00Z";
33
34pub const UNKNOWN: &str = UNKNOWN_EXIT_CODE;
36
37pub const NO_ERROR: &str = "(none)";
39
40pub const POST_MORTEM_HEADER: &str = "=== Container post-mortem ===";
42
43pub const DOCKER_STATE_INSPECT_FORMAT: &str =
45 "{{.State.ExitCode}} {{.State.OOMKilled}} {{.State.StartedAt}} {{.State.FinishedAt}}";
46
47pub mod shell_vars {
49 pub const EXIT: &str = "__start_command_exit";
50 pub const OOM: &str = "__start_command_oom";
51 pub const STARTED: &str = "__start_command_started";
52 pub const FINISHED: &str = "__start_command_finished";
53 pub const ERROR: &str = "__start_command_error";
54 pub const ERROR_TEXT: &str = "__start_command_error_text";
55 pub const SIGNAL: &str = "__start_command_signal";
56 pub const EXIT_TEXT: &str = "__start_command_exit_text";
57 pub const LIFETIME: &str = "__start_command_lifetime";
58}
59
60#[derive(Debug, Clone, Default, PartialEq, Eq)]
62pub struct ContainerPostMortem {
63 pub container_name: String,
64 pub exit_code: Option<i32>,
65 pub oom_killed: Option<bool>,
66 pub started_at: Option<String>,
67 pub finished_at: Option<String>,
68 pub error: Option<String>,
69}
70
71pub fn normalize_docker_timestamp(value: Option<&str>) -> Option<String> {
77 let text = value.unwrap_or("").trim();
78 if text.is_empty()
79 || text == DOCKER_ZERO_TIME
80 || text == UNKNOWN
81 || text == "<no value>"
82 || text == NO_ERROR
83 {
84 return None;
85 }
86 parse_timestamp(text)?;
87 Some(text.to_string())
88}
89
90fn parse_timestamp(text: &str) -> Option<DateTime<Utc>> {
91 DateTime::parse_from_rfc3339(text)
92 .map(|value| value.with_timezone(&Utc))
93 .ok()
94}
95
96pub fn format_lifetime(started_at: Option<&str>, finished_at: Option<&str>) -> Option<String> {
98 let start = parse_timestamp(&normalize_docker_timestamp(started_at)?)?;
99 let end = parse_timestamp(&normalize_docker_timestamp(finished_at)?)?;
100 let ms = end.signed_duration_since(start).num_milliseconds();
101 if ms < 0 {
102 return None;
103 }
104 Some(format!("{}.{:03}s", ms / 1000, ms % 1000))
105}
106
107fn optional_bool_text(value: Option<bool>) -> String {
108 match value {
109 Some(value) => value.to_string(),
110 None => UNKNOWN.to_string(),
111 }
112}
113
114pub fn format_container_post_mortem(facts: &ContainerPostMortem) -> String {
116 let started = normalize_docker_timestamp(facts.started_at.as_deref());
117 let finished = normalize_docker_timestamp(facts.finished_at.as_deref());
118 let lifetime = format_lifetime(facts.started_at.as_deref(), facts.finished_at.as_deref());
119 let error = facts.error.as_deref().unwrap_or("").trim().to_string();
120 let container = if facts.container_name.is_empty() {
121 UNKNOWN
122 } else {
123 &facts.container_name
124 };
125 format!(
126 "{}\nContainer: {}\nExit Code: {}\nOOMKilled: {}\nStartedAt: {}\nFinishedAt: {}\nLifetime: {}\nError: {}\n",
127 POST_MORTEM_HEADER,
128 container,
129 describe_exit_code(facts.exit_code).text,
130 optional_bool_text(facts.oom_killed),
131 started.as_deref().unwrap_or(UNKNOWN),
132 finished.as_deref().unwrap_or(UNKNOWN),
133 lifetime.as_deref().unwrap_or(UNKNOWN),
134 if error.is_empty() { NO_ERROR } else { &error },
135 )
136}
137
138pub fn format_container_removal_note(facts: &ContainerPostMortem) -> String {
141 let described = describe_exit_code(facts.exit_code);
142 let signal = described
143 .signal
144 .as_deref()
145 .map(|name| format!(", {}", name))
146 .unwrap_or_default();
147 let lifetime = format_lifetime(facts.started_at.as_deref(), facts.finished_at.as_deref())
148 .unwrap_or_else(|| UNKNOWN.to_string());
149 let container = if facts.container_name.is_empty() {
150 UNKNOWN
151 } else {
152 &facts.container_name
153 };
154 format!(
155 "Container removed: {} (exit {}{}, lifetime {}, oomKilled={})\n",
156 container,
157 described
158 .code
159 .map(|code| code.to_string())
160 .unwrap_or_else(|| UNKNOWN.to_string()),
161 signal,
162 lifetime,
163 optional_bool_text(facts.oom_killed),
164 )
165}
166
167pub fn build_docker_state_snippet(container_name: &str) -> String {
174 let quoted_name = shell_quote(container_name);
175 [
176 format!(
177 "__start_command_state=$(docker inspect -f '{}' {} 2>/dev/null || printf '%s' '-1 false {} {}')",
178 DOCKER_STATE_INSPECT_FORMAT, quoted_name, UNKNOWN, UNKNOWN
179 ),
180 "set -- $__start_command_state".to_string(),
181 format!("{}=${{1:--1}}", shell_vars::EXIT),
182 format!("{}=${{2:-false}}", shell_vars::OOM),
183 format!("{}=${{3:-{}}}", shell_vars::STARTED, UNKNOWN),
184 format!("{}=${{4:-{}}}", shell_vars::FINISHED, UNKNOWN),
185 format!(
186 "{}=$(docker inspect -f '{{{{.State.Error}}}}' {} 2>/dev/null)",
187 shell_vars::ERROR,
188 quoted_name
189 ),
190 format!(
191 "case \"${}\" in {}|'') {}={};; esac",
192 shell_vars::STARTED,
193 DOCKER_ZERO_TIME,
194 shell_vars::STARTED,
195 UNKNOWN
196 ),
197 format!(
198 "case \"${}\" in {}|'') {}={};; esac",
199 shell_vars::FINISHED,
200 DOCKER_ZERO_TIME,
201 shell_vars::FINISHED,
202 UNKNOWN
203 ),
204 build_signal_decode_snippet(),
205 build_lifetime_snippet(),
206 ]
207 .join("; ")
208}
209
210pub fn build_signal_decode_snippet() -> String {
216 let arms: Vec<String> = SIGNAL_NAMES
217 .iter()
218 .map(|(signal, name)| format!("{}) {}={};;", 128 + signal, shell_vars::SIGNAL, name))
219 .collect();
220 [
221 format!("{}=''", shell_vars::SIGNAL),
222 format!("case \"${}\" in {} esac", shell_vars::EXIT, arms.join(" ")),
223 format!("{}=\"${}\"", shell_vars::EXIT_TEXT, shell_vars::EXIT),
224 format!(
225 "if [ -n \"${}\" ]; then {}=\"${} (${} - 128+$(({} - 128)))\"; fi",
226 shell_vars::SIGNAL,
227 shell_vars::EXIT_TEXT,
228 shell_vars::EXIT,
229 shell_vars::SIGNAL,
230 shell_vars::EXIT
231 ),
232 ]
233 .join("; ")
234}
235
236pub fn build_lifetime_snippet() -> String {
248 [
249 concat!(
250 "__start_command_millis() { ",
251 "case \"$1\" in *.*) __start_command_frac=${1#*.};; ",
252 "*) printf '000'; return;; esac; ",
253 "__start_command_frac=${__start_command_frac%%[!0-9]*}; ",
254 "printf '%.3s' \"${__start_command_frac}000\"; }"
255 )
256 .to_string(),
257 concat!(
258 "__start_command_epoch_ms() { ",
259 "__start_command_ts=$(date -u -d \"$1\" +%s%3N 2>/dev/null); ",
260 "case \"$__start_command_ts\" in ''|*[!0-9]*) ",
261 "__start_command_ts=$(date -u -j -f '%Y-%m-%dT%H:%M:%S' \"${1%.*}\" +%s 2>/dev/null) && ",
262 "__start_command_ts=\"${__start_command_ts}$(__start_command_millis \"$1\")\";; esac; ",
263 "case \"$__start_command_ts\" in ''|*[!0-9]*) __start_command_ts='';; esac; ",
264 "printf '%s' \"$__start_command_ts\"; }"
265 )
266 .to_string(),
267 format!("{}={}", shell_vars::LIFETIME, UNKNOWN),
268 format!(
269 "__start_command_t0=$(__start_command_epoch_ms \"${}\")",
270 shell_vars::STARTED
271 ),
272 format!(
273 "__start_command_t1=$(__start_command_epoch_ms \"${}\")",
274 shell_vars::FINISHED
275 ),
276 format!(
277 concat!(
278 "if [ -n \"$__start_command_t0\" ] && [ -n \"$__start_command_t1\" ] && ",
279 "[ \"$__start_command_t1\" -ge \"$__start_command_t0\" ] 2>/dev/null; then ",
280 "__start_command_ms=$((__start_command_t1 - __start_command_t0)); ",
281 "{}=\"$((__start_command_ms / 1000)).$(printf '%03d' \"$((__start_command_ms % 1000))\")s\"; fi"
282 ),
283 shell_vars::LIFETIME
284 ),
285 ]
286 .join("; ")
287}
288
289pub fn build_docker_post_mortem_snippet(container_name: &str, quoted_log_path: &str) -> String {
291 format!(
292 concat!(
293 "{error_text}=\"${error}\"; ",
294 "if [ -z \"${error_text}\" ]; then {error_text}='{none}'; fi; ",
295 "printf '\\n{header}\\nContainer: %s\\nExit Code: %s\\n",
296 "OOMKilled: %s\\nStartedAt: %s\\nFinishedAt: %s\\nLifetime: %s\\nError: %s\\n' ",
297 "{name} \"${exit_text}\" \"${oom}\" \"${started}\" \"${finished}\" ",
298 "\"${lifetime}\" \"${error_text}\" >> {log}"
299 ),
300 error_text = shell_vars::ERROR_TEXT,
301 error = shell_vars::ERROR,
302 none = NO_ERROR,
303 header = POST_MORTEM_HEADER,
304 name = shell_quote(container_name),
305 exit_text = shell_vars::EXIT_TEXT,
306 oom = shell_vars::OOM,
307 started = shell_vars::STARTED,
308 finished = shell_vars::FINISHED,
309 lifetime = shell_vars::LIFETIME,
310 log = quoted_log_path,
311 )
312}
313
314pub fn build_docker_removal_note_snippet(container_name: &str, quoted_log_path: &str) -> String {
316 format!(
317 concat!(
318 "__start_command_signal_note=''; ",
319 "if [ -n \"${signal}\" ]; then __start_command_signal_note=\", ${signal}\"; fi; ",
320 "printf '\\nContainer removed: %s (exit %s%s, lifetime %s, oomKilled=%s)\\n' ",
321 "{name} \"${exit}\" \"$__start_command_signal_note\" \"${lifetime}\" \"${oom}\" >> {log}"
322 ),
323 signal = shell_vars::SIGNAL,
324 name = shell_quote(container_name),
325 exit = shell_vars::EXIT,
326 lifetime = shell_vars::LIFETIME,
327 oom = shell_vars::OOM,
328 log = quoted_log_path,
329 )
330}