diff --git a/src/main/java/com/github/dockerjava/api/DockerClient.java b/src/main/java/com/github/dockerjava/api/DockerClient.java index 9e275eb35..723c1bad3 100644 --- a/src/main/java/com/github/dockerjava/api/DockerClient.java +++ b/src/main/java/com/github/dockerjava/api/DockerClient.java @@ -114,6 +114,8 @@ public CopyFileFromContainerCmd copyFileFromContainerCmd( public EventsCmd eventsCmd(EventCallback eventCallback); + public StatsCmd statsCmd(StatsCallback statsCallback); + @Override public void close() throws IOException; diff --git a/src/main/java/com/github/dockerjava/api/command/DockerCmdExecFactory.java b/src/main/java/com/github/dockerjava/api/command/DockerCmdExecFactory.java index 8b9880747..294d50994 100644 --- a/src/main/java/com/github/dockerjava/api/command/DockerCmdExecFactory.java +++ b/src/main/java/com/github/dockerjava/api/command/DockerCmdExecFactory.java @@ -78,6 +78,9 @@ public interface DockerCmdExecFactory extends Closeable { public UnpauseContainerCmd.Exec createUnpauseContainerCmdExec(); public EventsCmd.Exec createEventsCmdExec(); + + public StatsCmd.Exec createStatsCmdExec(); + @Override public void close() throws IOException; diff --git a/src/main/java/com/github/dockerjava/api/command/StatsCallback.java b/src/main/java/com/github/dockerjava/api/command/StatsCallback.java new file mode 100644 index 000000000..610c0f831 --- /dev/null +++ b/src/main/java/com/github/dockerjava/api/command/StatsCallback.java @@ -0,0 +1,13 @@ +package com.github.dockerjava.api.command; + +import com.github.dockerjava.api.model.Statistics; + +/** + * Stats callback + */ +public interface StatsCallback { + public void onStats(Statistics stats); + public void onException(Throwable throwable); + public void onCompletion(int numStats); + public boolean isReceiving(); +} diff --git a/src/main/java/com/github/dockerjava/api/command/StatsCmd.java b/src/main/java/com/github/dockerjava/api/command/StatsCmd.java new file mode 100644 index 000000000..a605d4267 --- /dev/null +++ b/src/main/java/com/github/dockerjava/api/command/StatsCmd.java @@ -0,0 +1,22 @@ +package com.github.dockerjava.api.command; + +import java.util.concurrent.ExecutorService; + + +/** + * Get stats + * + */ +public interface StatsCmd extends DockerCmd { + public StatsCmd withContainerId(String containerId); + + public String getContainerId(); + + public StatsCmd withStatsCallback(StatsCallback statsCallback); + + public StatsCallback getStatsCallback(); + + public static interface Exec extends DockerCmdExec { + } + +} diff --git a/src/main/java/com/github/dockerjava/api/model/Statistics.java b/src/main/java/com/github/dockerjava/api/model/Statistics.java new file mode 100644 index 000000000..ff1d6dc95 --- /dev/null +++ b/src/main/java/com/github/dockerjava/api/model/Statistics.java @@ -0,0 +1,51 @@ +package com.github.dockerjava.api.model; + +import java.util.Map; + +import org.apache.commons.lang.builder.ToStringBuilder; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Representation of a Docker statistics. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class Statistics { + + @JsonProperty("read") + private String read; + + @JsonProperty("network") + private Map networkStats; + + @JsonProperty("memory_stats") + private Map memoryStats; + + @JsonProperty("blkio_stats") + private Map blkioStats; + + @JsonProperty("cpu_stats") + private Map cpuStats; + + public Map getNetworkStats() { + return networkStats; + } + + public Map getCpuStats() { + return cpuStats; + } + + public Map getMemoryStats() { + return memoryStats; + } + + public Map getBlkioStats() { + return blkioStats; + } + + @Override + public String toString() { + return ToStringBuilder.reflectionToString(this); + } +} diff --git a/src/main/java/com/github/dockerjava/core/DockerClientImpl.java b/src/main/java/com/github/dockerjava/core/DockerClientImpl.java index 534c58ebf..db64d2843 100644 --- a/src/main/java/com/github/dockerjava/core/DockerClientImpl.java +++ b/src/main/java/com/github/dockerjava/core/DockerClientImpl.java @@ -336,6 +336,12 @@ public EventsCmd eventsCmd(EventCallback eventCallback) { return new EventsCmdImpl(getDockerCmdExecFactory() .createEventsCmdExec(), eventCallback); } + + @Override + public StatsCmd statsCmd(StatsCallback statsCallback) { + return new StatsCmdImpl(getDockerCmdExecFactory() + .createStatsCmdExec(), statsCallback); + } @Override public void close() throws IOException { diff --git a/src/main/java/com/github/dockerjava/core/command/StatsCmdImpl.java b/src/main/java/com/github/dockerjava/core/command/StatsCmdImpl.java new file mode 100644 index 000000000..33fcf6371 --- /dev/null +++ b/src/main/java/com/github/dockerjava/core/command/StatsCmdImpl.java @@ -0,0 +1,61 @@ +package com.github.dockerjava.core.command; + +import static com.google.common.base.Preconditions.checkNotNull; + +import java.util.concurrent.ExecutorService; + +import com.github.dockerjava.api.command.EventCallback; +import com.github.dockerjava.api.command.EventsCmd; +import com.github.dockerjava.api.command.StatsCallback; +import com.github.dockerjava.api.command.StatsCmd; +import com.github.dockerjava.api.command.TopContainerCmd; + +/** + * Stream docker stats + */ +public class StatsCmdImpl extends AbstrDockerCmd implements StatsCmd { + + private String containerId; + private StatsCallback statsCallback; + + public StatsCmdImpl(StatsCmd.Exec exec, StatsCallback statsCallback) { + super(exec); + withStatsCallback(statsCallback); + } + + @Override + public StatsCmd withContainerId(String containerId) { + checkNotNull(containerId, "containerId was not specified"); + this.containerId = containerId; + return this; + } + + @Override + public String getContainerId() { + return containerId; + } + + @Override + public StatsCmd withStatsCallback(StatsCallback statsCallback) { + this.statsCallback = statsCallback; + return this; + } + + + @Override + public StatsCallback getStatsCallback() { + return statsCallback; + } + + @Override + public ExecutorService exec() { + return super.exec(); + } + + @Override + public String toString() { + return new StringBuilder("stats") + .append(containerId != null ? " --id=" + containerId : "") + .toString(); + } +} diff --git a/src/main/java/com/github/dockerjava/jaxrs/DockerCmdExecFactoryImpl.java b/src/main/java/com/github/dockerjava/jaxrs/DockerCmdExecFactoryImpl.java index bc3afecad..10c039188 100644 --- a/src/main/java/com/github/dockerjava/jaxrs/DockerCmdExecFactoryImpl.java +++ b/src/main/java/com/github/dockerjava/jaxrs/DockerCmdExecFactoryImpl.java @@ -312,6 +312,11 @@ public UnpauseContainerCmd.Exec createUnpauseContainerCmdExec() { public EventsCmd.Exec createEventsCmdExec() { return new EventsCmdExec(getBaseResource()); } + + @Override + public StatsCmd.Exec createStatsCmdExec() { + return new StatsCmdExec(getBaseResource()); + } @Override public void close() throws IOException { diff --git a/src/main/java/com/github/dockerjava/jaxrs/StatsCmdExec.java b/src/main/java/com/github/dockerjava/jaxrs/StatsCmdExec.java new file mode 100644 index 000000000..97b9a2754 --- /dev/null +++ b/src/main/java/com/github/dockerjava/jaxrs/StatsCmdExec.java @@ -0,0 +1,90 @@ +package com.github.dockerjava.jaxrs; + +import static com.google.common.base.Preconditions.checkNotNull; + +import java.io.InputStream; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Response; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.dockerjava.api.command.StatsCallback; +import com.github.dockerjava.api.command.StatsCmd; +import com.github.dockerjava.api.model.Statistics; +import com.github.dockerjava.jaxrs.util.WrappedResponseInputStream; + +public class StatsCmdExec extends AbstrDockerCmdExec implements StatsCmd.Exec { + private static final Logger LOGGER = LoggerFactory.getLogger(StatsCmdExec.class); + + public StatsCmdExec(WebTarget baseResource) { + super(baseResource); + } + + @Override + protected ExecutorService execute(StatsCmd command) { + ExecutorService executorService = Executors.newSingleThreadExecutor(); + + WebTarget webResource = getBaseResource().path("/containers/{id}/stats") + .resolveTemplate("id", command.getContainerId()); + + LOGGER.trace("GET: {}", webResource); + StatsNotifier eventNotifier = StatsNotifier.create(command.getStatsCallback(), webResource); + executorService.submit(eventNotifier); + return executorService; + } + + private static class StatsNotifier implements Callable { + private static final JsonFactory JSON_FACTORY = new JsonFactory(); + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + private final StatsCallback statsCallback; + private final WebTarget webTarget; + + private StatsNotifier(StatsCallback statsCallback, WebTarget webTarget) { + this.statsCallback = statsCallback; + this.webTarget = webTarget; + } + + public static StatsNotifier create(StatsCallback statsCallback, WebTarget webTarget) { + checkNotNull(statsCallback, "An StatsCallback must be provided"); + checkNotNull(webTarget, "An WebTarget must be provided"); + return new StatsNotifier(statsCallback, webTarget); + } + + @Override + public Void call() throws Exception { + int numStats=0; + Response response = null; + try { + response = webTarget.request().get(Response.class); + InputStream inputStream = new WrappedResponseInputStream(response); + JsonParser jp = JSON_FACTORY.createParser(inputStream); + while (jp.nextToken() != JsonToken.END_OBJECT && !jp.isClosed() && statsCallback.isReceiving()) { + statsCallback.onStats(OBJECT_MAPPER.readValue(jp, Statistics.class)); + numStats++; + } + statsCallback.onCompletion(numStats); + LOGGER.info("Finished collecting stats"); + return null ; + } + catch(Throwable t) { + statsCallback.onException(t); + } + finally { + if (response != null) { + response.close(); + } + } + return null ; + } + } +} diff --git a/src/test/java/com/github/dockerjava/client/AbstractDockerClientTest.java b/src/test/java/com/github/dockerjava/client/AbstractDockerClientTest.java index 1e61b9a7d..b04ff63ba 100644 --- a/src/test/java/com/github/dockerjava/client/AbstractDockerClientTest.java +++ b/src/test/java/com/github/dockerjava/client/AbstractDockerClientTest.java @@ -60,7 +60,7 @@ private DockerClientConfig config() { protected DockerClientConfig config(String password) { DockerClientConfig.DockerClientConfigBuilder builder = DockerClientConfig.createDefaultConfigBuilder() - .withServerAddress("https://index.docker.io/v1/"); + .withServerAddress("https://index.docker.io/v1/").withMaxTotalConnections(5).withMaxPerRouteConnections(5); if (password!=null) { builder = builder.withPassword(password); } diff --git a/src/test/java/com/github/dockerjava/core/TestDockerCmdExecFactory.java b/src/test/java/com/github/dockerjava/core/TestDockerCmdExecFactory.java index 869a12fcf..fb764474f 100644 --- a/src/test/java/com/github/dockerjava/core/TestDockerCmdExecFactory.java +++ b/src/test/java/com/github/dockerjava/core/TestDockerCmdExecFactory.java @@ -258,6 +258,11 @@ public UnpauseContainerCmd.Exec createUnpauseContainerCmdExec() { public EventsCmd.Exec createEventsCmdExec() { return delegate.createEventsCmdExec(); } + + @Override + public StatsCmd.Exec createStatsCmdExec() { + return delegate.createStatsCmdExec(); + } public List getContainerNames() { return new ArrayList(containerNames); diff --git a/src/test/java/com/github/dockerjava/core/command/StatsCmdImplTest.java b/src/test/java/com/github/dockerjava/core/command/StatsCmdImplTest.java new file mode 100644 index 000000000..c5e575373 --- /dev/null +++ b/src/test/java/com/github/dockerjava/core/command/StatsCmdImplTest.java @@ -0,0 +1,133 @@ +package com.github.dockerjava.core.command; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.isEmptyString; +import static org.hamcrest.Matchers.not; + +import com.github.dockerjava.api.DockerException; +import com.github.dockerjava.api.command.CreateContainerResponse; +import com.github.dockerjava.api.command.StatsCallback; +import com.github.dockerjava.api.command.StatsCmd; +import com.github.dockerjava.api.model.Statistics; +import com.github.dockerjava.client.AbstractDockerClientTest; + +import org.testng.ITestResult; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.AfterTest; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.lang.reflect.Method; +import java.security.SecureRandom; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +@Test(groups = "integration") +public class StatsCmdImplTest extends AbstractDockerClientTest { + + private static int NUM_STATS = 5; + + @BeforeTest + public void beforeTest() throws DockerException { + super.beforeTest(); + } + + @AfterTest + public void afterTest() { + super.afterTest(); + } + + @BeforeMethod + public void beforeMethod(Method method) { + super.beforeMethod(method); + } + + @AfterMethod + public void afterMethod(ITestResult result) { + super.afterMethod(result); + } + + @Test + public void testStatsStreaming() throws InterruptedException, IOException { + TimeUnit.SECONDS.sleep(1); + + CountDownLatch countDownLatch = new CountDownLatch(NUM_STATS); + StatsCallbackTest statsCallback = new StatsCallbackTest(countDownLatch); + + String containerName = "generated_" + new SecureRandom().nextInt(); + + CreateContainerResponse container = dockerClient + .createContainerCmd("busybox") + .withCmd("top") + .withName(containerName).exec(); + LOG.info("Created container {}", container.toString()); + assertThat(container.getId(), not(isEmptyString())); + + dockerClient.startContainerCmd(container.getId()).exec(); + + StatsCmd statsCmd = dockerClient.statsCmd(statsCallback).withContainerId(container.getId()); + ExecutorService executorService = statsCmd.exec(); + + countDownLatch.await(3, TimeUnit.SECONDS); + boolean gotStats = statsCallback.gotStats(); + + LOG.info("Stop stats collection"); + executorService.shutdown(); + statsCallback.close(); + + LOG.info("Stopping container"); + dockerClient.stopContainerCmd(container.getId()).exec(); + dockerClient.removeContainerCmd(container.getId()).exec(); + + LOG.info("Completed test"); + assertTrue(gotStats, "Expected true"); + + } + + private class StatsCallbackTest implements StatsCallback { + private final CountDownLatch countDownLatch; + private final AtomicBoolean isReceiving = new AtomicBoolean(true); + private boolean gotStats = false; + + public StatsCallbackTest(CountDownLatch countDownLatch) { + this.countDownLatch = countDownLatch; + } + + public void close() { + LOG.info("Closing StatsCallback"); + isReceiving.set(false); + } + + @Override + public void onStats(Statistics stats) { + LOG.info("Received stats #{}: {}", countDownLatch.getCount(), stats); + if(stats != null) { + gotStats = true; + } + countDownLatch.countDown(); + } + + @Override + public void onException(Throwable throwable) { + LOG.error("Error occurred: {}", throwable.getMessage()); + } + + @Override + public void onCompletion(int numStats) { + LOG.info("Number of stats received: {}", numStats); + } + + @Override + public boolean isReceiving() { + return isReceiving.get(); + } + + public boolean gotStats() { + return gotStats; + } + } +}