use crate::{
client::Client,
docker::DockerLogContainer,
models::{LogOutput, LogsOptions},
};
use futures_util::{StreamExt, pin_mut};
#[derive(Debug, thiserror::Error)]
pub enum GetLogsError {
#[error("Failed to get container logs: {0}")]
ContainerLogs(String),
}
impl<D: DockerLogContainer> Client<D> {
#[doc = "Example code:\n\n"]
#[doc = "```rust,no_run"]
#[doc = include_str!("../../examples/get_logs.rs")]
#[doc = "```"]
pub async fn get_logs(
&self,
container_id_or_name: &str,
options: Option<LogsOptions>,
) -> Result<Vec<LogOutput>, GetLogsError> {
let bollard_options = options.map(bollard::query_parameters::LogsOptions::from);
let stream = self.docker.logs(container_id_or_name, bollard_options);
pin_mut!(stream);
let mut logs = Vec::new();
while let Some(result) = stream.next().await {
let log_output = result.map_err(GetLogsError::ContainerLogs)?;
logs.push(LogOutput::from(log_output));
}
Ok(logs)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::models::LogsOptions;
use futures_util::{Stream, stream};
use mockall::mock;
mock! {
Docker {}
impl DockerLogContainer for Docker {
fn logs<'a>(
&'a self,
container_id: &str,
options: Option<bollard::query_parameters::LogsOptions>,
) -> impl Stream<Item = Result<bollard::container::LogOutput, String>>;
}
}
#[tokio::test]
async fn test_get_logs_success() {
let mut mock_docker = MockDocker::new();
mock_docker
.expect_logs()
.withf(|container_id, options| container_id == "test-container" && options.is_some())
.times(1)
.returning(|_, _| {
Box::pin(stream::iter(vec![
Ok(bollard::container::LogOutput::StdOut {
message: "Log line 1\n".into(),
}),
Ok(bollard::container::LogOutput::StdOut {
message: "Log line 2\n".into(),
}),
]))
});
let client = Client::new(mock_docker);
let options = LogsOptions::builder().stdout(true).stderr(true).build();
let logs = client
.get_logs("test-container", Some(options))
.await
.expect("get_logs should succeed");
assert_eq!(logs.len(), 2);
assert!(logs[0].is_stdout());
assert!(logs[1].is_stdout());
}
#[tokio::test]
async fn test_get_logs_error() {
let mut mock_docker = MockDocker::new();
mock_docker
.expect_logs()
.withf(|container_id, options| {
container_id == "nonexistent-container" && options.is_none()
})
.times(1)
.returning(|_, _| Box::pin(stream::iter(vec![Err("No such container".to_string())])));
let client = Client::new(mock_docker);
let result = client.get_logs("nonexistent-container", None).await;
assert!(result.is_err());
assert!(matches!(
result.as_ref().unwrap_err(),
GetLogsError::ContainerLogs(_)
));
}
#[tokio::test]
async fn test_get_logs_mixed_stdout_stderr() {
let mut mock_docker = MockDocker::new();
mock_docker
.expect_logs()
.withf(|container_id, _| container_id == "test-container")
.times(1)
.returning(|_, _| {
Box::pin(stream::iter(vec![
Ok(bollard::container::LogOutput::StdOut {
message: "stdout line\n".into(),
}),
Ok(bollard::container::LogOutput::StdErr {
message: "stderr line\n".into(),
}),
Ok(bollard::container::LogOutput::StdOut {
message: "another stdout line\n".into(),
}),
]))
});
let client = Client::new(mock_docker);
let options = LogsOptions::builder().stdout(true).stderr(true).build();
let logs = client
.get_logs("test-container", Some(options))
.await
.expect("get_logs should succeed");
assert_eq!(logs.len(), 3);
assert!(logs[0].is_stdout());
assert!(logs[1].is_stderr());
assert!(logs[2].is_stdout());
}
}