Skip to main content

conmonrs/container_log/
cri.rs

1//! The logger of the CRI log format.
2
3use 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/// Writes the container output in the CRI log format, one entry per line
12/// with its timestamp, pipe and partial marker, to a file which gets rotated
13/// at its maximum size.
14#[derive(Debug)]
15pub(super) struct CriLogger {
16    /// The log file on disk.
17    file: LogFile,
18
19    /// The timestamp of the entries of the current chunk.
20    timestamp: Timestamp,
21}
22
23impl CriLogger {
24    /// Create a new file logger instance.
25    pub(super) fn new<T: AsRef<Path>>(path: T, max_log_size: Option<usize>) -> CriLogger {
26        Self {
27            // Created readable by the group as well, like by conmon.
28            file: LogFile::new(path, Some(0o640), max_log_size),
29            timestamp: Timestamp::default(),
30        }
31    }
32
33    /// Use a fixed timestamp for every entry, so that tests can compare the
34    /// written bytes.
35    #[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/// The RFC 3339 timestamp of the time a chunk got read, with nanoseconds like
43/// the ones of conmon. The local date, time of day and UTC offset only change
44/// with the second, they get formatted once per second, the fraction for
45/// every chunk.
46#[derive(Debug, Default)]
47struct Timestamp {
48    /// The second since the epoch the cached parts belong to.
49    second: Option<i64>,
50
51    /// The local date and time of day of that second, like
52    /// `2026-10-20T12:00:00`.
53    date_time: String,
54
55    /// The UTC offset of that second, like `+02:00` or `Z`.
56    offset: String,
57
58    /// The whole timestamp of the current chunk.
59    formatted: String,
60
61    #[cfg(test)]
62    frozen: bool,
63}
64
65impl Timestamp {
66    /// The length of the date and time of day of an RFC 3339 timestamp.
67    const DATE_TIME_LEN: usize = "2026-10-20T12:00:00".len();
68
69    /// Take the current time and return its timestamp.
70    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    /// Return the timestamp of `now`.
79    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            // Without a fraction, the date and time of day are followed by
84            // the offset.
85            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    /// The log file on disk.
113    pub(super) fn file(&mut self) -> &mut LogFile {
114        &mut self.file
115    }
116
117    /// Write a raw chunk of output of the pipe, partial lines right away
118    /// with the `P` marker.
119    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;