conmonrs/container_log/
cri.rs1use crate::{
4 container_io::Pipe,
5 container_log::{LogFile, lines},
6};
7use anyhow::{Context, Result};
8use std::{fmt::Write, path::Path};
9use time::{OffsetDateTime, UtcOffset, format_description::well_known::Rfc3339};
10
11#[derive(Debug)]
15pub(super) struct CriLogger {
16 file: LogFile,
18
19 timestamp: Timestamp,
21}
22
23impl CriLogger {
24 pub(super) fn new<T: AsRef<Path>>(path: T, max_log_size: Option<usize>) -> CriLogger {
26 Self {
27 file: LogFile::new(path, Some(0o640), max_log_size),
29 timestamp: Timestamp::default(),
30 }
31 }
32
33 #[cfg(test)]
36 pub(super) fn freeze_timestamp(&mut self, timestamp: &str) {
37 self.timestamp.formatted = timestamp.into();
38 self.timestamp.frozen = true;
39 }
40}
41
42#[derive(Debug, Default)]
47struct Timestamp {
48 second: Option<i64>,
50
51 date_time: String,
54
55 offset: String,
57
58 formatted: String,
60
61 #[cfg(test)]
62 frozen: bool,
63}
64
65impl Timestamp {
66 const DATE_TIME_LEN: usize = "2026-10-20T12:00:00".len();
68
69 fn update(&mut self) -> Result<&str> {
71 #[cfg(test)]
72 if self.frozen {
73 return Ok(&self.formatted);
74 }
75 self.update_at(OffsetDateTime::now_utc())
76 }
77
78 fn update_at(&mut self, now: OffsetDateTime) -> Result<&str> {
80 let second = now.unix_timestamp();
81 if self.second != Some(second) {
82 let offset = UtcOffset::local_offset_at(now).unwrap_or(UtcOffset::UTC);
83 let whole = now
86 .to_offset(offset)
87 .replace_nanosecond(0)
88 .context("strip nanoseconds")?
89 .format(&Rfc3339)
90 .context("format datetime")?;
91 let (date_time, offset) = whole
92 .split_at_checked(Self::DATE_TIME_LEN)
93 .context("split datetime")?;
94 self.date_time = date_time.into();
95 self.offset = offset.into();
96 self.second = Some(second);
97 }
98 self.formatted.clear();
99 write!(
100 self.formatted,
101 "{}.{:09}{}",
102 self.date_time,
103 now.nanosecond(),
104 self.offset
105 )
106 .context("format timestamp")?;
107 Ok(&self.formatted)
108 }
109}
110
111impl CriLogger {
112 pub(super) fn file(&mut self) -> &mut LogFile {
114 &mut self.file
115 }
116
117 pub(super) async fn write_chunk(&mut self, pipe: Pipe, bytes: &[u8]) -> Result<()> {
120 let timestamp = self.timestamp.update()?;
121
122 let pipe_tag: &[u8] = match pipe {
123 Pipe::StdOut => b" stdout ",
124 Pipe::StdErr => b" stderr ",
125 };
126
127 self.file
128 .write_entries(lines(bytes), |buf, (line, partial)| {
129 buf.extend_from_slice(timestamp.as_bytes());
130 buf.extend_from_slice(pipe_tag);
131 buf.extend_from_slice(if partial { b"P " } else { b"F " });
132 buf.extend_from_slice(line);
133 if partial {
134 buf.push(b'\n');
135 }
136 })
137 .await
138 }
139}
140
141#[cfg(test)]
142mod tests;