Skip to content

Commit 2c5f274

Browse files
authored
Add getLogs() to GenericContainer, allowing all logs to be retrieved (#1206)
Fixes #1205
1 parent 6725230 commit 2c5f274

File tree

9 files changed

+216
-86
lines changed

9 files changed

+216
-86
lines changed

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ jobs:
2222

2323
- stage: test
2424
env: [ NAME=core ]
25-
jdk: openjdk-ea
25+
jdk: openjdk12
2626
script: ./gradlew testcontainers:check --scan --no-daemon
2727

2828
- env: [ NAME=selenium ]

core/src/main/java/org/testcontainers/containers/ContainerState.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import com.github.dockerjava.api.model.Ports;
88
import com.google.common.base.Preconditions;
99
import org.testcontainers.DockerClientFactory;
10+
import org.testcontainers.containers.output.OutputFrame;
11+
import org.testcontainers.utility.LogUtils;
1012

1113
import java.util.ArrayList;
1214
import java.util.List;
@@ -149,6 +151,22 @@ default List<Integer> getBoundPortNumbers() {
149151
.collect(Collectors.toList());
150152
}
151153

154+
155+
/**
156+
* @return all log output from the container from start until the current instant (both stdout and stderr)
157+
*/
158+
default String getLogs() {
159+
return LogUtils.getOutput(DockerClientFactory.instance().client(), getContainerId());
160+
}
161+
162+
/**
163+
* @param types log types to return
164+
* @return all log output from the container from start until the current instant
165+
*/
166+
default String getLogs(OutputFrame.OutputType... types) {
167+
return LogUtils.getOutput(DockerClientFactory.instance().client(), getContainerId(), types);
168+
}
169+
152170
/**
153171
* @return the id of the container
154172
*/

core/src/main/java/org/testcontainers/containers/GenericContainer.java

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,7 @@
2020
import org.rnorth.visibleassertions.VisibleAssertions;
2121
import org.slf4j.Logger;
2222
import org.testcontainers.DockerClientFactory;
23-
import org.testcontainers.containers.output.FrameConsumerResultCallback;
2423
import org.testcontainers.containers.output.OutputFrame;
25-
import org.testcontainers.containers.output.Slf4jLogConsumer;
2624
import org.testcontainers.containers.startupcheck.IsRunningStartupCheckStrategy;
2725
import org.testcontainers.containers.startupcheck.MinimumDurationRunningStartupCheckStrategy;
2826
import org.testcontainers.containers.startupcheck.StartupCheckStrategy;
@@ -50,8 +48,6 @@
5048
import java.util.stream.Stream;
5149

5250
import static com.google.common.collect.Lists.newArrayList;
53-
import static org.testcontainers.containers.output.OutputFrame.OutputType.STDERR;
54-
import static org.testcontainers.containers.output.OutputFrame.OutputType.STDOUT;
5551
import static org.testcontainers.utility.CommandLine.runShellCommand;
5652

5753
/**
@@ -242,24 +238,24 @@ private void tryStart() {
242238
logger().info("Starting container with ID: {}", containerId);
243239
dockerClient.startContainerCmd(containerId).exec();
244240

241+
logger().info("Container {} is starting: {}", dockerImageName, containerId);
242+
245243
// For all registered output consumers, start following as close to container startup as possible
246244
this.logConsumers.forEach(this::followOutput);
247245

248-
logger().info("Container {} is starting: {}", dockerImageName, containerId);
249-
250246
// Tell subclasses that we're starting
251247
containerInfo = dockerClient.inspectContainerCmd(containerId).exec();
252248
containerName = containerInfo.getName();
253249
containerIsStarting(containerInfo);
254250

255-
// Wait until the container is running (may not be fully started)
256-
251+
// Wait until the container has reached the desired running state
257252
if (!this.startupCheckStrategy.waitUntilStartupSuccessful(dockerClient, containerId)) {
258253
// Bail out, don't wait for the port to start listening.
259254
// (Exception thrown here will be caught below and wrapped)
260255
throw new IllegalStateException("Container did not start correctly.");
261256
}
262257

258+
// Wait until the process within the container has become ready for use (e.g. listening on network, log message emitted, etc).
263259
waitUntilContainerStarted();
264260

265261
logger().info("Container {} started", dockerImageName);
@@ -269,17 +265,12 @@ private void tryStart() {
269265

270266
if (containerId != null) {
271267
// Log output if startup failed, either due to a container failure or exception (including timeout)
272-
logger().error("Container log output (if any) will follow:");
273-
FrameConsumerResultCallback resultCallback = new FrameConsumerResultCallback();
274-
resultCallback.addConsumer(STDOUT, new Slf4jLogConsumer(logger()));
275-
resultCallback.addConsumer(STDERR, new Slf4jLogConsumer(logger()));
276-
dockerClient.logContainerCmd(containerId).withStdOut(true).withStdErr(true).exec(resultCallback);
277-
278-
// Try to ensure that container log output is shown before proceeding
279-
try {
280-
resultCallback.getCompletionLatch().await(1, TimeUnit.MINUTES);
281-
} catch (InterruptedException ignored) {
282-
// Cannot do anything at this point
268+
final String containerLogs = getLogs();
269+
270+
if (containerLogs.length() > 0) {
271+
logger().error("Log output from the failed container:\n{}", getLogs());
272+
} else {
273+
logger().error("There are no stdout/stderr logs available for the failed container");
283274
}
284275
}
285276

core/src/main/java/org/testcontainers/containers/output/WaitingConsumer.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,13 @@ private void waitUntil(Predicate<OutputFrame> predicate, long expiry, int times)
113113
/**
114114
* Wait until Docker closes the stream of output.
115115
*/
116-
public void waitUntilEnd() throws TimeoutException {
117-
waitUntilEnd(Long.MAX_VALUE);
116+
public void waitUntilEnd() {
117+
try {
118+
waitUntilEnd(Long.MAX_VALUE);
119+
} catch (TimeoutException e) {
120+
// timeout condition can never occur in a realistic timeframe
121+
throw new IllegalStateException(e);
122+
}
118123
}
119124

120125
/**

core/src/main/java/org/testcontainers/utility/LogUtils.java

Lines changed: 68 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,14 @@
22

33
import com.github.dockerjava.api.DockerClient;
44
import com.github.dockerjava.api.command.LogContainerCmd;
5-
import com.github.dockerjava.api.model.AuthConfig;
6-
import com.google.common.base.MoreObjects;
75
import lombok.experimental.UtilityClass;
86
import org.testcontainers.containers.output.FrameConsumerResultCallback;
97
import org.testcontainers.containers.output.OutputFrame;
8+
import org.testcontainers.containers.output.ToStringConsumer;
9+
import org.testcontainers.containers.output.WaitingConsumer;
1010

1111
import java.util.function.Consumer;
1212

13-
import static com.google.common.base.Strings.isNullOrEmpty;
1413
import static org.testcontainers.containers.output.OutputFrame.OutputType.STDERR;
1514
import static org.testcontainers.containers.output.OutputFrame.OutputType.STDOUT;
1615

@@ -19,15 +18,76 @@
1918
*/
2019
@UtilityClass
2120
public class LogUtils {
21+
22+
/**
23+
* Attach a log consumer to a container's log outputs in follow mode. The consumer will receive all previous
24+
* and all future log frames of the specified type(s).
25+
*
26+
* @param dockerClient a Docker client
27+
* @param containerId container ID to attach to
28+
* @param consumer a consumer of {@link OutputFrame}s
29+
* @param types types of {@link OutputFrame} to receive
30+
*/
31+
public void followOutput(DockerClient dockerClient,
32+
String containerId,
33+
Consumer<OutputFrame> consumer,
34+
OutputFrame.OutputType... types) {
35+
36+
attachConsumer(dockerClient, containerId, consumer, true, types);
37+
}
38+
39+
/**
40+
* Attach a log consumer to a container's log outputs in follow mode. The consumer will receive all previous
41+
* and all future log frames (both stdout and stderr).
42+
*
43+
* @param dockerClient a Docker client
44+
* @param containerId container ID to attach to
45+
* @param consumer a consumer of {@link OutputFrame}s
46+
*/
47+
public void followOutput(DockerClient dockerClient,
48+
String containerId,
49+
Consumer<OutputFrame> consumer) {
50+
51+
followOutput(dockerClient, containerId, consumer, STDOUT, STDERR);
52+
}
53+
2254
/**
23-
* {@inheritDoc}
55+
* Retrieve all previous log outputs for a container of the specified type(s).
56+
*
57+
* @param dockerClient a Docker client
58+
* @param containerId container ID to attach to
59+
* @param types types of {@link OutputFrame} to receive
60+
* @return all previous output frames (stdout/stderr being separated by newline characters)
2461
*/
25-
public void followOutput(DockerClient dockerClient, String containerId,
26-
Consumer<OutputFrame> consumer, OutputFrame.OutputType... types) {
62+
public String getOutput(DockerClient dockerClient,
63+
String containerId,
64+
OutputFrame.OutputType... types) {
65+
66+
if (containerId == null) {
67+
return "";
68+
}
69+
70+
if (types.length == 0) {
71+
types = new OutputFrame.OutputType[] { STDOUT, STDERR };
72+
}
73+
74+
final ToStringConsumer consumer = new ToStringConsumer();
75+
final WaitingConsumer wait = new WaitingConsumer();
76+
attachConsumer(dockerClient, containerId, consumer.andThen(wait), false, types);
77+
78+
wait.waitUntilEnd();
79+
return consumer.toUtf8String();
80+
}
81+
82+
private static void attachConsumer(DockerClient dockerClient,
83+
String containerId,
84+
Consumer<OutputFrame> consumer,
85+
boolean followStream,
86+
OutputFrame.OutputType... types) {
2787

2888
final LogContainerCmd cmd = dockerClient.logContainerCmd(containerId)
29-
.withFollowStream(true)
30-
.withSince(0);
89+
.withFollowStream(followStream)
90+
.withSince(0);
3191

3292
final FrameConsumerResultCallback callback = new FrameConsumerResultCallback();
3393
for (OutputFrame.OutputType type : types) {
@@ -38,9 +98,4 @@ public void followOutput(DockerClient dockerClient, String containerId,
3898

3999
cmd.exec(callback);
40100
}
41-
42-
public void followOutput(DockerClient dockerClient, String containerId, Consumer<OutputFrame> consumer) {
43-
followOutput(dockerClient, containerId, consumer, STDOUT, STDERR);
44-
}
45-
46101
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package org.testcontainers.containers.output;
2+
3+
import org.junit.Test;
4+
import org.testcontainers.containers.GenericContainer;
5+
import org.testcontainers.containers.startupcheck.OneShotStartupCheckStrategy;
6+
7+
import static org.rnorth.visibleassertions.VisibleAssertions.assertEquals;
8+
import static org.rnorth.visibleassertions.VisibleAssertions.assertTrue;
9+
import static org.testcontainers.containers.output.OutputFrame.OutputType.STDERR;
10+
import static org.testcontainers.containers.output.OutputFrame.OutputType.STDOUT;
11+
12+
public class ContainerLogsTest {
13+
14+
@Test
15+
public void getLogsReturnsAllLogsToDate() {
16+
try (GenericContainer container = shortLivedContainer()) {
17+
container.start();
18+
19+
// docsGetAllLogs {
20+
final String logs = container.getLogs();
21+
// }
22+
assertEquals("stdout and stderr are reflected in the returned logs", "stdout\nstderr", logs);
23+
}
24+
}
25+
26+
@Test
27+
public void getLogsReturnsStdOutToDate() {
28+
try (GenericContainer container = shortLivedContainer()) {
29+
container.start();
30+
31+
// docsGetStdOut {
32+
final String logs = container.getLogs(STDOUT);
33+
// }
34+
assertEquals("stdout and stderr are reflected in the returned logs", "stdout", logs);
35+
}
36+
}
37+
38+
@Test
39+
public void getLogsReturnsStdErrToDate() {
40+
try (GenericContainer container = shortLivedContainer()) {
41+
container.start();
42+
43+
// docsGetStdErr {
44+
final String logs = container.getLogs(STDERR);
45+
// }
46+
assertEquals("stdout and stderr are reflected in the returned logs", "stderr", logs);
47+
}
48+
}
49+
50+
@Test
51+
public void getLogsForLongRunningContainer() throws InterruptedException {
52+
try (GenericContainer container = longRunningContainer()) {
53+
container.start();
54+
55+
Thread.sleep(1000L);
56+
57+
final String logs = container.getLogs(STDOUT);
58+
assertTrue("stdout is reflected in the returned logs for a running container", logs.contains("seq=0"));
59+
}
60+
}
61+
62+
private static GenericContainer shortLivedContainer() {
63+
return new GenericContainer("alpine:3.3")
64+
.withCommand("/bin/sh", "-c", "echo -n 'stdout' && echo -n 'stderr' 1>&2")
65+
.withStartupCheckStrategy(new OneShotStartupCheckStrategy());
66+
}
67+
68+
private static GenericContainer longRunningContainer() {
69+
return new GenericContainer("alpine:3.3")
70+
.withCommand("ping -c 100 127.0.0.1");
71+
}
72+
}

core/src/test/java/org/testcontainers/junit/WorkingDirectoryTest.java

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,34 +3,24 @@
33
import org.junit.ClassRule;
44
import org.junit.Test;
55
import org.testcontainers.containers.GenericContainer;
6-
import org.testcontainers.containers.output.OutputFrame;
7-
import org.testcontainers.containers.output.ToStringConsumer;
8-
import org.testcontainers.containers.output.WaitingConsumer;
96
import org.testcontainers.containers.startupcheck.OneShotStartupCheckStrategy;
107

11-
import java.util.function.Consumer;
12-
138
import static org.rnorth.visibleassertions.VisibleAssertions.assertTrue;
149

1510
/**
1611
* Created by rnorth on 26/07/2016.
1712
*/
1813
public class WorkingDirectoryTest {
1914

20-
private static WaitingConsumer waitingConsumer = new WaitingConsumer();
21-
private static ToStringConsumer toStringConsumer = new ToStringConsumer();
22-
private static Consumer<OutputFrame> compositeConsumer = waitingConsumer.andThen(toStringConsumer);
23-
2415
@ClassRule
2516
public static GenericContainer container = new GenericContainer("alpine:3.2")
2617
.withWorkingDirectory("/etc")
2718
.withStartupCheckStrategy(new OneShotStartupCheckStrategy())
28-
.withCommand("ls", "-al")
29-
.withLogConsumer(compositeConsumer);
19+
.withCommand("ls", "-al");
3020

3121
@Test
3222
public void checkOutput() {
33-
String listing = toStringConsumer.toUtf8String();
23+
String listing = container.getLogs();
3424

3525
assertTrue("Directory listing contains expected /etc content", listing.contains("hostname"));
3626
assertTrue("Directory listing contains expected /etc content", listing.contains("init.d"));

0 commit comments

Comments
 (0)