From f2d1f8e8ac055fa1a8f75e59c8e0d2361a00ee6a Mon Sep 17 00:00:00 2001 From: Volkov Sergei Date: Thu, 29 Jan 2026 12:10:26 +0300 Subject: [PATCH] Adds opt-in log capture via withLogCapture() and getCapturedLogs(). This allows retrieving container output even when startup fails or the container exits quickly, where getLogs() may return empty output. Fixes #11215" --- .../containers/GenericContainer.java | 34 +++++++ .../GenericContainerLogCaptureTest.java | 97 +++++++++++++++++++ 2 files changed, 131 insertions(+) create mode 100644 core/src/test/java/org/testcontainers/containers/GenericContainerLogCaptureTest.java diff --git a/core/src/main/java/org/testcontainers/containers/GenericContainer.java b/core/src/main/java/org/testcontainers/containers/GenericContainer.java index 0fe944433ae..c9528c46810 100644 --- a/core/src/main/java/org/testcontainers/containers/GenericContainer.java +++ b/core/src/main/java/org/testcontainers/containers/GenericContainer.java @@ -37,6 +37,7 @@ import org.testcontainers.DockerClientFactory; import org.testcontainers.UnstableAPI; import org.testcontainers.containers.output.OutputFrame; +import org.testcontainers.containers.output.ToStringConsumer; import org.testcontainers.containers.startupcheck.IsRunningStartupCheckStrategy; import org.testcontainers.containers.startupcheck.MinimumDurationRunningStartupCheckStrategy; import org.testcontainers.containers.startupcheck.StartupCheckStrategy; @@ -184,6 +185,11 @@ public class GenericContainer> private List> logConsumers = new ArrayList<>(); + /** + * In-memory log buffer used by {@link #withLogCapture()}. + */ + private ToStringConsumer capturedLogsConsumer; + private static final Set AVAILABLE_IMAGE_NAME_CACHE = new HashSet<>(); @Nullable @@ -1362,6 +1368,34 @@ public SELF withLogConsumer(Consumer consumer) { return self(); } + /** + * Enables in-memory capture of container logs. + *

+ * This is useful for debugging startup failures or containers that exit quickly, + * where {@link #getLogs()} may return empty output. + * + * @return this container instance + */ + public SELF withLogCapture() { + if (capturedLogsConsumer == null) { + capturedLogsConsumer = new ToStringConsumer(); + withLogConsumer(capturedLogsConsumer); + } + return self(); + } + + /** + * Returns logs captured in memory when {@link #withLogCapture()} is enabled. + *

+ * This is useful for debugging startup failures or containers that exit quickly, + * where {@link #getLogs()} may return empty output. + * + * @return captured container output (stdout and stderr), or an empty string if log capture is not enabled + */ + public String getCapturedLogs() { + return capturedLogsConsumer != null ? capturedLogsConsumer.toUtf8String() : ""; + } + /** * {@inheritDoc} */ diff --git a/core/src/test/java/org/testcontainers/containers/GenericContainerLogCaptureTest.java b/core/src/test/java/org/testcontainers/containers/GenericContainerLogCaptureTest.java new file mode 100644 index 00000000000..dc5ad67fc5a --- /dev/null +++ b/core/src/test/java/org/testcontainers/containers/GenericContainerLogCaptureTest.java @@ -0,0 +1,97 @@ +package org.testcontainers.containers; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.startupcheck.OneShotStartupCheckStrategy; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.utility.DockerImageName; + +import java.time.Duration; + +/** + * Tests for in-memory log capture in {@link GenericContainer}. + *

+ * These tests verify that container logs can be captured even when container startup fails or the + * container exits immediately after producing output. + */ +class GenericContainerLogCaptureTest { + + private static final DockerImageName TEST_IMAGE = + DockerImageName.parse("busybox:1.36"); + + private static final String CONTAINER_LOG_MESSAGE = + "container startup output"; + + /** + * A log pattern that is intentionally never produced by the container. Used to force startup + * failure via {@link Wait#forLogMessage(String, int)}. + */ + private static final String NON_MATCHING_LOG_PATTERN = + "this-log-message-will-never-appear"; + + private static final Duration STARTUP_TIMEOUT = + Duration.ofSeconds(2); + + @Test + void shouldNotCaptureLogsByDefaultWhenStartupFails() { + try (GenericContainer container = createFailingContainer()) { + + Assertions.assertThrows( + ContainerLaunchException.class, + container::start, + "Container startup should fail due to unmet wait condition" + ); + + Assertions.assertTrue( + container.getCapturedLogs().isEmpty(), + "Captured logs should be empty when log capture is not enabled" + ); + } + } + + @Test + void shouldCaptureLogsWhenEnabledEvenIfStartupFails() { + try (GenericContainer container = + createFailingContainer().withLogCapture()) { + + Assertions.assertThrows( + ContainerLaunchException.class, + container::start, + "Container startup should fail due to unmet wait condition" + ); + + String capturedLogs = container.getCapturedLogs(); + + Assertions.assertFalse( + capturedLogs.isEmpty(), + "Captured logs should not be empty when log capture is enabled" + ); + + Assertions.assertTrue( + capturedLogs.contains(CONTAINER_LOG_MESSAGE), + "Captured logs should contain the container output" + ); + } + } + + /** + * Creates a container configuration that: + *

    + *
  • Produces a single line of output
  • + *
  • Exits immediately
  • + *
  • Fails startup due to an unmet log-based wait condition
  • + *
+ * + * @return a configured {@link GenericContainer} instance + */ + private static GenericContainer createFailingContainer() { + return new GenericContainer<>(TEST_IMAGE) + .withStartupCheckStrategy(new OneShotStartupCheckStrategy()) + .waitingFor( + Wait.forLogMessage(NON_MATCHING_LOG_PATTERN, 1) + .withStartupTimeout(STARTUP_TIMEOUT) + ) + .withCommand("sh", "-c", "echo \"" + CONTAINER_LOG_MESSAGE + "\""); + } +} +