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>
| ... | ... |
@@ -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 |
| ... | ... |
@@ -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 |