docker_wrapper/compose/
logs.rs

1//! Docker Compose logs command implementation.
2
3use super::{execute_compose_command, ComposeCommand, ComposeConfig, ComposeOutput};
4use crate::error::Result;
5use async_trait::async_trait;
6
7/// Docker Compose logs command builder
8#[derive(Debug, Clone)]
9#[allow(clippy::struct_excessive_bools)] // Multiple boolean flags are appropriate for logs command
10pub struct ComposeLogsCommand {
11    /// Base compose configuration
12    config: ComposeConfig,
13    /// Services to show logs for (empty for all)
14    services: Vec<String>,
15    /// Follow log output
16    follow: bool,
17    /// Show timestamps
18    timestamps: bool,
19    /// Number of lines to show from the end
20    tail: Option<String>,
21    /// Show logs since timestamp
22    since: Option<String>,
23    /// Show logs until timestamp  
24    until: Option<String>,
25    /// Don't print prefix
26    no_log_prefix: bool,
27    /// Don't use colors
28    no_color: bool,
29}
30
31impl ComposeLogsCommand {
32    /// Create a new compose logs command
33    #[must_use]
34    pub fn new() -> Self {
35        Self {
36            config: ComposeConfig::new(),
37            services: Vec::new(),
38            follow: false,
39            timestamps: false,
40            tail: None,
41            since: None,
42            until: None,
43            no_log_prefix: false,
44            no_color: false,
45        }
46    }
47
48    /// Create with a specific compose configuration
49    #[must_use]
50    pub fn with_config(config: ComposeConfig) -> Self {
51        Self {
52            config,
53            ..Self::new()
54        }
55    }
56
57    /// Add a service to show logs for
58    #[must_use]
59    pub fn service(mut self, service: impl Into<String>) -> Self {
60        self.services.push(service.into());
61        self
62    }
63
64    /// Follow log output
65    #[must_use]
66    pub fn follow(mut self) -> Self {
67        self.follow = true;
68        self
69    }
70
71    /// Show timestamps
72    #[must_use]
73    pub fn timestamps(mut self) -> Self {
74        self.timestamps = true;
75        self
76    }
77
78    /// Number of lines to show from the end
79    #[must_use]
80    pub fn tail(mut self, lines: impl Into<String>) -> Self {
81        self.tail = Some(lines.into());
82        self
83    }
84
85    /// Show logs since timestamp
86    #[must_use]
87    pub fn since(mut self, timestamp: impl Into<String>) -> Self {
88        self.since = Some(timestamp.into());
89        self
90    }
91
92    /// Show logs until timestamp
93    #[must_use]
94    pub fn until(mut self, timestamp: impl Into<String>) -> Self {
95        self.until = Some(timestamp.into());
96        self
97    }
98
99    /// Don't print prefix
100    #[must_use]
101    pub fn no_log_prefix(mut self) -> Self {
102        self.no_log_prefix = true;
103        self
104    }
105
106    /// Don't use colors
107    #[must_use]
108    pub fn no_color(mut self) -> Self {
109        self.no_color = true;
110        self
111    }
112
113    /// Set compose file
114    #[must_use]
115    pub fn file(mut self, path: impl Into<std::path::PathBuf>) -> Self {
116        self.config = self.config.file(path);
117        self
118    }
119
120    /// Set project name
121    #[must_use]
122    pub fn project_name(mut self, name: impl Into<String>) -> Self {
123        self.config = self.config.project_name(name);
124        self
125    }
126
127    /// Execute the compose logs command
128    ///
129    /// # Errors
130    /// Returns an error if:
131    /// - Docker Compose is not installed
132    /// - Compose file is not found
133    /// - Service doesn't exist
134    pub async fn run(&self) -> Result<ComposeLogsResult> {
135        let output = self.execute().await?;
136
137        Ok(ComposeLogsResult {
138            output,
139            services: self.services.clone(),
140        })
141    }
142}
143
144impl Default for ComposeLogsCommand {
145    fn default() -> Self {
146        Self::new()
147    }
148}
149
150#[async_trait]
151impl ComposeCommand for ComposeLogsCommand {
152    type Output = ComposeOutput;
153
154    fn subcommand(&self) -> &'static str {
155        "logs"
156    }
157
158    fn build_args(&self) -> Vec<String> {
159        let mut args = Vec::new();
160
161        if self.follow {
162            args.push("--follow".to_string());
163        }
164
165        if self.timestamps {
166            args.push("--timestamps".to_string());
167        }
168
169        if let Some(ref tail) = self.tail {
170            args.push("--tail".to_string());
171            args.push(tail.clone());
172        }
173
174        if let Some(ref since) = self.since {
175            args.push("--since".to_string());
176            args.push(since.clone());
177        }
178
179        if let Some(ref until) = self.until {
180            args.push("--until".to_string());
181            args.push(until.clone());
182        }
183
184        if self.no_log_prefix {
185            args.push("--no-log-prefix".to_string());
186        }
187
188        if self.no_color {
189            args.push("--no-color".to_string());
190        }
191
192        // Add service names at the end
193        args.extend(self.services.clone());
194
195        args
196    }
197
198    async fn execute(&self) -> Result<Self::Output> {
199        execute_compose_command(&self.config, self.subcommand(), self.build_args()).await
200    }
201
202    fn config(&self) -> &ComposeConfig {
203        &self.config
204    }
205}
206
207/// Result from compose logs command
208#[derive(Debug, Clone)]
209pub struct ComposeLogsResult {
210    /// Raw command output
211    pub output: ComposeOutput,
212    /// Services logs were fetched for
213    pub services: Vec<String>,
214}
215
216impl ComposeLogsResult {
217    /// Check if the command was successful
218    #[must_use]
219    pub fn success(&self) -> bool {
220        self.output.success
221    }
222
223    /// Get the services
224    #[must_use]
225    pub fn services(&self) -> &[String] {
226        &self.services
227    }
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233
234    #[test]
235    fn test_compose_logs_basic() {
236        let cmd = ComposeLogsCommand::new();
237        let args = cmd.build_args();
238        assert!(args.is_empty());
239    }
240
241    #[test]
242    fn test_compose_logs_follow() {
243        let cmd = ComposeLogsCommand::new().follow().timestamps();
244        let args = cmd.build_args();
245        assert_eq!(args, vec!["--follow", "--timestamps"]);
246    }
247
248    #[test]
249    fn test_compose_logs_with_tail() {
250        let cmd = ComposeLogsCommand::new().tail("100").service("web");
251        let args = cmd.build_args();
252        assert_eq!(args, vec!["--tail", "100", "web"]);
253    }
254
255    #[test]
256    fn test_compose_logs_all_options() {
257        let cmd = ComposeLogsCommand::new()
258            .follow()
259            .timestamps()
260            .tail("50")
261            .since("2024-01-01T00:00:00")
262            .no_color()
263            .service("web")
264            .service("db");
265
266        let args = cmd.build_args();
267        assert!(args.contains(&"--follow".to_string()));
268        assert!(args.contains(&"--timestamps".to_string()));
269        assert!(args.contains(&"--tail".to_string()));
270        assert!(args.contains(&"50".to_string()));
271        assert!(args.contains(&"--since".to_string()));
272        assert!(args.contains(&"2024-01-01T00:00:00".to_string()));
273        assert!(args.contains(&"--no-color".to_string()));
274        assert!(args.contains(&"web".to_string()));
275        assert!(args.contains(&"db".to_string()));
276    }
277}