or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

configuration.mdcontainer-lifecycle.mddocker-client.mddocker-compose.mdimage-building.mdimage-management.mdimage-pull-policies.mdindex.mdjunit-jupiter-integration.mdlifecycle.mdmodules-overview.mdnetwork-configuration.mdoutput-handling.mdstartup-checks.mdutility-classes.mdwait-strategies.md
tile.json

output-handling.mddocs/

Output Handling

Log and output stream management for containers. Provides multiple consumer implementations for capturing, processing, and analyzing container output streams with support for ANSI code filtering and various output sinks.

Capabilities

OutputFrame

Represents a single frame of container output with timestamp and type information.

/**
 * Represents a frame of output from a container.
 * Contains the output bytes, type (stdout/stderr), and timestamp.
 */
public class OutputFrame {
    /**
     * Constructor for creating an OutputFrame from output type and bytes.
     *
     * @param type the output type (STDOUT, STDERR, or END)
     * @param bytes the output bytes
     */
    public OutputFrame(OutputType type, byte[] bytes);

    /**
     * Create an OutputFrame from a docker-java Frame.
     *
     * @param frame the docker-java Frame object
     * @return OutputFrame instance
     */
    public static OutputFrame forFrame(Frame frame);

    /**
     * Get the output as a UTF-8 string.
     *
     * @return output as UTF-8 string
     */
    public String getUtf8String();

    /**
     * Get the output as a UTF-8 string without the trailing line ending.
     *
     * @return output without line ending
     */
    public String getUtf8StringWithoutLineEnding();

    /**
     * Get the raw output bytes.
     *
     * @return raw byte array
     */
    public byte[] getBytes();

    /**
     * Get the type of output (stdout, stderr, or end marker).
     *
     * @return output type
     */
    public OutputType getType();

    /**
     * Static constant representing end of stream marker.
     * Use this to check for or signal stream completion.
     */
    public static final OutputFrame END;

    /**
     * Output type enumeration.
     */
    public enum OutputType {
        /** Standard output */
        STDOUT,
        /** Standard error */
        STDERR,
        /** End of stream marker */
        END;

        /**
         * Create an OutputType from a docker-java StreamType.
         *
         * @param streamType the docker-java StreamType
         * @return corresponding OutputType
         */
        public static OutputType forStreamType(StreamType streamType);
    }
}

Usage Example:

import org.testcontainers.containers.output.OutputFrame;
import java.util.function.Consumer;

// Process output frames
Consumer<OutputFrame> frameProcessor = frame -> {
    switch (frame.getType()) {
        case STDOUT:
            System.out.println("[OUT] " + frame.getUtf8String());
            break;
        case STDERR:
            System.err.println("[ERR] " + frame.getUtf8String());
            break;
        case END:
            System.out.println("Stream ended");
            break;
    }
};

BaseConsumer

Abstract base class for implementing output consumers with ANSI code filtering support.

/**
 * Base class for output consumers providing common functionality.
 * By default, ANSI escape codes are removed from output (removeColorCodes defaults to true).
 * Note: Internal field is named removeColorCodes, but the method parameter is removeAnsiCodes.
 *
 * @param <SELF> Self-referential generic type for fluent API
 */
public abstract class BaseConsumer<SELF extends BaseConsumer<SELF>>
    implements Consumer<OutputFrame> {

    /**
     * Configure whether to remove ANSI escape codes from output.
     * Defaults to true (ANSI codes are removed by default).
     *
     * @param removeAnsiCodes true to remove ANSI codes, false to keep them
     * @return this consumer for method chaining
     */
    public SELF withRemoveAnsiCodes(boolean removeAnsiCodes);

    /**
     * Get whether ANSI escape codes are removed from output.
     * Lombok @Getter generates this method from the removeColorCodes field.
     *
     * @return true if ANSI codes are removed, false if kept
     */
    public boolean isRemoveColorCodes();

    /**
     * Set whether to remove ANSI escape codes from output.
     * Lombok @Setter generates this method from the removeColorCodes field.
     *
     * @param removeColorCodes true to remove ANSI codes, false to keep them
     */
    public void setRemoveColorCodes(boolean removeColorCodes);

    /**
     * Accept and process an output frame.
     *
     * @param frame the output frame to process
     */
    @Override
    public abstract void accept(OutputFrame frame);
}

Slf4jLogConsumer

Send container output to an SLF4J logger for integration with application logging.

