crilog/
lib.rs

1use chrono::DateTime;
2use std::str::FromStr;
3
4/// struct that represents a single log entry in CRI log format.
5///  CRI Log format example:
6///    2016-10-06T00:17:09.669794202Z stdout P log content 1
7//     2016-10-06T00:17:09.669794203Z stderr F log content 2
8//  See: https://github.com/kubernetes/kubernetes/blob/master/pkg/kubelet/kuberuntime/logs/logs.go#L128
9pub struct CriLog {
10    timestamp: DateTime<chrono::offset::FixedOffset>,
11    stream_type: StreamType,
12    tag: String,
13    log: String,
14}
15
16impl CriLog {
17    /// Get timestamp associated to log entry
18    pub fn timestamp(&self) -> &DateTime<chrono::offset::FixedOffset> {
19        &self.timestamp
20    }
21
22    /// Returns true if log entry is of type stderr
23    pub fn is_stderr(&self) -> bool {
24        self.stream_type == StreamType::StdErr
25    }
26
27    /// Returns true if log entry is of type stdout
28    pub fn is_stdout(&self) -> bool {
29        self.stream_type == StreamType::StdOut
30    }
31
32    /// Get tag attribute from log entry
33    pub fn tag(&self) -> &str {
34        &self.tag
35    }
36
37    /// Get message from log entry
38    pub fn log(&self) -> &str {
39        &self.log
40    }
41}
42
43impl FromStr for CriLog {
44    type Err = ParsingError;
45
46    fn from_str(input: &str) -> Result<Self, <Self as FromStr>::Err> {
47        let mut iter = input.split_whitespace();
48
49        let timestamp_str = iter.next().ok_or(ParsingError::MissingTimestamp)?;
50        let timestamp = DateTime::parse_from_rfc3339(timestamp_str)
51            .map_err(|_| ParsingError::TimestampFormat(timestamp_str.into()))?;
52
53        let stream_type_str = iter.next().ok_or(ParsingError::MissingStreamType)?;
54        let stream_type = StreamType::from_str(stream_type_str)
55            .map_err(|_| ParsingError::InvalidStreamType(stream_type_str.into()))?;
56
57        let tag = iter.next().ok_or(ParsingError::MissingLogTag)?.to_owned();
58
59        let log = iter.collect::<Vec<&str>>().join(" ");
60
61        Ok(CriLog {
62            timestamp,
63            stream_type,
64            tag,
65            log,
66        })
67    }
68}
69
70#[derive(Debug, thiserror::Error)]
71pub enum ParsingError {
72    #[error("Missing timestamp in log entry")]
73    MissingTimestamp,
74    #[error("Timestamp format error: {0}")]
75    TimestampFormat(String),
76    #[error("Missing stream type")]
77    MissingStreamType,
78    #[error("Invalid stream type: {0}")]
79    InvalidStreamType(String),
80    #[error("Missing log tag")]
81    MissingLogTag,
82}
83
84#[derive(Debug, PartialEq)]
85pub enum StreamType {
86    StdOut,
87    StdErr,
88}
89
90impl FromStr for StreamType {
91    type Err = InvalidStreamType;
92    fn from_str(input: &str) -> Result<Self, <Self as FromStr>::Err> {
93        match input {
94            "stderr" => Ok(StreamType::StdErr),
95            "stdout" => Ok(StreamType::StdOut),
96            input => Err(InvalidStreamType(input.into())),
97        }
98    }
99}
100
101pub struct InvalidStreamType(String);
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106    #[test]
107    fn stdout() {
108        //   2016-10-06T00:17:09.669794202Z stdout P log content 1
109        //   2016-10-06T00:17:09.669794203Z stderr F log content 2
110        let log_str = "2016-10-06T00:17:09.669794202Z stdout P log content 1";
111        let crilog = CriLog::from_str(log_str).expect("failed to parse");
112        assert!(crilog.is_stdout());
113        assert_eq!(crilog.tag(), "P");
114        assert_eq!(crilog.log(), "log content 1");
115    }
116
117    #[test]
118    fn stderr() {
119        let log_str = "2016-10-06T00:17:09.669794203Z stderr F log content 2";
120        let crilog = CriLog::from_str(log_str).expect("failed to parse");
121        assert!(crilog.is_stderr());
122        assert_eq!(crilog.tag(), "F");
123        assert_eq!(crilog.log(), "log content 2");
124    }
125}