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