Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: introduce LoggingConsumer based on log crate #682

Merged
merged 12 commits into from
Jul 6, 2024
2 changes: 2 additions & 0 deletions testcontainers/src/core/logs/consumer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ use futures::{future::BoxFuture, FutureExt};

use crate::core::logs::LogFrame;

pub mod logging_consumer;

/// Log consumer is a trait that allows to consume log frames.
/// Consumers will be called for each log frame that is produced by the container for the whole lifecycle of the container.
pub trait LogConsumer: Send + Sync {
Expand Down
56 changes: 56 additions & 0 deletions testcontainers/src/core/logs/consumer/logging_consumer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
use futures::{future::BoxFuture, FutureExt};

use crate::core::logs::{consumer::LogConsumer, LogFrame};

/// A consumer that logs the output of container with the [`log`] crate.
///
/// By default, both standard out and standard error will both be emitted at INFO level.
#[derive(Debug)]
pub struct LoggingConsumer {
stdout_level: log::Level,
stderr_level: log::Level,
}

impl LoggingConsumer {
/// Creates a new instance of the logging consumer.
pub fn new() -> Self {
Self {
stdout_level: log::Level::Info,
stderr_level: log::Level::Info,
}
}

/// Sets the log level for standard out. By default, this is `INFO`.
pub fn with_stdout_level(mut self, level: log::Level) -> Self {
self.stdout_level = level;
self
}

/// Sets the log level for standard error. By default, this is `INFO`.
pub fn with_stderr_level(mut self, level: log::Level) -> Self {
self.stderr_level = level;
self
}
}

impl Default for LoggingConsumer {
fn default() -> Self {
Self::new()
}
}

impl LogConsumer for LoggingConsumer {
fn accept<'a>(&'a self, record: &'a LogFrame) -> BoxFuture<'a, ()> {
async move {
match record {
LogFrame::StdOut(bytes) => {
log::log!(self.stdout_level, "{}", String::from_utf8_lossy(bytes));
}
LogFrame::StdErr(bytes) => {
log::log!(self.stderr_level, "{}", String::from_utf8_lossy(bytes));
}
}
}
.boxed()
}
}
5 changes: 4 additions & 1 deletion testcontainers/tests/async_runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ use bollard::Docker;
use reqwest::StatusCode;
use testcontainers::{
core::{
logs::LogFrame, wait::HttpWaitStrategy, CmdWaitFor, ExecCommand, IntoContainerPort, WaitFor,
logs::{consumer::logging_consumer::LoggingConsumer, LogFrame},
wait::HttpWaitStrategy,
CmdWaitFor, ExecCommand, IntoContainerPort, WaitFor,
},
runners::AsyncRunner,
GenericImage, *,
Expand Down Expand Up @@ -181,6 +183,7 @@ async fn async_run_with_log_consumer() -> anyhow::Result<()> {
let _ = tx.send(());
}
})
.with_log_consumer(LoggingConsumer::new().with_stderr_level(log::Level::Error))
.start()
.await?;
rx.recv()?; // notification from consumer
Expand Down
24 changes: 23 additions & 1 deletion testcontainers/tests/sync_runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@

use reqwest::StatusCode;
use testcontainers::{
core::{wait::HttpWaitStrategy, CmdWaitFor, ExecCommand, Host, IntoContainerPort, WaitFor},
core::{
logs::{consumer::logging_consumer::LoggingConsumer, LogFrame},
wait::HttpWaitStrategy,
CmdWaitFor, ExecCommand, Host, IntoContainerPort, WaitFor,
},
runners::SyncRunner,
*,
};
Expand Down Expand Up @@ -196,3 +200,21 @@ fn sync_run_exec() -> anyhow::Result<()> {
assert_eq!(stderr, "stderr 1\nstderr 2\n");
Ok(())
}

#[test]
fn sync_run_with_log_consumer() -> anyhow::Result<()> {
let _ = pretty_env_logger::try_init();

let (tx, rx) = std::sync::mpsc::sync_channel(1);
let _container = HelloWorld
.with_log_consumer(move |frame: &LogFrame| {
// notify when the expected message is found
if String::from_utf8_lossy(frame.bytes()) == "Hello from Docker!\n" {
let _ = tx.send(());
}
})
.with_log_consumer(LoggingConsumer::new().with_stderr_level(log::Level::Error))
.start()?;
rx.recv()?; // notification from consumer
Ok(())
}
Loading