Browse code

Allow docker stats without arguments

This patch adds the ability to run `docker stats` w/o arguments and get
statistics for all running containers by default. Also add a new
`--all` flag to list statistics for all containers (like `docker ps`).
New running containers are added to the list as they show up also.
Add integration tests for this new behavior.
Docs updated accordingly. Fix missing stuff in man/commandline
reference for `docker stats`.

Signed-off-by: Antonio Murdaca <runcom@redhat.com>

Antonio Murdaca authored on 2015/10/03 21:53:25
Showing 6 changed files
... ...
@@ -13,7 +13,7 @@ import (
13 13
 
14 14
 	"github.com/docker/docker/api/types"
15 15
 	Cli "github.com/docker/docker/cli"
16
-	flag "github.com/docker/docker/pkg/mflag"
16
+	"github.com/docker/docker/pkg/jsonmessage"
17 17
 	"github.com/docker/docker/pkg/units"
18 18
 )
19 19
 
... ...
@@ -31,6 +31,11 @@ type containerStats struct {
31 31
 	err              error
32 32
 }
33 33
 
34
+type stats struct {
35
+	mu sync.Mutex
36
+	cs []*containerStats
37
+}
38
+
34 39
 func (s *containerStats) Collect(cli *DockerCli, streamStats bool) {
35 40
 	v := url.Values{}
36 41
 	if streamStats {
... ...
@@ -139,18 +144,41 @@ func (s *containerStats) Display(w io.Writer) error {
139 139
 //
140 140
 // This shows real-time information on CPU usage, memory usage, and network I/O.
141 141
 //
142
-// Usage: docker stats CONTAINER [CONTAINER...]
142
+// Usage: docker stats [OPTIONS] [CONTAINER...]
143 143
 func (cli *DockerCli) CmdStats(args ...string) error {
144
-	cmd := Cli.Subcmd("stats", []string{"CONTAINER [CONTAINER...]"}, Cli.DockerCommands["stats"].Description, true)
144
+	cmd := Cli.Subcmd("stats", []string{"[CONTAINER...]"}, Cli.DockerCommands["stats"].Description, true)
145
+	all := cmd.Bool([]string{"a", "-all"}, false, "Show all containers (default shows just running)")
145 146
 	noStream := cmd.Bool([]string{"-no-stream"}, false, "Disable streaming stats and only pull the first result")
146
-	cmd.Require(flag.Min, 1)
147 147
 
148 148
 	cmd.ParseFlags(args, true)
149 149
 
150 150
 	names := cmd.Args()
151
+	showAll := len(names) == 0
152
+
153
+	if showAll {
154
+		v := url.Values{}
155
+		if *all {
156
+			v.Set("all", "1")
157
+		}
158
+		body, _, err := readBody(cli.call("GET", "/containers/json?"+v.Encode(), nil, nil))
159
+		if err != nil {
160
+			return err
161
+		}
162
+		var cs []types.Container
163
+		if err := json.Unmarshal(body, &cs); err != nil {
164
+			return err
165
+		}
166
+		for _, c := range cs {
167
+			names = append(names, c.ID[:12])
168
+		}
169
+	}
170
+	if len(names) == 0 && !showAll {
171
+		return fmt.Errorf("No containers found")
172
+	}
151 173
 	sort.Strings(names)
174
+
152 175
 	var (
153
-		cStats []*containerStats
176
+		cStats = stats{}
154 177
 		w      = tabwriter.NewWriter(cli.out, 20, 1, 3, ' ', 0)
155 178
 	)
156 179
 	printHeader := func() {
... ...
@@ -162,42 +190,125 @@ func (cli *DockerCli) CmdStats(args ...string) error {
162 162
 	}
163 163
 	for _, n := range names {
164 164
 		s := &containerStats{Name: n}
165
-		cStats = append(cStats, s)
165
+		// no need to lock here since only the main goroutine is running here
166
+		cStats.cs = append(cStats.cs, s)
166 167
 		go s.Collect(cli, !*noStream)
167 168
 	}
169
+	closeChan := make(chan error)
170
+	if showAll {
171
+		type watch struct {
172
+			cid   string
173
+			event string
174
+			err   error
175
+		}
176
+		getNewContainers := func(c chan<- watch) {
177
+			res, err := cli.call("GET", "/events", nil, nil)
178
+			if err != nil {
179
+				c <- watch{err: err}
180
+				return
181
+			}
182
+			defer res.body.Close()
183
+
184
+			dec := json.NewDecoder(res.body)
185
+			for {
186
+				var j *jsonmessage.JSONMessage
187
+				if err := dec.Decode(&j); err != nil {
188
+					c <- watch{err: err}
189
+					return
190
+				}
191
+				c <- watch{j.ID[:12], j.Status, nil}
192
+			}
193
+		}
194
+		go func(stopChan chan<- error) {
195
+			cChan := make(chan watch)
196
+			go getNewContainers(cChan)
197
+			for {
198
+				c := <-cChan
199
+				if c.err != nil {
200
+					stopChan <- c.err
201
+					return
202
+				}
203
+				switch c.event {
204
+				case "create":
205
+					s := &containerStats{Name: c.cid}
206
+					cStats.mu.Lock()
207
+					cStats.cs = append(cStats.cs, s)
208
+					cStats.mu.Unlock()
209
+					go s.Collect(cli, !*noStream)
210
+				case "stop":
211
+				case "die":
212
+					if !*all {
213
+						var remove int
214
+						// cStats cannot be O(1) with a map cause ranging over it would cause
215
+						// containers in stats to move up and down in the list...:(
216
+						cStats.mu.Lock()
217
+						for i, s := range cStats.cs {
218
+							if s.Name == c.cid {
219
+								remove = i
220
+								break
221
+							}
222
+						}
223
+						cStats.cs = append(cStats.cs[:remove], cStats.cs[remove+1:]...)
224
+						cStats.mu.Unlock()
225
+					}
226
+				}
227
+			}
228
+		}(closeChan)
229
+	} else {
230
+		close(closeChan)
231
+	}
168 232
 	// do a quick pause so that any failed connections for containers that do not exist are able to be
169 233
 	// evicted before we display the initial or default values.
170 234
 	time.Sleep(1500 * time.Millisecond)
171 235
 	var errs []string
172
-	for _, c := range cStats {
236
+	cStats.mu.Lock()
237
+	for _, c := range cStats.cs {
173 238
 		c.mu.Lock()
174 239
 		if c.err != nil {
175 240
 			errs = append(errs, fmt.Sprintf("%s: %v", c.Name, c.err))
176 241
 		}
177 242
 		c.mu.Unlock()
178 243
 	}
244
+	cStats.mu.Unlock()
179 245
 	if len(errs) > 0 {
180 246
 		return fmt.Errorf("%s", strings.Join(errs, ", "))
181 247
 	}
182 248
 	for range time.Tick(500 * time.Millisecond) {
183 249
 		printHeader()
184 250
 		toRemove := []int{}
185
-		for i, s := range cStats {
251
+		cStats.mu.Lock()
252
+		for i, s := range cStats.cs {
186 253
 			if err := s.Display(w); err != nil && !*noStream {
187 254
 				toRemove = append(toRemove, i)
188 255
 			}
189 256
 		}
190 257
 		for j := len(toRemove) - 1; j >= 0; j-- {
191 258
 			i := toRemove[j]
192
-			cStats = append(cStats[:i], cStats[i+1:]...)
259
+			cStats.cs = append(cStats.cs[:i], cStats.cs[i+1:]...)
193 260
 		}
194
-		if len(cStats) == 0 {
261
+		if len(cStats.cs) == 0 && !showAll {
195 262
 			return nil
196 263
 		}
264
+		cStats.mu.Unlock()
197 265
 		w.Flush()
198 266
 		if *noStream {
199 267
 			break
200 268
 		}
269
+		select {
270
+		case err, ok := <-closeChan:
271
+			if ok {
272
+				if err != nil {
273
+					// this is suppressing "unexpected EOF" in the cli when the
274
+					// daemon restarts so it shudowns cleanly
275
+					if err == io.ErrUnexpectedEOF {
276
+						return nil
277
+					}
278
+					return err
279
+				}
280
+			}
281
+		default:
282
+			// just skip
283
+		}
201 284
 	}
202 285
 	return nil
203 286
 }
... ...
@@ -24,7 +24,6 @@ type ContainerStatsConfig struct {
24 24
 // ContainerStats writes information about the container to the stream
25 25
 // given in the config object.
26 26
 func (daemon *Daemon) ContainerStats(prefixOrName string, config *ContainerStatsConfig) error {
27
-
28 27
 	container, err := daemon.Get(prefixOrName)
29 28
 	if err != nil {
30 29
 		return err
... ...
@@ -10,24 +10,31 @@ parent = "smn_cli"
10 10
 
11 11
 # stats
12 12
 
13
-    Usage: docker stats [OPTIONS] CONTAINER [CONTAINER...]
13
+    Usage: docker stats [OPTIONS] [CONTAINER...]
14 14
 
15 15
     Display a live stream of one or more containers' resource usage statistics
16 16
 
17
+      -a, --all=false    Show all containers (default shows just running)
17 18
       --help=false       Print usage
18 19
       --no-stream=false  Disable streaming stats and only pull the first result
19 20
 
20
-Running `docker stats` on multiple containers
21
+The `docker stats` command returns a live data stream for running containers. To limit data to one or more specific containers, specify a list of container names or ids separated by a space. You can specify a stopped container but stopped containers do not return any data.
21 22
 
22
-    $ docker stats redis1 redis2
23
+If you want more detailed information about a container's resource usage, use the `/containers/(id)/stats` API endpoint. 
24
+
25
+## Examples
26
+
27
+Running `docker stats` on all running containers
28
+
29
+    $ docker stats
23 30
     CONTAINER           CPU %               MEM USAGE / LIMIT     MEM %               NET I/O             BLOCK I/O
24 31
     redis1              0.07%               796 KB / 64 MB        1.21%               788 B / 648 B       3.568 MB / 512 KB
25 32
     redis2              0.07%               2.746 MB / 64 MB      4.29%               1.266 KB / 648 B    12.4 MB / 0 B
33
+    nginx1              0.03%               4.583 MB / 64 MB      6.30%               2.854 KB / 648 B    27.7 MB / 0 B
26 34
 
35
+Running `docker stats` on multiple containers by name and id.
27 36
 
28
-The `docker stats` command will only return a live stream of data for running
29
-containers. Stopped containers will not return any data.
30
-
31
-> **Note:**
32
-> If you want more detailed information about a container's resource
33
-> usage, use the API endpoint.
37
+    $ docker stats fervent_panini 5acfcb1b4fd1
38
+    CONTAINER           CPU %               MEM USAGE/LIMIT     MEM %               NET I/O
39
+    5acfcb1b4fd1        0.00%               115.2 MB/1.045 GB   11.03%              1.422 kB/648 B
40
+    fervent_panini      0.02%               11.08 MB/1.045 GB   1.06%               648 B/648 B
... ...
@@ -206,6 +206,7 @@ func (s *DockerSuite) TestHelpTextVerify(c *check.C) {
206 206
 				"login":   "",
207 207
 				"logout":  "",
208 208
 				"network": "",
209
+				"stats":   "",
209 210
 			}
210 211
 
211 212
 			if _, ok := noShortUsage[cmd]; !ok {
... ...
@@ -1,7 +1,9 @@
1 1
 package main
2 2
 
3 3
 import (
4
+	"bufio"
4 5
 	"os/exec"
6
+	"regexp"
5 7
 	"strings"
6 8
 	"time"
7 9
 
... ...
@@ -48,3 +50,80 @@ func (s *DockerSuite) TestStatsContainerNotFound(c *check.C) {
48 48
 	c.Assert(err, checker.NotNil)
49 49
 	c.Assert(out, checker.Contains, "no such id: notfound", check.Commentf("Expected to fail on not found container stats with --no-stream, got %q instead", out))
50 50
 }
51
+
52
+func (s *DockerSuite) TestStatsAllRunningNoStream(c *check.C) {
53
+	testRequires(c, DaemonIsLinux)
54
+
55
+	out, _ := dockerCmd(c, "run", "-d", "busybox", "top")
56
+	id1 := strings.TrimSpace(out)[:12]
57
+	c.Assert(waitRun(id1), check.IsNil)
58
+	out, _ = dockerCmd(c, "run", "-d", "busybox", "top")
59
+	id2 := strings.TrimSpace(out)[:12]
60
+	c.Assert(waitRun(id2), check.IsNil)
61
+	out, _ = dockerCmd(c, "run", "-d", "busybox", "top")
62
+	id3 := strings.TrimSpace(out)[:12]
63
+	c.Assert(waitRun(id3), check.IsNil)
64
+	dockerCmd(c, "stop", id3)
65
+
66
+	out, _ = dockerCmd(c, "stats", "--no-stream")
67
+	if !strings.Contains(out, id1) || !strings.Contains(out, id2) {
68
+		c.Fatalf("Expected stats output to contain both %s and %s, got %s", id1, id2, out)
69
+	}
70
+	if strings.Contains(out, id3) {
71
+		c.Fatalf("Did not expect %s in stats, got %s", id3, out)
72
+	}
73
+}
74
+
75
+func (s *DockerSuite) TestStatsAllNoStream(c *check.C) {
76
+	testRequires(c, DaemonIsLinux)
77
+
78
+	out, _ := dockerCmd(c, "run", "-d", "busybox", "top")
79
+	id1 := strings.TrimSpace(out)[:12]
80
+	c.Assert(waitRun(id1), check.IsNil)
81
+	dockerCmd(c, "stop", id1)
82
+	out, _ = dockerCmd(c, "run", "-d", "busybox", "top")
83
+	id2 := strings.TrimSpace(out)[:12]
84
+	c.Assert(waitRun(id2), check.IsNil)
85
+
86
+	out, _ = dockerCmd(c, "stats", "--all", "--no-stream")
87
+	if !strings.Contains(out, id1) || !strings.Contains(out, id2) {
88
+		c.Fatalf("Expected stats output to contain both %s and %s, got %s", id1, id2, out)
89
+	}
90
+}
91
+
92
+func (s *DockerSuite) TestStatsAllNewContainersAdded(c *check.C) {
93
+	testRequires(c, DaemonIsLinux)
94
+
95
+	id := make(chan string)
96
+	addedChan := make(chan struct{})
97
+
98
+	dockerCmd(c, "run", "-d", "busybox", "top")
99
+	statsCmd := exec.Command(dockerBinary, "stats")
100
+	stdout, err := statsCmd.StdoutPipe()
101
+	c.Assert(err, check.IsNil)
102
+	c.Assert(statsCmd.Start(), check.IsNil)
103
+	defer statsCmd.Process.Kill()
104
+
105
+	go func() {
106
+		containerID := <-id
107
+		matchID := regexp.MustCompile(containerID)
108
+
109
+		scanner := bufio.NewScanner(stdout)
110
+		for scanner.Scan() {
111
+			switch {
112
+			case matchID.MatchString(scanner.Text()):
113
+				close(addedChan)
114
+			}
115
+		}
116
+	}()
117
+
118
+	out, _ := dockerCmd(c, "run", "-d", "busybox", "top")
119
+	id <- strings.TrimSpace(out)[:12]
120
+
121
+	select {
122
+	case <-time.After(5 * time.Second):
123
+		c.Fatal("failed to observe new container created added to stats")
124
+	case <-addedChan:
125
+		// ignore, done
126
+	}
127
+}
... ...
@@ -6,15 +6,19 @@ docker-stats - Display a live stream of one or more containers' resource usage s
6 6
 
7 7
 # SYNOPSIS
8 8
 **docker stats**
9
+[**-a**|**--all**[=*false*]]
9 10
 [**--help**]
10 11
 [**--no-stream**[=*false*]]
11
-CONTAINER [CONTAINER...]
12
+[CONTAINER...]
12 13
 
13 14
 # DESCRIPTION
14 15
 
15 16
 Display a live stream of one or more containers' resource usage statistics
16 17
 
17 18
 # OPTIONS
19
+**-a**, **--all**=*true*|*false*
20
+   Show all containers. Only running containers are shown by default. The default is *false*.
21
+
18 22
 **--help**
19 23
   Print usage statement
20 24
 
... ...
@@ -23,9 +27,17 @@ Display a live stream of one or more containers' resource usage statistics
23 23
 
24 24
 # EXAMPLES
25 25
 
26
-Run **docker stats** with multiple containers.
26
+Running `docker stats` on all running containers
27 27
 
28
-    $ docker stats redis1 redis2
28
+    $ docker stats
29 29
     CONTAINER           CPU %               MEM USAGE / LIMIT     MEM %               NET I/O             BLOCK I/O
30 30
     redis1              0.07%               796 KB / 64 MB        1.21%               788 B / 648 B       3.568 MB / 512 KB
31 31
     redis2              0.07%               2.746 MB / 64 MB      4.29%               1.266 KB / 648 B    12.4 MB / 0 B
32
+    nginx1              0.03%               4.583 MB / 64 MB      6.30%               2.854 KB / 648 B    27.7 MB / 0 B
33
+
34
+Running `docker stats` on multiple containers by name and id.
35
+
36
+    $ docker stats fervent_panini 5acfcb1b4fd1
37
+    CONTAINER           CPU %               MEM USAGE/LIMIT     MEM %               NET I/O
38
+    5acfcb1b4fd1        0.00%               115.2 MB/1.045 GB   11.03%              1.422 kB/648 B
39
+    fervent_panini      0.02%               11.08 MB/1.045 GB   1.06%               648 B/648 B