/**
 * Consumer that sends container output to an SLF4J logger.
 * By default, all output (both STDOUT and STDERR) is logged at INFO level.
 * When separateOutputStreams is true, STDOUT is logged at INFO level and STDERR at ERROR level.
 *
 * Prefix formatting behavior:
 * - With prefix and separateOutputStreams: format is "[prefix] : {message}"
 * - With prefix and without separateOutputStreams: format is "[prefix] {outputType}: {message}"
 * - Without prefix and separateOutputStreams: just "{message}"
 * - Without prefix and without separateOutputStreams: "{outputType}: {message}"
 * Note: Prefixes are automatically wrapped in brackets with a trailing space.
 */
public class Slf4jLogConsumer extends BaseConsumer<Slf4jLogConsumer> {
    /**
     * Create a log consumer with the specified logger.
     *
     * @param logger the SLF4J logger to use
     */
    public Slf4jLogConsumer(Logger logger);

    /**
     * Create a log consumer with separate output stream handling.
     *
     * @param logger the SLF4J logger to use
     * @param separateOutputStreams true to log stdout to INFO and stderr to ERROR
     */
    public Slf4jLogConsumer(Logger logger, boolean separateOutputStreams);

    /**
     * Add a prefix to all log messages.
     * The prefix will be automatically wrapped in brackets with a trailing space: "[prefix] "
     *
     * @param prefix the prefix string (will be formatted as "[prefix] ")
     * @return this consumer for method chaining
     */
    public Slf4jLogConsumer withPrefix(String prefix);

    /**
     * Add an MDC (Mapped Diagnostic Context) entry.
     *
     * @param key MDC key
     * @param value MDC value
     * @return this consumer for method chaining
     */
    public Slf4jLogConsumer withMdc(String key, String value);

    /**
     * Add multiple MDC (Mapped Diagnostic Context) entries.
     *
     * @param mdc map of MDC entries
     * @return this consumer for method chaining
     */
    public Slf4jLogConsumer withMdc(Map<String, String> mdc);

    /**
     * Enable separate output streams (stdout to INFO, stderr to ERROR).
     *
     * @return this consumer for method chaining
     */
    public Slf4jLogConsumer withSeparateOutputStreams();

    @Override
    public Slf4jLogConsumer withRemoveAnsiCodes(boolean removeAnsiCodes);

    @Override
    public void accept(OutputFrame frame);
}

Usage Examples:

import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.output.Slf4jLogConsumer;
import org.testcontainers.utility.DockerImageName;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

// Send container logs to application logger
Logger logger = LoggerFactory.getLogger("container.postgres");

GenericContainer<?> postgres = new GenericContainer<>(DockerImageName.parse("postgres:15"))
    .withExposedPorts(5432)
    .withLogConsumer(new Slf4jLogConsumer(logger));

postgres.start();
// Container output is logged via SLF4J

// With ANSI code filtering
Logger appLogger = LoggerFactory.getLogger("container.app");

GenericContainer<?> app = new GenericContainer<>(DockerImageName.parse("myapp:latest"))
    .withExposedPorts(8080)
    .withLogConsumer(new Slf4jLogConsumer(appLogger)
        .withRemoveAnsiCodes(true));  // Remove color codes

// Use logger name based on container
GenericContainer<?> redis = new GenericContainer<>(DockerImageName.parse("redis:7.0"))
    .withExposedPorts(6379)
    .withLogConsumer(new Slf4jLogConsumer(
        LoggerFactory.getLogger("testcontainers.redis")));

ToStringConsumer

Collect container output into a string for later analysis or assertions.

/**
 * Consumer that collects output into a string buffer.
 * Useful for capturing output for assertions in tests.
 */
public class ToStringConsumer extends BaseConsumer<ToStringConsumer> {
    /**
     * Get the collected output as a UTF-8 string.
     *
     * @return all collected output
     */
    public String toUtf8String();

    /**
     * Get the collected output with a specific charset.
     *
     * @param charset the charset to use for decoding
     * @return all collected output decoded with the specified charset
     */
    public String toString(Charset charset);

    @Override
    public ToStringConsumer withRemoveAnsiCodes(boolean removeAnsiCodes);

    @Override
    public void accept(OutputFrame frame);
}

Usage Examples:

import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.output.ToStringConsumer;
import org.testcontainers.utility.DockerImageName;

// Collect all output for assertion
ToStringConsumer consumer = new ToStringConsumer();

GenericContainer<?> container = new GenericContainer<>(DockerImageName.parse("alpine:latest"))
    .withCommand("sh", "-c", "echo 'Hello World' && echo 'Test Output'")
    .withLogConsumer(consumer);

