Skip to content

Commit 2c95200

Browse files
committed
Simple summary logging of images pulled during execution
1 parent 8bc3bf8 commit 2c95200

File tree

6 files changed

+127
-5
lines changed

6 files changed

+127
-5
lines changed

core/src/main/java/org/testcontainers/DockerClientFactory.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,7 @@ private boolean checkMountableFile() {
326326
public void checkAndPullImage(DockerClient client, String image) {
327327
List<Image> images = client.listImagesCmd().withImageNameFilter(image).exec();
328328
if (images.isEmpty()) {
329-
client.pullImageCmd(image).exec(new TimeLimitedLoggedPullImageResultCallback(log)).awaitCompletion();
329+
client.pullImageCmd(image).exec(new TimeLimitedLoggedPullImageResultCallback(log , image)).awaitCompletion();
330330
}
331331
}
332332

core/src/main/java/org/testcontainers/images/LoggedPullImageResultCallback.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import com.github.dockerjava.api.command.PullImageResultCallback;
44
import com.github.dockerjava.api.model.PullResponseItem;
55
import org.slf4j.Logger;
6+
import org.testcontainers.utility.ImagePullCountLogger;
67

78
import java.io.Closeable;
89
import java.time.Duration;
@@ -17,6 +18,7 @@
1718
*/
1819
class LoggedPullImageResultCallback extends PullImageResultCallback {
1920
private final Logger logger;
21+
private final String canonicalImageName;
2022

2123
private final Set<String> allLayers = new HashSet<>();
2224
private final Set<String> downloadedLayers = new HashSet<>();
@@ -26,8 +28,9 @@ class LoggedPullImageResultCallback extends PullImageResultCallback {
2628
private boolean completed;
2729
private Instant start;
2830

29-
LoggedPullImageResultCallback(final Logger logger) {
31+
LoggedPullImageResultCallback(final Logger logger, final String canonicalImageName) {
3032
this.logger = logger;
33+
this.canonicalImageName = canonicalImageName;
3134
}
3235

3336
@Override
@@ -109,6 +112,8 @@ public void onComplete() {
109112
byteCountToDisplaySize(downloadedLayerSize),
110113
byteCountToDisplaySize(downloadedLayerSize / duration));
111114
}
115+
116+
ImagePullCountLogger.instance().recordPull(canonicalImageName);
112117
}
113118

114119
private long downloadedLayerSize() {

core/src/main/java/org/testcontainers/images/RemoteDockerImage.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ protected final String resolve() {
7777
dockerClient
7878
.pullImageCmd(imageName.getUnversionedPart())
7979
.withTag(imageName.getVersionPart())
80-
.exec(new TimeLimitedLoggedPullImageResultCallback(logger))
80+
.exec(new TimeLimitedLoggedPullImageResultCallback(logger, imageName.asCanonicalNameString()))
8181
.awaitCompletion();
8282

8383
LocalImagesCache.INSTANCE.refreshCache(imageName);

core/src/main/java/org/testcontainers/images/TimeLimitedLoggedPullImageResultCallback.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ public class TimeLimitedLoggedPullImageResultCallback extends LoggedPullImageRes
4141
// All threads that are 'awaiting' this pull
4242
private final Set<Thread> waitingThreads = new HashSet<>();
4343

44-
public TimeLimitedLoggedPullImageResultCallback(Logger logger) {
45-
super(logger);
44+
public TimeLimitedLoggedPullImageResultCallback(Logger logger, final String canonicalImageName) {
45+
super(logger, canonicalImageName);
4646
this.logger = logger;
4747
}
4848

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package org.testcontainers.utility;
2+
3+
import com.google.common.annotations.VisibleForTesting;
4+
import lombok.extern.slf4j.Slf4j;
5+
6+
import java.util.Map;
7+
import java.util.concurrent.ConcurrentHashMap;
8+
import java.util.concurrent.atomic.AtomicInteger;
9+
import java.util.stream.Collectors;
10+
11+
/**
12+
* Simple utility to log which images have been pulled by {@link org.testcontainers.Testcontainers} and how many times.
13+
*/
14+
@Slf4j
15+
public class ImagePullCountLogger {
16+
17+
private static ImagePullCountLogger instance;
18+
private final Map<String, AtomicInteger> pullCounters = new ConcurrentHashMap<>();
19+
20+
public synchronized static ImagePullCountLogger instance() {
21+
if (instance == null) {
22+
instance = new ImagePullCountLogger();
23+
Runtime.getRuntime().addShutdownHook(new Thread(instance::logStatistics));
24+
}
25+
26+
return instance;
27+
}
28+
29+
@VisibleForTesting
30+
ImagePullCountLogger() {
31+
32+
}
33+
34+
public void logStatistics() {
35+
if (pullCounters.size() > 0) {
36+
final String summary = pullCounters.entrySet().stream()
37+
.map(it -> it.getKey() + (it.getValue().intValue() > 1 ? " (" + it.getValue() + " times)" : ""))
38+
.sorted()
39+
.collect(Collectors.joining("\n ", "\n ", "\n"));
40+
41+
log.info("Testcontainers pulled the following images during execution:{}", summary);
42+
} else {
43+
log.info("Testcontainers did not need to pull any images during execution");
44+
}
45+
}
46+
47+
public void recordPull(final String image) {
48+
pullCounters.computeIfAbsent(image, __ -> new AtomicInteger()).incrementAndGet();
49+
}
50+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package org.testcontainers.utility;
2+
3+
import ch.qos.logback.classic.Logger;
4+
import ch.qos.logback.classic.spi.ILoggingEvent;
5+
import ch.qos.logback.core.read.ListAppender;
6+
import org.junit.After;
7+
import org.junit.Before;
8+
import org.junit.Test;
9+
import org.slf4j.LoggerFactory;
10+
11+
import java.util.Optional;
12+
13+
import static org.junit.Assert.assertEquals;
14+
import static org.junit.Assert.assertTrue;
15+
16+
public class ImagePullCountLoggerTest {
17+
18+
private ImagePullCountLogger underTest;
19+
private ListAppender<ILoggingEvent> listAppender;
20+
private Logger logger;
21+
22+
@Before
23+
public void setUp() throws Exception {
24+
logger = (Logger) LoggerFactory.getLogger(ImagePullCountLogger.class);
25+
listAppender = new ListAppender<>();
26+
logger.addAppender(listAppender);
27+
listAppender.start();
28+
}
29+
30+
@Test
31+
public void testPullCountsLogged() {
32+
underTest = new ImagePullCountLogger();
33+
34+
underTest.recordPull("imageA");
35+
underTest.recordPull("imageA");
36+
underTest.recordPull("imageB");
37+
underTest.recordPull("imageC");
38+
39+
underTest.logStatistics();
40+
41+
assertEquals(1, listAppender.list.size());
42+
final Optional<String> messages = listAppender.list.stream().map(ILoggingEvent::getFormattedMessage).findFirst();
43+
assertTrue(messages.isPresent());
44+
final String message = messages.get();
45+
assertTrue(message.contains("imageA (2 times)\n"));
46+
assertTrue(message.contains("imageB\n"));
47+
assertTrue(message.contains("imageC\n"));
48+
}
49+
50+
@Test
51+
public void testNoPullsLogged() {
52+
underTest = new ImagePullCountLogger();
53+
54+
underTest.logStatistics();
55+
56+
assertEquals(1, listAppender.list.size());
57+
final Optional<String> messages = listAppender.list.stream().map(ILoggingEvent::getFormattedMessage).findFirst();
58+
assertTrue(messages.isPresent());
59+
final String message = messages.get();
60+
assertEquals("Testcontainers did not need to pull any images during execution", message);
61+
}
62+
63+
@After
64+
public void tearDown() throws Exception {
65+
logger.detachAppender(listAppender);
66+
}
67+
}

0 commit comments

Comments
 (0)