Skip to main content

atlas_local/models/
logs_options.rs

1use chrono::{DateTime, Utc};
2
3/// Specifies how many lines to retrieve from the tail of the logs.
4#[derive(Debug, Clone, PartialEq, Eq)]
5pub enum Tail {
6    /// Return all log lines
7    All,
8    /// Return a specific number of lines from the end
9    Number(u64),
10}
11
12/// Error type for parsing `Tail` from a string.
13#[derive(Debug, thiserror::Error)]
14pub enum TailParseError {
15    /// The string is not a valid tail value (must be "all" or a positive number)
16    #[error("Invalid tail value: '{0}'. Expected 'all' or a positive number")]
17    InvalidValue(String),
18}
19
20impl From<u64> for Tail {
21    fn from(n: u64) -> Self {
22        Tail::Number(n)
23    }
24}
25
26impl TryFrom<&str> for Tail {
27    type Error = TailParseError;
28
29    fn try_from(s: &str) -> Result<Self, Self::Error> {
30        match s {
31            "all" => Ok(Tail::All),
32            _ => s
33                .parse::<u64>()
34                .map(Tail::Number)
35                .map_err(|_| TailParseError::InvalidValue(s.to_string())),
36        }
37    }
38}
39
40impl TryFrom<String> for Tail {
41    type Error = TailParseError;
42
43    fn try_from(s: String) -> Result<Self, Self::Error> {
44        s.as_str().try_into()
45    }
46}
47
48impl std::fmt::Display for Tail {
49    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50        match self {
51            Tail::All => write!(f, "all"),
52            Tail::Number(n) => write!(f, "{}", n),
53        }
54    }
55}
56
57/// Options for retrieving logs from a container.
58///
59/// This struct provides configuration options for fetching container logs,
60/// including filtering by stream type (stdout/stderr), limiting the number
61/// of lines, and adding timestamps.
62///
63/// # Examples
64///
65/// ```
66/// use atlas_local::models::{LogsOptions, Tail};
67///
68/// let options = LogsOptions::builder()
69///     .stdout(true)
70///     .stderr(true)
71///     .tail(Tail::Number(100))
72///     .timestamps(true)
73///     .build();
74/// ```
75#[derive(Debug, Clone, Default, PartialEq, typed_builder::TypedBuilder)]
76#[builder(doc)]
77pub struct LogsOptions {
78    /// Return logs from stdout
79    #[builder(default = false)]
80    pub stdout: bool,
81    /// Return logs from stderr
82    #[builder(default = false)]
83    pub stderr: bool,
84    /// Return logs from the given timestamp
85    #[builder(default, setter(strip_option))]
86    pub since: Option<DateTime<Utc>>,
87    /// Return logs before the given timestamp
88    #[builder(default, setter(strip_option))]
89    pub until: Option<DateTime<Utc>>,
90    /// Add timestamps to every log line
91    #[builder(default = false)]
92    pub timestamps: bool,
93    /// Return this number of lines at the tail of the logs
94    #[builder(default, setter(strip_option, into))]
95    pub tail: Option<Tail>,
96}
97
98impl From<LogsOptions> for bollard::query_parameters::LogsOptions {
99    fn from(options: LogsOptions) -> Self {
100        bollard::query_parameters::LogsOptions {
101            follow: false,
102            stdout: options.stdout,
103            stderr: options.stderr,
104            since: options.since.map(|t| t.timestamp() as i32).unwrap_or(0),
105            until: options.until.map(|t| t.timestamp() as i32).unwrap_or(0),
106            timestamps: options.timestamps,
107            tail: options.tail.map(|t| t.to_string()).unwrap_or_default(),
108        }
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    #[test]
117    fn test_logs_options_into_bollard() {
118        let options = LogsOptions::builder()
119            .stdout(true)
120            .stderr(true)
121            .since(DateTime::from_timestamp(1234567890, 0).unwrap())
122            .until(DateTime::from_timestamp(1234567900, 0).unwrap())
123            .timestamps(true)
124            .tail(Tail::Number(100))
125            .build();
126
127        let bollard_options: bollard::query_parameters::LogsOptions = options.into();
128
129        assert!(bollard_options.stdout);
130        assert!(bollard_options.stderr);
131        assert_eq!(bollard_options.since, 1234567890);
132        assert_eq!(bollard_options.until, 1234567900);
133        assert!(bollard_options.timestamps);
134        assert_eq!(bollard_options.tail, "100");
135        assert!(!bollard_options.follow);
136    }
137
138    #[test]
139    fn test_tail_display() {
140        assert_eq!(Tail::All.to_string(), "all");
141        assert_eq!(Tail::Number(100).to_string(), "100");
142        assert_eq!(Tail::Number(0).to_string(), "0");
143    }
144
145    #[test]
146    fn test_tail_try_from_str() {
147        // Valid values
148        assert_eq!(Tail::try_from("all").unwrap(), Tail::All);
149        assert_eq!(Tail::try_from("100").unwrap(), Tail::Number(100));
150        assert_eq!(Tail::try_from("0").unwrap(), Tail::Number(0));
151
152        // Test invalid values
153        let result = Tail::try_from("ALL");
154        assert!(result.is_err());
155        assert!(matches!(
156            result.unwrap_err(),
157            TailParseError::InvalidValue(_)
158        ));
159
160        let result = Tail::try_from("invalid");
161        assert!(result.is_err());
162        assert!(matches!(
163            result.unwrap_err(),
164            TailParseError::InvalidValue(_)
165        ));
166
167        let result = Tail::try_from("-1");
168        assert!(result.is_err());
169
170        let result = Tail::try_from("");
171        assert!(result.is_err());
172    }
173
174    #[test]
175    fn test_tail_try_from_string() {
176        assert_eq!(Tail::try_from("all".to_string()).unwrap(), Tail::All);
177        assert_eq!(
178            Tail::try_from("100".to_string()).unwrap(),
179            Tail::Number(100)
180        );
181
182        let result = Tail::try_from("invalid".to_string());
183        assert!(result.is_err());
184    }
185
186    #[test]
187    fn test_tail_from_u64() {
188        assert_eq!(Tail::from(100u64), Tail::Number(100));
189        assert_eq!(Tail::from(0u64), Tail::Number(0));
190    }
191
192    #[test]
193    fn test_logs_options_into_bollard_with_tail_all() {
194        let options = LogsOptions {
195            stdout: true,
196            stderr: false,
197            since: None,
198            until: None,
199            timestamps: false,
200            tail: Some(Tail::All),
201        };
202
203        let bollard_options: bollard::query_parameters::LogsOptions = options.into();
204        assert_eq!(bollard_options.tail, "all");
205    }
206
207    #[test]
208    fn test_logs_options_builder_with_u64_tail() {
209        // Test that builder accepts u64 directly
210        let options = LogsOptions::builder().stdout(true).tail(100u64).build();
211
212        assert_eq!(options.tail, Some(Tail::Number(100)));
213    }
214
215    #[test]
216    fn test_tail_parse_error_display() {
217        let err = TailParseError::InvalidValue("bad_value".to_string());
218        let error_msg = err.to_string();
219        assert!(error_msg.contains("Invalid tail value"));
220        assert!(error_msg.contains("bad_value"));
221    }
222}