diff --git a/.travis.yml b/.travis.yml index 571c078730d..575aa50b208 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,7 +22,7 @@ jobs: - stage: test env: [ NAME=core ] - jdk: openjdk-ea + jdk: openjdk12 script: ./gradlew testcontainers:check --scan --no-daemon - env: [ NAME=selenium ] diff --git a/core/src/main/java/org/testcontainers/containers/ContainerState.java b/core/src/main/java/org/testcontainers/containers/ContainerState.java index 26eec8c4ebe..2066fe72157 100644 --- a/core/src/main/java/org/testcontainers/containers/ContainerState.java +++ b/core/src/main/java/org/testcontainers/containers/ContainerState.java @@ -7,6 +7,8 @@ import com.github.dockerjava.api.model.Ports; import com.google.common.base.Preconditions; import org.testcontainers.DockerClientFactory; +import org.testcontainers.containers.output.OutputFrame; +import org.testcontainers.utility.LogUtils; import java.util.ArrayList; import java.util.List; @@ -149,6 +151,22 @@ default List getBoundPortNumbers() { .collect(Collectors.toList()); } + + /** + * @return all log output from the container from start until the current instant (both stdout and stderr) + */ + default String getLogs() { + return LogUtils.getOutput(DockerClientFactory.instance().client(), getContainerId()); + } + + /** + * @param types log types to return + * @return all log output from the container from start until the current instant + */ + default String getLogs(OutputFrame.OutputType... types) { + return LogUtils.getOutput(DockerClientFactory.instance().client(), getContainerId(), types); + } + /** * @return the id of the container */ diff --git a/core/src/main/java/org/testcontainers/containers/GenericContainer.java b/core/src/main/java/org/testcontainers/containers/GenericContainer.java index b00061b3d97..936d880d1a8 100644 --- a/core/src/main/java/org/testcontainers/containers/GenericContainer.java +++ b/core/src/main/java/org/testcontainers/containers/GenericContainer.java @@ -20,9 +20,7 @@ import org.rnorth.visibleassertions.VisibleAssertions; import org.slf4j.Logger; import org.testcontainers.DockerClientFactory; -import org.testcontainers.containers.output.FrameConsumerResultCallback; import org.testcontainers.containers.output.OutputFrame; -import org.testcontainers.containers.output.Slf4jLogConsumer; import org.testcontainers.containers.startupcheck.IsRunningStartupCheckStrategy; import org.testcontainers.containers.startupcheck.MinimumDurationRunningStartupCheckStrategy; import org.testcontainers.containers.startupcheck.StartupCheckStrategy; @@ -50,8 +48,6 @@ import java.util.stream.Stream; import static com.google.common.collect.Lists.newArrayList; -import static org.testcontainers.containers.output.OutputFrame.OutputType.STDERR; -import static org.testcontainers.containers.output.OutputFrame.OutputType.STDOUT; import static org.testcontainers.utility.CommandLine.runShellCommand; /** @@ -242,24 +238,24 @@ private void tryStart() { logger().info("Starting container with ID: {}", containerId); dockerClient.startContainerCmd(containerId).exec(); + logger().info("Container {} is starting: {}", dockerImageName, containerId); + // For all registered output consumers, start following as close to container startup as possible this.logConsumers.forEach(this::followOutput); - logger().info("Container {} is starting: {}", dockerImageName, containerId); - // Tell subclasses that we're starting containerInfo = dockerClient.inspectContainerCmd(containerId).exec(); containerName = containerInfo.getName(); containerIsStarting(containerInfo); - // Wait until the container is running (may not be fully started) - + // Wait until the container has reached the desired running state if (!this.startupCheckStrategy.waitUntilStartupSuccessful(dockerClient, containerId)) { // Bail out, don't wait for the port to start listening. // (Exception thrown here will be caught below and wrapped) throw new IllegalStateException("Container did not start correctly."); } + // Wait until the process within the container has become ready for use (e.g. listening on network, log message emitted, etc). waitUntilContainerStarted(); logger().info("Container {} started", dockerImageName); @@ -269,17 +265,12 @@ private void tryStart() { if (containerId != null) { // Log output if startup failed, either due to a container failure or exception (including timeout) - logger().error("Container log output (if any) will follow:"); - FrameConsumerResultCallback resultCallback = new FrameConsumerResultCallback(); - resultCallback.addConsumer(STDOUT, new Slf4jLogConsumer(logger())); - resultCallback.addConsumer(STDERR, new Slf4jLogConsumer(logger())); - dockerClient.logContainerCmd(containerId).withStdOut(true).withStdErr(true).exec(resultCallback); - - // Try to ensure that container log output is shown before proceeding - try { - resultCallback.getCompletionLatch().await(1, TimeUnit.MINUTES); - } catch (InterruptedException ignored) { - // Cannot do anything at this point + final String containerLogs = getLogs(); + + if (containerLogs.length() > 0) { + logger().error("Log output from the failed container:\n{}", getLogs()); + } else { + logger().error("There are no stdout/stderr logs available for the failed container"); } } diff --git a/core/src/main/java/org/testcontainers/containers/output/WaitingConsumer.java b/core/src/main/java/org/testcontainers/containers/output/WaitingConsumer.java index 367581528c2..858f5a4b487 100644 --- a/core/src/main/java/org/testcontainers/containers/output/WaitingConsumer.java +++ b/core/src/main/java/org/testcontainers/containers/output/WaitingConsumer.java @@ -113,8 +113,13 @@ private void waitUntil(Predicate predicate, long expiry, int times) /** * Wait until Docker closes the stream of output. */ - public void waitUntilEnd() throws TimeoutException { - waitUntilEnd(Long.MAX_VALUE); + public void waitUntilEnd() { + try { + waitUntilEnd(Long.MAX_VALUE); + } catch (TimeoutException e) { + // timeout condition can never occur in a realistic timeframe + throw new IllegalStateException(e); + } } /** diff --git a/core/src/main/java/org/testcontainers/utility/LogUtils.java b/core/src/main/java/org/testcontainers/utility/LogUtils.java index 100a79a8395..b51325cdda7 100644 --- a/core/src/main/java/org/testcontainers/utility/LogUtils.java +++ b/core/src/main/java/org/testcontainers/utility/LogUtils.java @@ -2,15 +2,14 @@ import com.github.dockerjava.api.DockerClient; import com.github.dockerjava.api.command.LogContainerCmd; -import com.github.dockerjava.api.model.AuthConfig; -import com.google.common.base.MoreObjects; import lombok.experimental.UtilityClass; import org.testcontainers.containers.output.FrameConsumerResultCallback; import org.testcontainers.containers.output.OutputFrame; +import org.testcontainers.containers.output.ToStringConsumer; +import org.testcontainers.containers.output.WaitingConsumer; import java.util.function.Consumer; -import static com.google.common.base.Strings.isNullOrEmpty; import static org.testcontainers.containers.output.OutputFrame.OutputType.STDERR; import static org.testcontainers.containers.output.OutputFrame.OutputType.STDOUT; @@ -19,15 +18,76 @@ */ @UtilityClass public class LogUtils { + + /** + * Attach a log consumer to a container's log outputs in follow mode. The consumer will receive all previous + * and all future log frames of the specified type(s). + * + * @param dockerClient a Docker client + * @param containerId container ID to attach to + * @param consumer a consumer of {@link OutputFrame}s + * @param types types of {@link OutputFrame} to receive + */ + public void followOutput(DockerClient dockerClient, + String containerId, + Consumer consumer, + OutputFrame.OutputType... types) { + + attachConsumer(dockerClient, containerId, consumer, true, types); + } + + /** + * Attach a log consumer to a container's log outputs in follow mode. The consumer will receive all previous + * and all future log frames (both stdout and stderr). + * + * @param dockerClient a Docker client + * @param containerId container ID to attach to + * @param consumer a consumer of {@link OutputFrame}s + */ + public void followOutput(DockerClient dockerClient, + String containerId, + Consumer consumer) { + + followOutput(dockerClient, containerId, consumer, STDOUT, STDERR); + } + /** - * {@inheritDoc} + * Retrieve all previous log outputs for a container of the specified type(s). + * + * @param dockerClient a Docker client + * @param containerId container ID to attach to + * @param types types of {@link OutputFrame} to receive + * @return all previous output frames (stdout/stderr being separated by newline characters) */ - public void followOutput(DockerClient dockerClient, String containerId, - Consumer consumer, OutputFrame.OutputType... types) { + public String getOutput(DockerClient dockerClient, + String containerId, + OutputFrame.OutputType... types) { + + if (containerId == null) { + return ""; + } + + if (types.length == 0) { + types = new OutputFrame.OutputType[] { STDOUT, STDERR }; + } + + final ToStringConsumer consumer = new ToStringConsumer(); + final WaitingConsumer wait = new WaitingConsumer(); + attachConsumer(dockerClient, containerId, consumer.andThen(wait), false, types); + + wait.waitUntilEnd(); + return consumer.toUtf8String(); + } + + private static void attachConsumer(DockerClient dockerClient, + String containerId, + Consumer consumer, + boolean followStream, + OutputFrame.OutputType... types) { final LogContainerCmd cmd = dockerClient.logContainerCmd(containerId) - .withFollowStream(true) - .withSince(0); + .withFollowStream(followStream) + .withSince(0); final FrameConsumerResultCallback callback = new FrameConsumerResultCallback(); for (OutputFrame.OutputType type : types) { @@ -38,9 +98,4 @@ public void followOutput(DockerClient dockerClient, String containerId, cmd.exec(callback); } - - public void followOutput(DockerClient dockerClient, String containerId, Consumer consumer) { - followOutput(dockerClient, containerId, consumer, STDOUT, STDERR); - } - } diff --git a/core/src/test/java/org/testcontainers/containers/output/ContainerLogsTest.java b/core/src/test/java/org/testcontainers/containers/output/ContainerLogsTest.java new file mode 100644 index 00000000000..f95d5a06850 --- /dev/null +++ b/core/src/test/java/org/testcontainers/containers/output/ContainerLogsTest.java @@ -0,0 +1,72 @@ +package org.testcontainers.containers.output; + +import org.junit.Test; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.startupcheck.OneShotStartupCheckStrategy; + +import static org.rnorth.visibleassertions.VisibleAssertions.assertEquals; +import static org.rnorth.visibleassertions.VisibleAssertions.assertTrue; +import static org.testcontainers.containers.output.OutputFrame.OutputType.STDERR; +import static org.testcontainers.containers.output.OutputFrame.OutputType.STDOUT; + +public class ContainerLogsTest { + + @Test + public void getLogsReturnsAllLogsToDate() { + try (GenericContainer container = shortLivedContainer()) { + container.start(); + + // docsGetAllLogs { + final String logs = container.getLogs(); + // } + assertEquals("stdout and stderr are reflected in the returned logs", "stdout\nstderr", logs); + } + } + + @Test + public void getLogsReturnsStdOutToDate() { + try (GenericContainer container = shortLivedContainer()) { + container.start(); + + // docsGetStdOut { + final String logs = container.getLogs(STDOUT); + // } + assertEquals("stdout and stderr are reflected in the returned logs", "stdout", logs); + } + } + + @Test + public void getLogsReturnsStdErrToDate() { + try (GenericContainer container = shortLivedContainer()) { + container.start(); + + // docsGetStdErr { + final String logs = container.getLogs(STDERR); + // } + assertEquals("stdout and stderr are reflected in the returned logs", "stderr", logs); + } + } + + @Test + public void getLogsForLongRunningContainer() throws InterruptedException { + try (GenericContainer container = longRunningContainer()) { + container.start(); + + Thread.sleep(1000L); + + final String logs = container.getLogs(STDOUT); + assertTrue("stdout is reflected in the returned logs for a running container", logs.contains("seq=0")); + } + } + + private static GenericContainer shortLivedContainer() { + return new GenericContainer("alpine:3.3") + .withCommand("/bin/sh", "-c", "echo -n 'stdout' && echo -n 'stderr' 1>&2") + .withStartupCheckStrategy(new OneShotStartupCheckStrategy()); + } + + private static GenericContainer longRunningContainer() { + return new GenericContainer("alpine:3.3") + .withCommand("ping -c 100 127.0.0.1"); + } +} diff --git a/core/src/test/java/org/testcontainers/junit/WorkingDirectoryTest.java b/core/src/test/java/org/testcontainers/junit/WorkingDirectoryTest.java index 1118302f701..30230cc12ff 100644 --- a/core/src/test/java/org/testcontainers/junit/WorkingDirectoryTest.java +++ b/core/src/test/java/org/testcontainers/junit/WorkingDirectoryTest.java @@ -3,13 +3,8 @@ import org.junit.ClassRule; import org.junit.Test; import org.testcontainers.containers.GenericContainer; -import org.testcontainers.containers.output.OutputFrame; -import org.testcontainers.containers.output.ToStringConsumer; -import org.testcontainers.containers.output.WaitingConsumer; import org.testcontainers.containers.startupcheck.OneShotStartupCheckStrategy; -import java.util.function.Consumer; - import static org.rnorth.visibleassertions.VisibleAssertions.assertTrue; /** @@ -17,20 +12,15 @@ */ public class WorkingDirectoryTest { - private static WaitingConsumer waitingConsumer = new WaitingConsumer(); - private static ToStringConsumer toStringConsumer = new ToStringConsumer(); - private static Consumer compositeConsumer = waitingConsumer.andThen(toStringConsumer); - @ClassRule public static GenericContainer container = new GenericContainer("alpine:3.2") .withWorkingDirectory("/etc") .withStartupCheckStrategy(new OneShotStartupCheckStrategy()) - .withCommand("ls", "-al") - .withLogConsumer(compositeConsumer); + .withCommand("ls", "-al"); @Test public void checkOutput() { - String listing = toStringConsumer.toUtf8String(); + String listing = container.getLogs(); assertTrue("Directory listing contains expected /etc content", listing.contains("hostname")); assertTrue("Directory listing contains expected /etc content", listing.contains("init.d")); diff --git a/core/src/test/java/org/testcontainers/utility/DirectoryTarResourceTest.java b/core/src/test/java/org/testcontainers/utility/DirectoryTarResourceTest.java index b73bfeed46e..3789a654c20 100644 --- a/core/src/test/java/org/testcontainers/utility/DirectoryTarResourceTest.java +++ b/core/src/test/java/org/testcontainers/utility/DirectoryTarResourceTest.java @@ -5,15 +5,11 @@ import org.hamcrest.core.IsCollectionContaining; import org.junit.Test; import org.testcontainers.containers.GenericContainer; -import org.testcontainers.containers.output.ToStringConsumer; -import org.testcontainers.containers.output.WaitingConsumer; import org.testcontainers.containers.startupcheck.OneShotStartupCheckStrategy; import org.testcontainers.images.builder.ImageFromDockerfile; import java.io.File; import java.util.Arrays; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; import static org.hamcrest.CoreMatchers.allOf; import static org.hamcrest.CoreMatchers.containsString; @@ -23,12 +19,7 @@ public class DirectoryTarResourceTest { @Test - public void simpleRecursiveFileTest() throws TimeoutException { - - WaitingConsumer wait = new WaitingConsumer(); - - final ToStringConsumer toString = new ToStringConsumer(); - + public void simpleRecursiveFileTest() { // 'src' is expected to be the project base directory, so all source code/resources should be copied in File directory = new File("src"); @@ -40,24 +31,17 @@ public void simpleRecursiveFileTest() throws TimeoutException { .cmd("cat /foo/test/resources/test-recursive-file.txt") .build() ).withFileFromFile("/tmp/foo", directory)) - .withStartupCheckStrategy(new OneShotStartupCheckStrategy()) - .withLogConsumer(wait.andThen(toString)); + .withStartupCheckStrategy(new OneShotStartupCheckStrategy()); container.start(); - wait.waitUntilEnd(60, TimeUnit.SECONDS); - final String results = toString.toUtf8String(); + final String results = container.getLogs(); assertTrue("The container has a file that was copied in via a recursive copy", results.contains("Used for DirectoryTarResourceTest")); } @Test - public void simpleRecursiveFileWithPermissionTest() throws TimeoutException { - - WaitingConsumer wait = new WaitingConsumer(); - - final ToStringConsumer toString = new ToStringConsumer(); - + public void simpleRecursiveFileWithPermissionTest() { GenericContainer container = new GenericContainer( new ImageFromDockerfile() .withDockerfileFromBuilder(builder -> @@ -67,13 +51,10 @@ public void simpleRecursiveFileWithPermissionTest() throws TimeoutException { .build() ).withFileFromFile("/tmp/foo", new File("/mappable-resource/test-resource.txt"), 0754)) - .withStartupCheckStrategy(new OneShotStartupCheckStrategy()) - .withLogConsumer(wait.andThen(toString)); + .withStartupCheckStrategy(new OneShotStartupCheckStrategy()); container.start(); - wait.waitUntilEnd(60, TimeUnit.SECONDS); - - String listing = toString.toUtf8String(); + String listing = container.getLogs(); assertThat("Listing shows that file is copied with mode requested.", Arrays.asList(listing.split("\\n")), @@ -81,13 +62,9 @@ public void simpleRecursiveFileWithPermissionTest() throws TimeoutException { } @Test - public void simpleRecursiveClasspathResourceTest() throws TimeoutException { + public void simpleRecursiveClasspathResourceTest() { // This test combines the copying of classpath resources from JAR files with the recursive TAR approach, to allow JARed classpath resources to be copied in to an image - WaitingConsumer wait = new WaitingConsumer(); - - final ToStringConsumer toString = new ToStringConsumer(); - GenericContainer container = new GenericContainer( new ImageFromDockerfile() .withDockerfileFromBuilder(builder -> @@ -96,13 +73,11 @@ public void simpleRecursiveClasspathResourceTest() throws TimeoutException { .cmd("ls -lRt /foo") .build() ).withFileFromClasspath("/tmp/foo", "/recursive/dir")) // here we use /org/junit as a directory that really should exist on the classpath - .withStartupCheckStrategy(new OneShotStartupCheckStrategy()) - .withLogConsumer(wait.andThen(toString)); + .withStartupCheckStrategy(new OneShotStartupCheckStrategy()); container.start(); - wait.waitUntilEnd(60, TimeUnit.SECONDS); - final String results = toString.toUtf8String(); + final String results = container.getLogs(); // ExternalResource.class is known to exist in a subdirectory of /org/junit so should be successfully copied in assertTrue("The container has a file that was copied in via a recursive copy from a JAR resource", results.contains("content.txt")); diff --git a/docs/features/container_logs.md b/docs/features/container_logs.md index 389dc57c631..a9e2832eb70 100644 --- a/docs/features/container_logs.md +++ b/docs/features/container_logs.md @@ -1,13 +1,34 @@ # Accessing container logs -It is possible to capture container output using the `followOutput()` method. This method accepts a Consumer and (optionally) +It is possible to capture container output using: + + * the `getLogs()` method, which simply returns a `String` snapshot of a container's entire log output + * the `followOutput()` method. This method accepts a Consumer and (optionally) a varargs list stating which of STDOUT, STDERR, or both, should be followed. If not specified, both will be followed. At present, container output will always begin from the time of container creation. -Testcontainers includes some out-of-the-box Consumer implementations that can be used; examples follow. +## Reading all logs (from startup time to present) -## Streaming container output to an SLF4J logger +`getLogs()` is the simplest mechanism for accessing container logs, and can be used as follows: + + +[Accessing all output (stdout and stderr)](../../core/src/test/java/org/testcontainers/containers/output/ContainerLogsTest.java) inside_block:docsGetAllLogs + + + +[Accessing just stdout](../../core/src/test/java/org/testcontainers/containers/output/ContainerLogsTest.java) inside_block:docsGetStdOut + + + +[Accessing just stderr](../../core/src/test/java/org/testcontainers/containers/output/ContainerLogsTest.java) inside_block:docsGetStdErr + + +## Streaming logs + +Testcontainers includes some out-of-the-box Consumer implementations that can be used with the streaming `followOutput()` model; examples follow. + +### Streaming container output to an SLF4J logger Given an existing SLF4J logger instance named LOGGER: ```java @@ -15,7 +36,10 @@ Slf4jLogConsumer logConsumer = new Slf4jLogConsumer(LOGGER); container.followOutput(logConsumer); ``` -## Capturing container output as a String +### Capturing container output as a String + +To stream logs live or customize the decoding, `ToStringConsumer` may be used: + ```java ToStringConsumer toStringConsumer = new ToStringConsumer(); container.followOutput(toStringConsumer, OutputType.STDOUT); @@ -26,7 +50,7 @@ String utf8String = toStringConsumer.toUtf8String(); String otherString = toStringConsumer.toString(CharSet.forName("ISO-8859-1")); ``` -## Waiting for container output to contain expected content +### Waiting for container output to contain expected content `WaitingConsumer` will block until a frame of container output (usually a line) matches a provided predicate.