container.start();

// Wait for container to finish
Thread.sleep(1000);

// Assert on collected output
String output = consumer.toUtf8String();
assertTrue(output.contains("Hello World"));
assertTrue(output.contains("Test Output"));

// Collect output without ANSI codes
ToStringConsumer cleanConsumer = new ToStringConsumer()
    .withRemoveAnsiCodes(true);

GenericContainer<?> colorfulApp = new GenericContainer<>(DockerImageName.parse("myapp:latest"))
    .withLogConsumer(cleanConsumer);

colorfulApp.start();
String cleanOutput = cleanConsumer.toUtf8String();
// Output has ANSI escape codes removed

WaitingConsumer

Wait for specific output patterns or stream completion with timeout support.

Note: WaitingConsumer does not provide a toUtf8String() method for retrieving captured output. After waiting for conditions, use container.getLogs() to retrieve the complete container output, or use ToStringConsumer if you need to capture output as a string.

/**
 * Consumer that waits for specific output patterns or stream completion.
 * Useful for synchronizing test execution with container events.
 */
public class WaitingConsumer extends BaseConsumer<WaitingConsumer> {
    /**
     * Get access to the underlying frame buffer.
     *
     * @return the LinkedBlockingDeque containing all output frames
     */
    public LinkedBlockingDeque<OutputFrame> getFrames();

    /**
     * Wait indefinitely until a predicate matches an output frame.
     *
     * @param predicate condition to wait for
     * @throws TimeoutException if timeout is reached
     */
    public void waitUntil(Predicate<OutputFrame> predicate) throws TimeoutException;

    /**
     * Wait until a predicate matches an output frame, with timeout.
     *
     * @param predicate condition to wait for
     * @param limit timeout value
     * @param limitUnit timeout unit
     * @throws TimeoutException if timeout is reached
     */
    public void waitUntil(Predicate<OutputFrame> predicate, int limit, TimeUnit limitUnit)
        throws TimeoutException;

    /**
     * Wait until a predicate matches an output frame N times.
     *
     * @param predicate condition to wait for
     * @param limit timeout value
     * @param limitUnit timeout unit
     * @param times number of times the predicate must match
     * @throws TimeoutException if timeout is reached
     */
    public void waitUntil(Predicate<OutputFrame> predicate, long limit, TimeUnit limitUnit, int times)
        throws TimeoutException;

    /**
     * Wait until the output stream ends.
     * Waits indefinitely (Long.MAX_VALUE nanoseconds).
     */
    public void waitUntilEnd();

    /**
     * Wait until the output stream ends, with timeout.
     *
     * @param limit timeout value
     * @param limitUnit timeout unit
     * @throws TimeoutException if timeout is reached
     */
    public void waitUntilEnd(long limit, TimeUnit limitUnit) throws TimeoutException;

    @Override
    public WaitingConsumer withRemoveAnsiCodes(boolean removeAnsiCodes);

    @Override
    public void accept(OutputFrame frame);
}

Usage Examples:

import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.output.OutputFrame;
import org.testcontainers.containers.output.WaitingConsumer;
import org.testcontainers.utility.DockerImageName;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

// Wait for specific log message
WaitingConsumer waitingConsumer = new WaitingConsumer();

GenericContainer<?> app = new GenericContainer<>(DockerImageName.parse("myapp:latest"))
    .withExposedPorts(8080)
    .withLogConsumer(waitingConsumer);

app.start();

// Wait for application to be ready
waitingConsumer.waitUntil(
    frame -> frame.getUtf8String().contains("Application started"),
    30,
    TimeUnit.SECONDS
);

// Now safe to proceed with tests
String appUrl = String.format("http://%s:%d", app.getHost(), app.getMappedPort(8080));

// Wait for specific error message
WaitingConsumer errorWatcher = new WaitingConsumer();

GenericContainer<?> container = new GenericContainer<>(DockerImageName.parse("test:latest"))
    .withLogConsumer(errorWatcher);

container.start();

try {
    errorWatcher.waitUntil(
        frame -> frame.getType() == OutputFrame.OutputType.STDERR &&
                 frame.getUtf8String().contains("FATAL ERROR"),
        10,
        TimeUnit.SECONDS
    );
    fail("Expected fatal error did not occur");
} catch (TimeoutException e) {
    // No error occurred - test passes
}

// Wait for container to finish execution
WaitingConsumer finishWatcher = new WaitingConsumer();

