docker_wrapper/command/
logs.rs

1//! Docker logs command implementation.
2//!
3//! This module provides the `docker logs` command for viewing container logs.
4
5use super::{CommandExecutor, CommandOutput, DockerCommand};
6use crate::error::Result;
7use crate::stream::{OutputLine, StreamResult, StreamableCommand};
8use async_trait::async_trait;
9use tokio::process::Command as TokioCommand;
10use tokio::sync::mpsc;
11
12/// Docker logs command builder
13#[derive(Debug, Clone)]
14pub struct LogsCommand {
15    /// Container name or ID
16    container: String,
17    /// Follow log output
18    follow: bool,
19    /// Show timestamps
20    timestamps: bool,
21    /// Number of lines to show from the end
22    tail: Option<String>,
23    /// Show logs since timestamp
24    since: Option<String>,
25    /// Show logs until timestamp
26    until: Option<String>,
27    /// Show extra details
28    details: bool,
29    /// Command executor
30    pub executor: CommandExecutor,
31}
32
33impl LogsCommand {
34    /// Create a new logs command
35    #[must_use]
36    pub fn new(container: impl Into<String>) -> Self {
37        Self {
38            container: container.into(),
39            follow: false,
40            timestamps: false,
41            tail: None,
42            since: None,
43            until: None,
44            details: false,
45            executor: CommandExecutor::new(),
46        }
47    }
48
49    /// Follow log output (like tail -f)
50    #[must_use]
51    pub fn follow(mut self) -> Self {
52        self.follow = true;
53        self
54    }
55
56    /// Show timestamps
57    #[must_use]
58    pub fn timestamps(mut self) -> Self {
59        self.timestamps = true;
60        self
61    }
62
63    /// Number of lines to show from the end of the logs
64    #[must_use]
65    pub fn tail(mut self, lines: impl Into<String>) -> Self {
66        self.tail = Some(lines.into());
67        self
68    }
69
70    /// Show all logs (equivalent to tail("all"))
71    #[must_use]
72    pub fn all(mut self) -> Self {
73        self.tail = Some("all".to_string());
74        self
75    }
76
77    /// Show logs since timestamp (e.g., 2013-01-02T13:23:37Z) or relative (e.g., 42m)
78    #[must_use]
79    pub fn since(mut self, timestamp: impl Into<String>) -> Self {
80        self.since = Some(timestamp.into());
81        self
82    }
83
84    /// Show logs until timestamp
85    #[must_use]
86    pub fn until(mut self, timestamp: impl Into<String>) -> Self {
87        self.until = Some(timestamp.into());
88        self
89    }
90
91    /// Show extra details provided to logs
92    #[must_use]
93    pub fn details(mut self) -> Self {
94        self.details = true;
95        self
96    }
97
98    /// Execute the logs command
99    ///
100    /// # Errors
101    /// Returns an error if:
102    /// - The Docker daemon is not running
103    /// - The specified container doesn't exist
104    /// - The container has been removed
105    pub async fn run(&self) -> Result<CommandOutput> {
106        self.execute().await
107    }
108}
109
110#[async_trait]
111impl DockerCommand for LogsCommand {
112    type Output = CommandOutput;
113
114    fn get_executor(&self) -> &CommandExecutor {
115        &self.executor
116    }
117
118    fn get_executor_mut(&mut self) -> &mut CommandExecutor {
119        &mut self.executor
120    }
121
122    fn build_command_args(&self) -> Vec<String> {
123        let mut args = vec!["logs".to_string()];
124
125        if self.follow {
126            args.push("--follow".to_string());
127        }
128
129        if self.timestamps {
130            args.push("--timestamps".to_string());
131        }
132
133        if let Some(ref tail) = self.tail {
134            args.push("--tail".to_string());
135            args.push(tail.clone());
136        }
137
138        if let Some(ref since) = self.since {
139            args.push("--since".to_string());
140            args.push(since.clone());
141        }
142
143        if let Some(ref until) = self.until {
144            args.push("--until".to_string());
145            args.push(until.clone());
146        }
147
148        if self.details {
149            args.push("--details".to_string());
150        }
151
152        // Add container name/ID
153        args.push(self.container.clone());
154
155        // Add raw arguments from executor
156        args.extend(self.executor.raw_args.clone());
157
158        args
159    }
160
161    async fn execute(&self) -> Result<Self::Output> {
162        let args = self.build_command_args();
163        self.execute_command(args).await
164    }
165}
166
167// Streaming support for LogsCommand
168#[async_trait]
169impl StreamableCommand for LogsCommand {
170    async fn stream<F>(&self, handler: F) -> Result<StreamResult>
171    where
172        F: FnMut(OutputLine) + Send + 'static,
173    {
174        let mut cmd = TokioCommand::new("docker");
175        cmd.arg("logs");
176
177        for arg in self.build_command_args() {
178            cmd.arg(arg);
179        }
180
181        crate::stream::stream_command(cmd, handler).await
182    }
183
184    async fn stream_channel(&self) -> Result<(mpsc::Receiver<OutputLine>, StreamResult)> {
185        let mut cmd = TokioCommand::new("docker");
186        cmd.arg("logs");
187
188        for arg in self.build_command_args() {
189            cmd.arg(arg);
190        }
191
192        crate::stream::stream_command_channel(cmd).await
193    }
194}
195
196impl LogsCommand {
197    /// Stream container logs with a custom handler
198    ///
199    /// This is particularly useful with the `follow` option to stream logs in real-time.
200    ///
201    /// # Examples
202    ///
203    /// ```no_run
204    /// use docker_wrapper::LogsCommand;
205    /// use docker_wrapper::StreamHandler;
206    ///
207    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
208    /// // Follow logs in real-time
209    /// let result = LogsCommand::new("mycontainer")
210    ///     .follow()
211    ///     .timestamps()
212    ///     .stream(StreamHandler::print())
213    ///     .await?;
214    /// # Ok(())
215    /// # }
216    /// ```
217    ///
218    /// # Errors
219    ///
220    /// Returns an error if the command fails or encounters an I/O error
221    pub async fn stream<F>(&self, handler: F) -> Result<StreamResult>
222    where
223        F: FnMut(OutputLine) + Send + 'static,
224    {
225        <Self as StreamableCommand>::stream(self, handler).await
226    }
227}
228
229#[cfg(test)]
230mod tests {
231    use super::*;
232
233    #[test]
234    fn test_logs_basic() {
235        let cmd = LogsCommand::new("test-container");
236        let args = cmd.build_command_args();
237        assert_eq!(args, vec!["logs", "test-container"]);
238    }
239
240    #[test]
241    fn test_logs_follow() {
242        let cmd = LogsCommand::new("test-container").follow();
243        let args = cmd.build_command_args();
244        assert_eq!(args, vec!["logs", "--follow", "test-container"]);
245    }
246
247    #[test]
248    fn test_logs_with_tail() {
249        let cmd = LogsCommand::new("test-container").tail("100");
250        let args = cmd.build_command_args();
251        assert_eq!(args, vec!["logs", "--tail", "100", "test-container"]);
252    }
253
254    #[test]
255    fn test_logs_with_timestamps() {
256        let cmd = LogsCommand::new("test-container").timestamps();
257        let args = cmd.build_command_args();
258        assert_eq!(args, vec!["logs", "--timestamps", "test-container"]);
259    }
260
261    #[test]
262    fn test_logs_with_since() {
263        let cmd = LogsCommand::new("test-container").since("10m");
264        let args = cmd.build_command_args();
265        assert_eq!(args, vec!["logs", "--since", "10m", "test-container"]);
266    }
267
268    #[test]
269    fn test_logs_all_options() {
270        let cmd = LogsCommand::new("test-container")
271            .follow()
272            .timestamps()
273            .tail("50")
274            .since("1h")
275            .details();
276        let args = cmd.build_command_args();
277        assert_eq!(
278            args,
279            vec![
280                "logs",
281                "--follow",
282                "--timestamps",
283                "--tail",
284                "50",
285                "--since",
286                "1h",
287                "--details",
288                "test-container"
289            ]
290        );
291    }
292}