Skip to content

Commit

Permalink
Add getLogs() to GenericContainer, allowing all logs to be retrieved (#…
Browse files Browse the repository at this point in the history
…1206)

Fixes #1205
  • Loading branch information
rnorth committed Mar 24, 2019
1 parent 6725230 commit 2c5f274
Show file tree
Hide file tree
Showing 9 changed files with 216 additions and 86 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Expand Up @@ -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 ]
Expand Down
Expand Up @@ -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;
Expand Down Expand Up @@ -149,6 +151,22 @@ default List<Integer> 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
*/
Expand Down
Expand Up @@ -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;
Expand Down Expand Up @@ -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;

/**
Expand Down Expand Up @@ -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);
Expand All @@ -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");
}
}

Expand Down
Expand Up @@ -113,8 +113,13 @@ private void waitUntil(Predicate<OutputFrame> 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);
}
}

/**
Expand Down
81 changes: 68 additions & 13 deletions core/src/main/java/org/testcontainers/utility/LogUtils.java
Expand Up @@ -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;

Expand All @@ -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<OutputFrame> 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<OutputFrame> 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<OutputFrame> 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<OutputFrame> 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) {
Expand All @@ -38,9 +98,4 @@ public void followOutput(DockerClient dockerClient, String containerId,

cmd.exec(callback);
}

public void followOutput(DockerClient dockerClient, String containerId, Consumer<OutputFrame> consumer) {
followOutput(dockerClient, containerId, consumer, STDOUT, STDERR);
}

}
@@ -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");
}
}
Expand Up @@ -3,34 +3,24 @@
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;

/**
* Created by rnorth on 26/07/2016.
*/
public class WorkingDirectoryTest {

private static WaitingConsumer waitingConsumer = new WaitingConsumer();
private static ToStringConsumer toStringConsumer = new ToStringConsumer();
private static Consumer<OutputFrame> 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"));
Expand Down

0 comments on commit 2c5f274

Please sign in to comment.