GenericContainer<?> oneShot = new GenericContainer<>(DockerImageName.parse("alpine:latest"))
    .withCommand("sh", "-c", "sleep 5 && echo 'Done'")
    .withLogConsumer(finishWatcher);

oneShot.start();

// Wait for output stream to end
finishWatcher.waitUntilEnd(10, TimeUnit.SECONDS);

// Container has finished execution
String logs = oneShot.getLogs();

FrameConsumerResultCallback

Adapter for integrating with docker-java's callback mechanism. Advanced users can use this for custom output handling integrations.

Note: Most users should use one of the higher-level consumer classes (Slf4jLogConsumer, ToStringConsumer, WaitingConsumer) or implement Consumer<OutputFrame> directly. FrameConsumerResultCallback is a lower-level API for advanced integration scenarios where you need direct control over the docker-java callback mechanism. Unless you have specific docker-java integration requirements, prefer the standard consumer implementations.

Important: GenericContainer.withLogConsumer() expects a Consumer<OutputFrame>, not a FrameConsumerResultCallback. To use multiple consumers, either combine them into a single Consumer<OutputFrame> (see Multi-Consumer Pattern example below) or call withLogConsumer() multiple times.

/**
 * Docker-java callback adapter for frame consumers.
 * Provides low-level access to container output streams.
 * WARNING: This is an advanced class. Most users should use Slf4jLogConsumer,
 * ToStringConsumer, or WaitingConsumer instead.
 */
public class FrameConsumerResultCallback
    extends ResultCallbackTemplate<FrameConsumerResultCallback, Frame> {

    /**
     * Add a consumer for a specific output type.
     *
     * @param outputType the output type (STDOUT, STDERR, END)
     * @param consumer the consumer to handle frames of this type
     */
    public void addConsumer(OutputFrame.OutputType outputType, Consumer<OutputFrame> consumer);

    /**
     * Get the completion latch for waiting until output stream ends.
     *
     * @return CountDownLatch that counts down when stream completes
     */
    public CountDownLatch getCompletionLatch();

    /**
     * Process the next frame from the docker-java stream.
     *
     * @param frame the next frame to process
     */
    @Override
    public void onNext(Frame frame);

    /**
     * Handle errors from the docker-java stream.
     *
     * @param throwable the error that occurred
     */
    @Override
    public void onError(Throwable throwable);

    /**
     * Close the callback and release resources.
     */
    @Override
    public void close();
}

Choosing the Right Consumer - Decision Guide:

Use this decision tree to select the appropriate consumer for your use case:

Use CaseRecommended ConsumerReason
Log container output to SLF4J loggerSlf4jLogConsumerIntegrates with your logging framework, respects log levels
Capture all output as string for assertionsToStringConsumerSimple string capture, good for testing output content
Wait for specific log message before proceedingWaitingConsumerBlocks until condition met, perfect for synchronization
Process each output line as it arrivesCustom Consumer<OutputFrame>Direct frame-by-frame processing, flexible
Advanced docker-java integrationFrameConsumerResultCallbackLow-level callback access, only when needed

Quick Decision Flowchart:

Need to log to SLF4J/Logback/Log4j?
├─ Yes → Use Slf4jLogConsumer
└─ No
   └─ Need to capture output for assertions?
      ├─ Yes → Use ToStringConsumer
      └─ No
         └─ Need to wait for specific output?
            ├─ Yes → Use WaitingConsumer
            └─ No
               └─ Need custom per-frame processing?
                  ├─ Yes → Implement Consumer<OutputFrame>
                  └─ No → Need docker-java callback integration?
                     └─ Yes → Use FrameConsumerResultCallback (advanced)

Common Patterns:

// Pattern 1: Debug logging during development
container.withLogConsumer(new Slf4jLogConsumer(LoggerFactory.getLogger("container")));

// Pattern 2: Verify output in tests
ToStringConsumer toStringConsumer = new ToStringConsumer();
container.withLogConsumer(toStringConsumer);
container.start();
assertThat(toStringConsumer.toUtf8String()).contains("Server started");

// Pattern 3: Synchronize with container startup
WaitingConsumer waitingConsumer = new WaitingConsumer();
container.withLogConsumer(waitingConsumer);
container.start();
waitingConsumer.waitUntil(frame ->
    frame.getUtf8String().contains("ready to accept connections"), 30, TimeUnit.SECONDS);

// Pattern 4: Multiple consumers for different purposes
container.withLogConsumer(new Slf4jLogConsumer(logger))  // Log everything
         .withLogConsumer(toStringConsumer);              // Also capture for assertions

