1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
use chrono::DateTime;
use std::str::FromStr;

/// struct that represents a single log entry in CRI log format.
///  CRI Log format example:
///    2016-10-06T00:17:09.669794202Z stdout P log content 1
//     2016-10-06T00:17:09.669794203Z stderr F log content 2
//  See: https://github.com/kubernetes/kubernetes/blob/master/pkg/kubelet/kuberuntime/logs/logs.go#L128
pub struct CriLog {
    timestamp: DateTime<chrono::offset::FixedOffset>,
    stream_type: StreamType,
    tag: String,
    log: String,
}

impl CriLog {
    /// Get timestamp associated to log entry
    pub fn timestamp(&self) -> &DateTime<chrono::offset::FixedOffset> {
        &self.timestamp
    }

    /// Returns true if log entry is of type stderr
    pub fn is_stderr(&self) -> bool {
        self.stream_type == StreamType::StdErr
    }

    /// Returns true if log entry is of type stdout
    pub fn is_stdout(&self) -> bool {
        self.stream_type == StreamType::StdOut
    }

    /// Get tag attribute from log entry
    pub fn tag(&self) -> &str {
        &self.tag
    }

    /// Get message from log entry
    pub fn log(&self) -> &str {
        &self.log
    }
}

impl FromStr for CriLog {
    type Err = ParsingError;

    fn from_str(input: &str) -> Result<Self, <Self as FromStr>::Err> {
        let mut iter = input.split_whitespace();

        let timestamp_str = iter.next().ok_or(ParsingError::MissingTimestamp)?;
        let timestamp = DateTime::parse_from_rfc3339(timestamp_str)
            .map_err(|_| ParsingError::TimestampFormat(timestamp_str.into()))?;

        let stream_type_str = iter.next().ok_or(ParsingError::MissingStreamType)?;
        let stream_type = StreamType::from_str(stream_type_str)
            .map_err(|_| ParsingError::InvalidStreamType(stream_type_str.into()))?;

        let tag = iter.next().ok_or(ParsingError::MissingLogTag)?.to_owned();

        let log = iter.collect::<Vec<&str>>().join(" ");

        Ok(CriLog {
            timestamp,
            stream_type,
            tag,
            log,
        })
    }
}

#[derive(Debug, thiserror::Error)]
pub enum ParsingError {
    #[error("Missing timestamp in log entry")]
    MissingTimestamp,
    #[error("Timestamp format error: {0}")]
    TimestampFormat(String),
    #[error("Missing stream type")]
    MissingStreamType,
    #[error("Invalid stream type: {0}")]
    InvalidStreamType(String),
    #[error("Missing log tag")]
    MissingLogTag,
}

#[derive(Debug, PartialEq)]
pub enum StreamType {
    StdOut,
    StdErr,
}

impl FromStr for StreamType {
    type Err = InvalidStreamType;
    fn from_str(input: &str) -> Result<Self, <Self as FromStr>::Err> {
        match input {
            "stderr" => Ok(StreamType::StdErr),
            "stdout" => Ok(StreamType::StdOut),
            input => Err(InvalidStreamType(input.into())),
        }
    }
}

pub struct InvalidStreamType(String);

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn stdout() {
        //   2016-10-06T00:17:09.669794202Z stdout P log content 1
        //   2016-10-06T00:17:09.669794203Z stderr F log content 2
        let log_str = "2016-10-06T00:17:09.669794202Z stdout P log content 1";
        let crilog = CriLog::from_str(log_str).expect("failed to parse");
        assert!(crilog.is_stdout());
        assert_eq!(crilog.tag(), "P");
        assert_eq!(crilog.log(), "log content 1");
    }

    #[test]
    fn stderr() {
        let log_str = "2016-10-06T00:17:09.669794203Z stderr F log content 2";
        let crilog = CriLog::from_str(log_str).expect("failed to parse");
        assert!(crilog.is_stderr());
        assert_eq!(crilog.tag(), "F");
        assert_eq!(crilog.log(), "log content 2");
    }
}