diff --git a/core/src/main/java/org/testcontainers/containers/Container.java b/core/src/main/java/org/testcontainers/containers/Container.java
index abea7fef576..ed684c14260 100644
--- a/core/src/main/java/org/testcontainers/containers/Container.java
+++ b/core/src/main/java/org/testcontainers/containers/Container.java
@@ -375,6 +375,13 @@ SELF withClasspathResourceMapping(
*/
SELF withWorkingDirectory(String workDir);
+ /**
+ * Set the container alias to distinguish between multiple containers.
+ *
+ * @param alias name
+ */
+ SELF withContainerAlias(String alias);
+
/**
* Resolve Docker image and set it.
*
diff --git a/core/src/main/java/org/testcontainers/containers/GenericContainer.java b/core/src/main/java/org/testcontainers/containers/GenericContainer.java
index 0fe944433ae..bc3ff9f2c59 100644
--- a/core/src/main/java/org/testcontainers/containers/GenericContainer.java
+++ b/core/src/main/java/org/testcontainers/containers/GenericContainer.java
@@ -138,6 +138,9 @@ public class GenericContainer>
@Nullable
private String workingDirectory = null;
+ @Nullable
+ private String containerAlias = null;
+
/**
* The shared memory size to use when starting the container.
* This value is in bytes.
@@ -343,7 +346,14 @@ protected void doStart() {
}
);
} catch (Exception e) {
- throw new ContainerLaunchException("Container startup failed for image " + getDockerImageName(), e);
+ String containerAliasStr = "";
+ if (StringUtils.isNotBlank(containerAlias)) {
+ containerAliasStr = " (containerAlias='" + containerAlias.trim() + "')";
+ }
+ throw new ContainerLaunchException(
+ "Container startup failed for image " + getDockerImageName() + containerAliasStr,
+ e
+ );
}
}
@@ -663,7 +673,11 @@ public void stop() {
* @return a logger that references the docker image name
*/
protected Logger logger() {
- return DockerLoggerFactory.getLogger(this.getDockerImageName());
+ String loggerName = this.getDockerImageName();
+ if (StringUtils.isNotBlank(containerAlias)) {
+ loggerName = loggerName + "--" + containerAlias.trim();
+ }
+ return DockerLoggerFactory.getLogger(loggerName);
}
/**
@@ -1256,6 +1270,12 @@ public SELF withWorkingDirectory(String workDir) {
return self();
}
+ @Override
+ public SELF withContainerAlias(String alias) {
+ this.setContainerAlias(alias);
+ return self();
+ }
+
/**
* {@inheritDoc}
*/
diff --git a/core/src/test/java/org/testcontainers/junit/ContainerAliasTest.java b/core/src/test/java/org/testcontainers/junit/ContainerAliasTest.java
new file mode 100644
index 00000000000..69a28fdd72c
--- /dev/null
+++ b/core/src/test/java/org/testcontainers/junit/ContainerAliasTest.java
@@ -0,0 +1,102 @@
+package org.testcontainers.junit;
+
+import ch.qos.logback.classic.Logger;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.core.read.ListAppender;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.slf4j.LoggerFactory;
+import org.testcontainers.TestImages;
+import org.testcontainers.containers.ContainerLaunchException;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.startupcheck.OneShotStartupCheckStrategy;
+
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatNoException;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+class ContainerAliasTest {
+
+ private final TestLogger testLogger = new TestLogger();
+
+ public GenericContainer container1 = new GenericContainer(TestImages.ALPINE_IMAGE)
+ .withContainerAlias("potato")
+ .withStartupCheckStrategy(new OneShotStartupCheckStrategy())
+ .withCommand("ls", "-al");
+
+ public GenericContainer container2 = new GenericContainer(TestImages.ALPINE_IMAGE)
+ .withContainerAlias("monkey")
+ .withStartupCheckStrategy(new OneShotStartupCheckStrategy())
+ .withCommand("non-existing-command");
+
+ @BeforeEach
+ void setUp() {
+ testLogger.startCapturing();
+ }
+
+ @AfterEach
+ void tearDown() {
+ testLogger.stopCapturing();
+ }
+
+ @Test
+ void checkOutput() {
+ assertThatNoException()
+ .isThrownBy(() -> {
+ container1.start();
+ });
+
+ assertThatThrownBy(() -> {
+ container2.start();
+ })
+ .isInstanceOf(ContainerLaunchException.class)
+ .hasMessage("Container startup failed for image alpine:3.17 (containerAlias='monkey')");
+
+ List container1PotatoLogs = testLogger
+ .getLogs()
+ .stream()
+ .filter(it -> "tc.alpine:3.17--potato".equals(it.getLoggerName()))
+ .map(ILoggingEvent::getFormattedMessage)
+ .toList();
+ assertThat(container1PotatoLogs)
+ .anyMatch(it -> it.equals("Creating container for image: alpine:3.17"))
+ .anyMatch(it -> it.startsWith("Container alpine:3.17 is starting: "))
+ .anyMatch(it -> it.startsWith("Container alpine:3.17 started in P"));
+
+ List container2MonkeyLogs = testLogger
+ .getLogs()
+ .stream()
+ .filter(it -> "tc.alpine:3.17--monkey".equals(it.getLoggerName()))
+ .map(ILoggingEvent::getFormattedMessage)
+ .toList();
+ assertThat(container2MonkeyLogs)
+ .anyMatch(it -> it.equals("Creating container for image: alpine:3.17"))
+ .anyMatch(it -> it.startsWith("Container alpine:3.17 is starting: "))
+ .anyMatch(it -> it.equals("Could not start container"))
+ .anyMatch(it -> it.equals("There are no stdout/stderr logs available for the failed container"));
+ }
+}
+
+class TestLogger {
+
+ private final ListAppender listAppender = new ListAppender<>();
+
+ public void startCapturing() {
+ Logger rootLogger = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
+ listAppender.start();
+ rootLogger.addAppender(listAppender);
+ }
+
+ public void stopCapturing() {
+ Logger rootLogger = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
+ rootLogger.detachAppender(listAppender);
+ listAppender.stop();
+ }
+
+ public List getLogs() {
+ return listAppender.list;
+ }
+}