Complete Usage Examples

Multi-Consumer Pattern

Attach multiple consumers to process output in different ways simultaneously.

import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.output.OutputFrame;
import org.testcontainers.containers.output.Slf4jLogConsumer;
import org.testcontainers.containers.output.ToStringConsumer;
import org.testcontainers.containers.output.WaitingConsumer;
import org.testcontainers.utility.DockerImageName;
import org.slf4j.LoggerFactory;
import java.util.function.Consumer;

public class MultiConsumerExample {

    @Test
    public void testWithMultipleConsumers() throws Exception {
        // Create multiple consumers
        Slf4jLogConsumer slf4jConsumer = new Slf4jLogConsumer(
            LoggerFactory.getLogger("test.container"));

        ToStringConsumer stringConsumer = new ToStringConsumer();

        WaitingConsumer waitingConsumer = new WaitingConsumer();

        // Combine consumers
        Consumer<OutputFrame> multiConsumer = frame -> {
            slf4jConsumer.accept(frame);      // Log to SLF4J
            stringConsumer.accept(frame);      // Collect to string
            waitingConsumer.accept(frame);     // Enable waiting
        };

        GenericContainer<?> app = new GenericContainer<>(
                DockerImageName.parse("myapp:latest"))
            .withExposedPorts(8080)
            .withLogConsumer(multiConsumer);

        app.start();

        // Wait for ready message
        waitingConsumer.waitUntil(
            frame -> frame.getUtf8String().contains("Server started"),
            30,
            TimeUnit.SECONDS
        );

        // Run tests
        // ...

        // Analyze collected output
        String allOutput = stringConsumer.toUtf8String();
        assertTrue(allOutput.contains("Server started"));
        assertFalse(allOutput.contains("ERROR"));
    }
}

Custom Output Consumer

Implement a custom consumer for specialized output processing.

import org.testcontainers.containers.output.BaseConsumer;
import org.testcontainers.containers.output.OutputFrame;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;

// Custom consumer that writes to a file
public class FileLogConsumer extends BaseConsumer<FileLogConsumer> {
    private final PrintWriter writer;
    private final DateTimeFormatter formatter;

    public FileLogConsumer(String filename) throws IOException {
        this.writer = new PrintWriter(new FileWriter(filename, true));
        this.formatter = DateTimeFormatter.ISO_INSTANT;
    }

    @Override
    public void accept(OutputFrame frame) {
        if (frame.getType() != OutputFrame.OutputType.END) {
            String timestamp = formatter.format(Instant.now());
            String type = frame.getType().name();
            String message = frame.getUtf8StringWithoutLineEnding();

            writer.println(String.format("[%s] %s: %s", timestamp, type, message));
            writer.flush();
        }
    }

    public void close() {
        writer.close();
    }
}

// Custom consumer that collects errors
public class ErrorCollector extends BaseConsumer<ErrorCollector> {
    private final List<String> errors = new ArrayList<>();

    @Override
    public void accept(OutputFrame frame) {
        if (frame.getType() == OutputFrame.OutputType.STDERR) {
            String message = frame.getUtf8StringWithoutLineEnding();
            if (message.contains("ERROR") || message.contains("Exception")) {
                errors.add(message);
            }
        }
    }

    public List<String> getErrors() {
        return new ArrayList<>(errors);
    }

    public boolean hasErrors() {
        return !errors.isEmpty();
    }
}

// Usage
ErrorCollector errorCollector = new ErrorCollector();
FileLogConsumer fileLogger = new FileLogConsumer("container.log");

Consumer<OutputFrame> combined = frame -> {
    errorCollector.accept(frame);
    fileLogger.accept(frame);
};

GenericContainer<?> container = new GenericContainer<>(DockerImageName.parse("myapp:latest"))
    .withLogConsumer(combined);

container.start();

// After test
assertFalse(errorCollector.hasErrors(),
    "Container had errors: " + errorCollector.getErrors());
fileLogger.close();

Pattern-Based Output Processing

import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.output.OutputFrame;
import org.testcontainers.containers.output.WaitingConsumer;
import org.testcontainers.utility.DockerImageName;
import java.time.Duration;
import java.util.regex.Pattern;

public class PatternBasedOutputTest {

