Skip to main content

atlas_local/client/
get_logs.rs

1use crate::{
2    client::Client,
3    docker::DockerLogContainer,
4    models::{LogOutput, LogsOptions},
5};
6use futures_util::{StreamExt, pin_mut};
7
8#[derive(Debug, thiserror::Error)]
9pub enum GetLogsError {
10    #[error("Failed to get container logs: {0}")]
11    ContainerLogs(String),
12}
13
14impl<D: DockerLogContainer> Client<D> {
15    /// Gets the logs from a container.
16    ///
17    /// # Arguments
18    ///
19    /// * `container_id_or_name` - The ID or name of the container to get logs from.
20    /// * `options` - Optional logging options (e.g., tail, timestamps, etc.)
21    ///
22    /// # Returns
23    ///
24    /// A `Result` containing a vector of log entries from the container, or an error if the logs could not be retrieved.
25    ///
26    /// # Examples
27    ///
28    /// See the complete working example:
29    ///
30    /// ```sh
31    /// cargo run --example get_logs
32    /// ```
33    ///
34    #[doc = "Example code:\n\n"]
35    #[doc = "```rust,no_run"]
36    #[doc = include_str!("../../examples/get_logs.rs")]
37    #[doc = "```"]
38    pub async fn get_logs(
39        &self,
40        container_id_or_name: &str,
41        options: Option<LogsOptions>,
42    ) -> Result<Vec<LogOutput>, GetLogsError> {
43        let bollard_options = options.map(bollard::query_parameters::LogsOptions::from);
44        let stream = self.docker.logs(container_id_or_name, bollard_options);
45        pin_mut!(stream);
46
47        let mut logs = Vec::new();
48        while let Some(result) = stream.next().await {
49            let log_output = result.map_err(GetLogsError::ContainerLogs)?;
50            logs.push(LogOutput::from(log_output));
51        }
52
53        Ok(logs)
54    }
55}
56
57#[cfg(test)]
58mod tests {
59    use super::*;
60    use crate::models::LogsOptions;
61    use futures_util::{Stream, stream};
62    use mockall::mock;
63
64    mock! {
65        Docker {}
66
67        impl DockerLogContainer for Docker {
68            fn logs<'a>(
69                &'a self,
70                container_id: &str,
71                options: Option<bollard::query_parameters::LogsOptions>,
72            ) -> impl Stream<Item = Result<bollard::container::LogOutput, String>>;
73        }
74    }
75
76    #[tokio::test]
77    async fn test_get_logs_success() {
78        // Arrange
79        let mut mock_docker = MockDocker::new();
80
81        // Set up expectations
82        mock_docker
83            .expect_logs()
84            .withf(|container_id, options| container_id == "test-container" && options.is_some())
85            .times(1)
86            .returning(|_, _| {
87                Box::pin(stream::iter(vec![
88                    Ok(bollard::container::LogOutput::StdOut {
89                        message: "Log line 1\n".into(),
90                    }),
91                    Ok(bollard::container::LogOutput::StdOut {
92                        message: "Log line 2\n".into(),
93                    }),
94                ]))
95            });
96
97        let client = Client::new(mock_docker);
98        let options = LogsOptions::builder().stdout(true).stderr(true).build();
99
100        // Act
101        let logs = client
102            .get_logs("test-container", Some(options))
103            .await
104            .expect("get_logs should succeed");
105
106        // Assert
107        assert_eq!(logs.len(), 2);
108        assert!(logs[0].is_stdout());
109        assert!(logs[1].is_stdout());
110    }
111
112    #[tokio::test]
113    async fn test_get_logs_error() {
114        // Arrange
115        let mut mock_docker = MockDocker::new();
116
117        // Set up expectations
118        mock_docker
119            .expect_logs()
120            .withf(|container_id, options| {
121                container_id == "nonexistent-container" && options.is_none()
122            })
123            .times(1)
124            .returning(|_, _| Box::pin(stream::iter(vec![Err("No such container".to_string())])));
125
126        let client = Client::new(mock_docker);
127
128        // Act
129        let result = client.get_logs("nonexistent-container", None).await;
130
131        // Assert
132        assert!(result.is_err());
133        assert!(matches!(
134            result.as_ref().unwrap_err(),
135            GetLogsError::ContainerLogs(_)
136        ));
137    }
138
139    #[tokio::test]
140    async fn test_get_logs_mixed_stdout_stderr() {
141        // Arrange
142        let mut mock_docker = MockDocker::new();
143
144        // Set up expectations
145        mock_docker
146            .expect_logs()
147            .withf(|container_id, _| container_id == "test-container")
148            .times(1)
149            .returning(|_, _| {
150                Box::pin(stream::iter(vec![
151                    Ok(bollard::container::LogOutput::StdOut {
152                        message: "stdout line\n".into(),
153                    }),
154                    Ok(bollard::container::LogOutput::StdErr {
155                        message: "stderr line\n".into(),
156                    }),
157                    Ok(bollard::container::LogOutput::StdOut {
158                        message: "another stdout line\n".into(),
159                    }),
160                ]))
161            });
162
163        let client = Client::new(mock_docker);
164        let options = LogsOptions::builder().stdout(true).stderr(true).build();
165
166        // Act
167        let logs = client
168            .get_logs("test-container", Some(options))
169            .await
170            .expect("get_logs should succeed");
171
172        // Assert
173        assert_eq!(logs.len(), 3);
174
175        // Verify first is stdout
176        assert!(logs[0].is_stdout());
177
178        // Verify second is stderr
179        assert!(logs[1].is_stderr());
180
181        // Verify third is stdout
182        assert!(logs[2].is_stdout());
183    }
184}