From ae818a820f49a9bd7ea8b753f124747fc548e501 Mon Sep 17 00:00:00 2001 From: Antonio Murdaca Date: Sat, 3 Oct 2015 14:53:25 +0200 Subject: [PATCH] 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 --- api/client/stats.go | 131 +++++++++++++++++++++-- daemon/stats.go | 1 - docs/reference/commandline/stats.md | 25 +++-- integration-cli/docker_cli_help_test.go | 1 + integration-cli/docker_cli_stats_test.go | 79 ++++++++++++++ man/docker-stats.1.md | 18 +++- 6 files changed, 232 insertions(+), 23 deletions(-) diff --git a/api/client/stats.go b/api/client/stats.go index 360d132d3b..151e602ee5 100644 --- a/api/client/stats.go +++ b/api/client/stats.go @@ -13,7 +13,7 @@ import ( "github.com/docker/docker/api/types" Cli "github.com/docker/docker/cli" - flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/docker/pkg/jsonmessage" "github.com/docker/docker/pkg/units" ) @@ -31,6 +31,11 @@ type containerStats struct { err error } +type stats struct { + mu sync.Mutex + cs []*containerStats +} + func (s *containerStats) Collect(cli *DockerCli, streamStats bool) { v := url.Values{} if streamStats { @@ -139,18 +144,41 @@ func (s *containerStats) Display(w io.Writer) error { // // This shows real-time information on CPU usage, memory usage, and network I/O. // -// Usage: docker stats CONTAINER [CONTAINER...] +// Usage: docker stats [OPTIONS] [CONTAINER...] func (cli *DockerCli) CmdStats(args ...string) error { - cmd := Cli.Subcmd("stats", []string{"CONTAINER [CONTAINER...]"}, Cli.DockerCommands["stats"].Description, true) + cmd := Cli.Subcmd("stats", []string{"[CONTAINER...]"}, Cli.DockerCommands["stats"].Description, true) + all := cmd.Bool([]string{"a", "-all"}, false, "Show all containers (default shows just running)") noStream := cmd.Bool([]string{"-no-stream"}, false, "Disable streaming stats and only pull the first result") - cmd.Require(flag.Min, 1) cmd.ParseFlags(args, true) names := cmd.Args() + showAll := len(names) == 0 + + if showAll { + v := url.Values{} + if *all { + v.Set("all", "1") + } + body, _, err := readBody(cli.call("GET", "/containers/json?"+v.Encode(), nil, nil)) + if err != nil { + return err + } + var cs []types.Container + if err := json.Unmarshal(body, &cs); err != nil { + return err + } + for _, c := range cs { + names = append(names, c.ID[:12]) + } + } + if len(names) == 0 && !showAll { + return fmt.Errorf("No containers found") + } sort.Strings(names) + var ( - cStats []*containerStats + cStats = stats{} w = tabwriter.NewWriter(cli.out, 20, 1, 3, ' ', 0) ) printHeader := func() { @@ -162,42 +190,125 @@ func (cli *DockerCli) CmdStats(args ...string) error { } for _, n := range names { s := &containerStats{Name: n} - cStats = append(cStats, s) + // no need to lock here since only the main goroutine is running here + cStats.cs = append(cStats.cs, s) go s.Collect(cli, !*noStream) } + closeChan := make(chan error) + if showAll { + type watch struct { + cid string + event string + err error + } + getNewContainers := func(c chan<- watch) { + res, err := cli.call("GET", "/events", nil, nil) + if err != nil { + c <- watch{err: err} + return + } + defer res.body.Close() + + dec := json.NewDecoder(res.body) + for { + var j *jsonmessage.JSONMessage + if err := dec.Decode(&j); err != nil { + c <- watch{err: err} + return + } + c <- watch{j.ID[:12], j.Status, nil} + } + } + go func(stopChan chan<- error) { + cChan := make(chan watch) + go getNewContainers(cChan) + for { + c := <-cChan + if c.err != nil { + stopChan <- c.err + return + } + switch c.event { + case "create": + s := &containerStats{Name: c.cid} + cStats.mu.Lock() + cStats.cs = append(cStats.cs, s) + cStats.mu.Unlock() + go s.Collect(cli, !*noStream) + case "stop": + case "die": + if !*all { + var remove int + // cStats cannot be O(1) with a map cause ranging over it would cause + // containers in stats to move up and down in the list...:( + cStats.mu.Lock() + for i, s := range cStats.cs { + if s.Name == c.cid { + remove = i + break + } + } + cStats.cs = append(cStats.cs[:remove], cStats.cs[remove+1:]...) + cStats.mu.Unlock() + } + } + } + }(closeChan) + } else { + close(closeChan) + } // do a quick pause so that any failed connections for containers that do not exist are able to be // evicted before we display the initial or default values. time.Sleep(1500 * time.Millisecond) var errs []string - for _, c := range cStats { + cStats.mu.Lock() + for _, c := range cStats.cs { c.mu.Lock() if c.err != nil { errs = append(errs, fmt.Sprintf("%s: %v", c.Name, c.err)) } c.mu.Unlock() } + cStats.mu.Unlock() if len(errs) > 0 { return fmt.Errorf("%s", strings.Join(errs, ", ")) } for range time.Tick(500 * time.Millisecond) { printHeader() toRemove := []int{} - for i, s := range cStats { + cStats.mu.Lock() + for i, s := range cStats.cs { if err := s.Display(w); err != nil && !*noStream { toRemove = append(toRemove, i) } } for j := len(toRemove) - 1; j >= 0; j-- { i := toRemove[j] - cStats = append(cStats[:i], cStats[i+1:]...) + cStats.cs = append(cStats.cs[:i], cStats.cs[i+1:]...) } - if len(cStats) == 0 { + if len(cStats.cs) == 0 && !showAll { return nil } + cStats.mu.Unlock() w.Flush() if *noStream { break } + select { + case err, ok := <-closeChan: + if ok { + if err != nil { + // this is suppressing "unexpected EOF" in the cli when the + // daemon restarts so it shudowns cleanly + if err == io.ErrUnexpectedEOF { + return nil + } + return err + } + } + default: + // just skip + } } return nil } diff --git a/daemon/stats.go b/daemon/stats.go index ae3a2d918b..f8f0e2e61a 100644 --- a/daemon/stats.go +++ b/daemon/stats.go @@ -24,7 +24,6 @@ type ContainerStatsConfig struct { // ContainerStats writes information about the container to the stream // given in the config object. func (daemon *Daemon) ContainerStats(prefixOrName string, config *ContainerStatsConfig) error { - container, err := daemon.Get(prefixOrName) if err != nil { return err diff --git a/docs/reference/commandline/stats.md b/docs/reference/commandline/stats.md index b2790edf54..18683e1157 100644 --- a/docs/reference/commandline/stats.md +++ b/docs/reference/commandline/stats.md @@ -10,24 +10,31 @@ parent = "smn_cli" # stats - Usage: docker stats [OPTIONS] CONTAINER [CONTAINER...] + Usage: docker stats [OPTIONS] [CONTAINER...] Display a live stream of one or more containers' resource usage statistics + -a, --all=false Show all containers (default shows just running) --help=false Print usage --no-stream=false Disable streaming stats and only pull the first result -Running `docker stats` on multiple containers +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. - $ docker stats redis1 redis2 +If you want more detailed information about a container's resource usage, use the `/containers/(id)/stats` API endpoint. + +## Examples + +Running `docker stats` on all running containers + + $ docker stats CONTAINER CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O redis1 0.07% 796 KB / 64 MB 1.21% 788 B / 648 B 3.568 MB / 512 KB redis2 0.07% 2.746 MB / 64 MB 4.29% 1.266 KB / 648 B 12.4 MB / 0 B + nginx1 0.03% 4.583 MB / 64 MB 6.30% 2.854 KB / 648 B 27.7 MB / 0 B +Running `docker stats` on multiple containers by name and id. -The `docker stats` command will only return a live stream of data for running -containers. Stopped containers will not return any data. - -> **Note:** -> If you want more detailed information about a container's resource -> usage, use the API endpoint. + $ docker stats fervent_panini 5acfcb1b4fd1 + CONTAINER CPU % MEM USAGE/LIMIT MEM % NET I/O + 5acfcb1b4fd1 0.00% 115.2 MB/1.045 GB 11.03% 1.422 kB/648 B + fervent_panini 0.02% 11.08 MB/1.045 GB 1.06% 648 B/648 B diff --git a/integration-cli/docker_cli_help_test.go b/integration-cli/docker_cli_help_test.go index 325b28013b..889e6cd5fd 100644 --- a/integration-cli/docker_cli_help_test.go +++ b/integration-cli/docker_cli_help_test.go @@ -206,6 +206,7 @@ func (s *DockerSuite) TestHelpTextVerify(c *check.C) { "login": "", "logout": "", "network": "", + "stats": "", } if _, ok := noShortUsage[cmd]; !ok { diff --git a/integration-cli/docker_cli_stats_test.go b/integration-cli/docker_cli_stats_test.go index eb8cb07356..372e40b09c 100644 --- a/integration-cli/docker_cli_stats_test.go +++ b/integration-cli/docker_cli_stats_test.go @@ -1,7 +1,9 @@ package main import ( + "bufio" "os/exec" + "regexp" "strings" "time" @@ -48,3 +50,80 @@ func (s *DockerSuite) TestStatsContainerNotFound(c *check.C) { c.Assert(err, checker.NotNil) 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)) } + +func (s *DockerSuite) TestStatsAllRunningNoStream(c *check.C) { + testRequires(c, DaemonIsLinux) + + out, _ := dockerCmd(c, "run", "-d", "busybox", "top") + id1 := strings.TrimSpace(out)[:12] + c.Assert(waitRun(id1), check.IsNil) + out, _ = dockerCmd(c, "run", "-d", "busybox", "top") + id2 := strings.TrimSpace(out)[:12] + c.Assert(waitRun(id2), check.IsNil) + out, _ = dockerCmd(c, "run", "-d", "busybox", "top") + id3 := strings.TrimSpace(out)[:12] + c.Assert(waitRun(id3), check.IsNil) + dockerCmd(c, "stop", id3) + + out, _ = dockerCmd(c, "stats", "--no-stream") + if !strings.Contains(out, id1) || !strings.Contains(out, id2) { + c.Fatalf("Expected stats output to contain both %s and %s, got %s", id1, id2, out) + } + if strings.Contains(out, id3) { + c.Fatalf("Did not expect %s in stats, got %s", id3, out) + } +} + +func (s *DockerSuite) TestStatsAllNoStream(c *check.C) { + testRequires(c, DaemonIsLinux) + + out, _ := dockerCmd(c, "run", "-d", "busybox", "top") + id1 := strings.TrimSpace(out)[:12] + c.Assert(waitRun(id1), check.IsNil) + dockerCmd(c, "stop", id1) + out, _ = dockerCmd(c, "run", "-d", "busybox", "top") + id2 := strings.TrimSpace(out)[:12] + c.Assert(waitRun(id2), check.IsNil) + + out, _ = dockerCmd(c, "stats", "--all", "--no-stream") + if !strings.Contains(out, id1) || !strings.Contains(out, id2) { + c.Fatalf("Expected stats output to contain both %s and %s, got %s", id1, id2, out) + } +} + +func (s *DockerSuite) TestStatsAllNewContainersAdded(c *check.C) { + testRequires(c, DaemonIsLinux) + + id := make(chan string) + addedChan := make(chan struct{}) + + dockerCmd(c, "run", "-d", "busybox", "top") + statsCmd := exec.Command(dockerBinary, "stats") + stdout, err := statsCmd.StdoutPipe() + c.Assert(err, check.IsNil) + c.Assert(statsCmd.Start(), check.IsNil) + defer statsCmd.Process.Kill() + + go func() { + containerID := <-id + matchID := regexp.MustCompile(containerID) + + scanner := bufio.NewScanner(stdout) + for scanner.Scan() { + switch { + case matchID.MatchString(scanner.Text()): + close(addedChan) + } + } + }() + + out, _ := dockerCmd(c, "run", "-d", "busybox", "top") + id <- strings.TrimSpace(out)[:12] + + select { + case <-time.After(5 * time.Second): + c.Fatal("failed to observe new container created added to stats") + case <-addedChan: + // ignore, done + } +} diff --git a/man/docker-stats.1.md b/man/docker-stats.1.md index 6bfa59e40f..67399bb6c2 100644 --- a/man/docker-stats.1.md +++ b/man/docker-stats.1.md @@ -6,15 +6,19 @@ docker-stats - Display a live stream of one or more containers' resource usage s # SYNOPSIS **docker stats** +[**-a**|**--all**[=*false*]] [**--help**] [**--no-stream**[=*false*]] -CONTAINER [CONTAINER...] +[CONTAINER...] # DESCRIPTION Display a live stream of one or more containers' resource usage statistics # OPTIONS +**-a**, **--all**=*true*|*false* + Show all containers. Only running containers are shown by default. The default is *false*. + **--help** Print usage statement @@ -23,9 +27,17 @@ Display a live stream of one or more containers' resource usage statistics # EXAMPLES -Run **docker stats** with multiple containers. +Running `docker stats` on all running containers - $ docker stats redis1 redis2 + $ docker stats CONTAINER CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O redis1 0.07% 796 KB / 64 MB 1.21% 788 B / 648 B 3.568 MB / 512 KB redis2 0.07% 2.746 MB / 64 MB 4.29% 1.266 KB / 648 B 12.4 MB / 0 B + nginx1 0.03% 4.583 MB / 64 MB 6.30% 2.854 KB / 648 B 27.7 MB / 0 B + +Running `docker stats` on multiple containers by name and id. + + $ docker stats fervent_panini 5acfcb1b4fd1 + CONTAINER CPU % MEM USAGE/LIMIT MEM % NET I/O + 5acfcb1b4fd1 0.00% 115.2 MB/1.045 GB 11.03% 1.422 kB/648 B + fervent_panini 0.02% 11.08 MB/1.045 GB 1.06% 648 B/648 B