    @Test
    public void testDatabaseMigration() throws Exception {
        WaitingConsumer consumer = new WaitingConsumer();

        GenericContainer<?> app = new GenericContainer<>(
                DockerImageName.parse("myapp:latest"))
            .withExposedPorts(8080)
            .withEnv("RUN_MIGRATIONS", "true")
            .withLogConsumer(consumer);

        app.start();

        // Wait for migration start
        consumer.waitUntil(
            frame -> frame.getUtf8String().contains("Starting database migrations"),
            10,
            TimeUnit.SECONDS
        );

        // Wait for migration completion with pattern matching
        Pattern migrationPattern = Pattern.compile(".*Completed (\\d+) migrations.*");
        consumer.waitUntil(
            frame -> migrationPattern.matcher(frame.getUtf8String()).find(),
            2,
            TimeUnit.MINUTES
        );

        // Application is ready for testing
    }

    @Test
    public void testWorkerScaling() throws Exception {
        WaitingConsumer consumer = new WaitingConsumer();

        GenericContainer<?> worker = new GenericContainer<>(
                DockerImageName.parse("worker:latest"))
            .withEnv("WORKERS", "3")
            .withLogConsumer(consumer);

        worker.start();

        // Count how many workers started
        int workersStarted = 0;
        Pattern workerPattern = Pattern.compile(".*Worker (\\d+) ready.*");

        for (int i = 0; i < 3; i++) {
            consumer.waitUntil(
                frame -> workerPattern.matcher(frame.getUtf8String()).find(),
                10,
                TimeUnit.SECONDS
            );
            workersStarted++;
        }

        assertEquals(3, workersStarted, "All workers should have started");
    }
}

Debugging with Output Consumers

import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.output.OutputFrame;
import org.testcontainers.containers.output.ToStringConsumer;
import org.testcontainers.utility.DockerImageName;

public class DebuggingOutputTest {

    @Test
    public void testWithDetailedLogging() {
        ToStringConsumer consumer = new ToStringConsumer();

        GenericContainer<?> app = new GenericContainer<>(
                DockerImageName.parse("myapp:latest"))
            .withExposedPorts(8080)
            .withEnv("DEBUG", "true")
            .withEnv("LOG_LEVEL", "DEBUG")
            .withLogConsumer(consumer);

        try {
            app.start();

            // Run test that might fail
            // ...

        } catch (Exception e) {
            // Dump container output for debugging
            System.err.println("=== Container Output ===");
            System.err.println(consumer.toUtf8String());
            System.err.println("=== End Container Output ===");
            throw e;
        }
    }

    @Test
    public void testWithFilteredOutput() {
        // Only capture ERROR and WARN messages
        ToStringConsumer errorConsumer = new ToStringConsumer();

        Consumer<OutputFrame> filteringConsumer = frame -> {
            String message = frame.getUtf8String();
            if (message.contains("ERROR") || message.contains("WARN")) {
                errorConsumer.accept(frame);
            }
        };

        GenericContainer<?> container = new GenericContainer<>(
                DockerImageName.parse("myapp:latest"))
            .withLogConsumer(filteringConsumer);

        container.start();

        // Only errors and warnings are collected
        String criticalMessages = errorConsumer.toUtf8String();

        if (!criticalMessages.isEmpty()) {
            System.err.println("Critical messages detected:");
            System.err.println(criticalMessages);
        }
    }
}

Real-time Log Monitoring

import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.output.OutputFrame;
import org.testcontainers.utility.DockerImageName;

public class RealTimeMonitoringTest {

    @Test
    public void testWithRealTimeMonitoring() throws InterruptedException {
        AtomicInteger lineCount = new AtomicInteger(0);
        AtomicBoolean errorDetected = new AtomicBoolean(false);

        Consumer<OutputFrame> monitor = frame -> {
            lineCount.incrementAndGet();

            String message = frame.getUtf8String();
            System.out.println("[CONTAINER] " + message);

            // Detect errors in real-time
            if (message.contains("ERROR") || message.contains("Exception")) {
                errorDetected.set(true);
                System.err.println("!!! ERROR DETECTED: " + message);
            }

            // Trigger actions based on output
            if (message.contains("Memory usage: 90%")) {
                System.err.println("!!! HIGH MEMORY WARNING");
            }
        };

        GenericContainer<?> app = new GenericContainer<>(
                DockerImageName.parse("myapp:latest"))
            .withExposedPorts(8080)
            .withLogConsumer(monitor);

        app.start();

        // Give container time to produce output
        Thread.sleep(5000);

        // Check monitoring results
        assertTrue(lineCount.get() > 0, "Container should have produced output");
        assertFalse(errorDetected.get(), "No errors should be detected");
    }
}