Repository: docker-java/docker-java
Pull request #189 (2015-04-02 00:35:11)

Add docker stats support
Add docker stats support for docker 1.5+
It works in the same ways as events (async)
Also added a test case

@marcuslinke 
Please review and merge


Patch:
diff --git a/src/main/java/com/github/dockerjava/api/DockerClient.java b/src/main/java/com/github/dockerjava/api/DockerClient.java
index 9e275eb3..723c1bad 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 interface DockerClient extends Closeable {
 
 	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 8b988074..294d5099 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 00000000..610c0f83
--- /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 00000000..a605d426
--- /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<ExecutorService> {
+    public StatsCmd withContainerId(String containerId);
+    
+    public String getContainerId();
+    
+    public StatsCmd withStatsCallback(StatsCallback statsCallback);
+    
+    public StatsCallback getStatsCallback();
+
+    public static interface Exec extends DockerCmdExec<StatsCmd, ExecutorService> {
+    }
+
+}
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 00000000..ff1d6dc9
--- /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<String,Object> networkStats;
+
+    @JsonProperty("memory_stats")
+    private Map<String,Object> memoryStats;
+    
+    @JsonProperty("blkio_stats")
+    private Map<String,Object> blkioStats;
+    
+    @JsonProperty("cpu_stats")
+    private Map<String,Object> cpuStats;
+
+    public Map<String,Object> getNetworkStats() {
+        return networkStats;
+    }
+    
+    public Map<String,Object> getCpuStats() {
+        return cpuStats;
+    }
+    
+    public Map<String,Object> getMemoryStats() {
+        return memoryStats;
+    }
+    
+    public Map<String,Object> 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 534c58eb..db64d284 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 class DockerClientImpl implements Closeable, DockerClient {
 		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 00000000..33fcf637
--- /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<StatsCmd, ExecutorService> 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 bc3afeca..10c03918 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 class DockerCmdExecFactoryImpl implements DockerCmdExecFactory {
 	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 00000000..97b9a275
--- /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<StatsCmd, ExecutorService> 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<Void> {
+        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 ;
+        }
+    }
+}


Test patch:
diff --git a/src/test/java/com/github/dockerjava/client/AbstractDockerClientTest.java b/src/test/java/com/github/dockerjava/client/AbstractDockerClientTest.java
index 1e61b9a7..b04ff63b 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 @@ public abstract class AbstractDockerClientTest extends Assert {
 
     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 869a12fc..fb764474 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 class TestDockerCmdExecFactory implements DockerCmdExecFactory {
 	public EventsCmd.Exec createEventsCmdExec() {
 		return delegate.createEventsCmdExec();
 	}
+	
+	@Override
+    public StatsCmd.Exec createStatsCmdExec() {
+	    return delegate.createStatsCmdExec();
+	}
 
 	public List<String> getContainerNames() {
 		return new ArrayList<String>(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 00000000..c5e57537
--- /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;
+	    }
+	}
+}