shuttle_common/models/
log.rs

1use chrono::{DateTime, Utc};
2#[cfg(feature = "display")]
3use crossterm::style::Stylize;
4use serde::{Deserialize, Serialize};
5
6#[derive(Clone, Debug, Deserialize, Serialize)]
7#[typeshare::typeshare]
8pub struct LogItem {
9    pub timestamp: DateTime<Utc>,
10    /// Which container / log stream this line came from
11    pub source: String,
12    pub line: String,
13}
14
15impl LogItem {
16    pub fn new(timestamp: DateTime<Utc>, source: String, line: String) -> Self {
17        Self {
18            timestamp,
19            source,
20            line,
21        }
22    }
23}
24
25#[cfg(feature = "display")]
26impl std::fmt::Display for LogItem {
27    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28        let datetime: chrono::DateTime<chrono::Local> = DateTime::from(self.timestamp);
29
30        write!(
31            f,
32            "{} [{}] {}",
33            datetime
34                .to_rfc3339_opts(chrono::SecondsFormat::Millis, false)
35                .dim(),
36            self.source,
37            self.line,
38        )
39    }
40}
41
42#[derive(Debug, Serialize, Deserialize)]
43#[typeshare::typeshare]
44pub struct LogsResponse {
45    pub logs: Vec<LogItem>,
46}
47
48#[cfg(test)]
49mod tests {
50    #[cfg_attr(not(feature = "display"), allow(unused_imports))]
51    use super::*;
52
53    // Chrono uses std Time (to libc) internally, if you want to use this method
54    // in more than one test, you need to handle async tests properly.
55    #[cfg(feature = "display")]
56    fn with_tz<F: FnOnce()>(tz: &str, f: F) {
57        let prev_tz = std::env::var("TZ").unwrap_or_default();
58        std::env::set_var("TZ", tz);
59        f();
60        std::env::set_var("TZ", prev_tz);
61    }
62
63    #[cfg(feature = "display")]
64    #[rstest::rstest]
65    #[case::utc("utc")]
66    #[case::cest("cest")]
67    fn test_timezone_formatting(#[case] tz: &str) {
68        let item = LogItem::new(
69            Utc::now(),
70            "test".to_string(),
71            r#"{"message": "Building"}"#.to_owned(),
72        );
73
74        with_tz(tz, || {
75            let value = item
76                .timestamp
77                .with_timezone(&chrono::Local)
78                .to_rfc3339_opts(chrono::SecondsFormat::Millis, false);
79
80            let log_line = format!("{}", &item);
81
82            assert!(log_line.contains(&value));
83        });
84    }
85}