Преглед изворни кода

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 пре 9 година
родитељ
комит
ae818a820f

+ 121 - 10
api/client/stats.go

@@ -13,7 +13,7 @@ import (
 
 
 	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/api/types"
 	Cli "github.com/docker/docker/cli"
 	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"
 	"github.com/docker/docker/pkg/units"
 )
 )
 
 
@@ -31,6 +31,11 @@ type containerStats struct {
 	err              error
 	err              error
 }
 }
 
 
+type stats struct {
+	mu sync.Mutex
+	cs []*containerStats
+}
+
 func (s *containerStats) Collect(cli *DockerCli, streamStats bool) {
 func (s *containerStats) Collect(cli *DockerCli, streamStats bool) {
 	v := url.Values{}
 	v := url.Values{}
 	if streamStats {
 	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.
 // 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 {
 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")
 	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)
 	cmd.ParseFlags(args, true)
 
 
 	names := cmd.Args()
 	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)
 	sort.Strings(names)
+
 	var (
 	var (
-		cStats []*containerStats
+		cStats = stats{}
 		w      = tabwriter.NewWriter(cli.out, 20, 1, 3, ' ', 0)
 		w      = tabwriter.NewWriter(cli.out, 20, 1, 3, ' ', 0)
 	)
 	)
 	printHeader := func() {
 	printHeader := func() {
@@ -162,42 +190,125 @@ func (cli *DockerCli) CmdStats(args ...string) error {
 	}
 	}
 	for _, n := range names {
 	for _, n := range names {
 		s := &containerStats{Name: n}
 		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)
 		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
 	// 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.
 	// evicted before we display the initial or default values.
 	time.Sleep(1500 * time.Millisecond)
 	time.Sleep(1500 * time.Millisecond)
 	var errs []string
 	var errs []string
-	for _, c := range cStats {
+	cStats.mu.Lock()
+	for _, c := range cStats.cs {
 		c.mu.Lock()
 		c.mu.Lock()
 		if c.err != nil {
 		if c.err != nil {
 			errs = append(errs, fmt.Sprintf("%s: %v", c.Name, c.err))
 			errs = append(errs, fmt.Sprintf("%s: %v", c.Name, c.err))
 		}
 		}
 		c.mu.Unlock()
 		c.mu.Unlock()
 	}
 	}
+	cStats.mu.Unlock()
 	if len(errs) > 0 {
 	if len(errs) > 0 {
 		return fmt.Errorf("%s", strings.Join(errs, ", "))
 		return fmt.Errorf("%s", strings.Join(errs, ", "))
 	}
 	}
 	for range time.Tick(500 * time.Millisecond) {
 	for range time.Tick(500 * time.Millisecond) {
 		printHeader()
 		printHeader()
 		toRemove := []int{}
 		toRemove := []int{}
-		for i, s := range cStats {
+		cStats.mu.Lock()
+		for i, s := range cStats.cs {
 			if err := s.Display(w); err != nil && !*noStream {
 			if err := s.Display(w); err != nil && !*noStream {
 				toRemove = append(toRemove, i)
 				toRemove = append(toRemove, i)
 			}
 			}
 		}
 		}
 		for j := len(toRemove) - 1; j >= 0; j-- {
 		for j := len(toRemove) - 1; j >= 0; j-- {
 			i := toRemove[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
 			return nil
 		}
 		}
+		cStats.mu.Unlock()
 		w.Flush()
 		w.Flush()
 		if *noStream {
 		if *noStream {
 			break
 			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
 	return nil
 }
 }

+ 0 - 1
daemon/stats.go

@@ -24,7 +24,6 @@ type ContainerStatsConfig struct {
 // ContainerStats writes information about the container to the stream
 // ContainerStats writes information about the container to the stream
 // given in the config object.
 // given in the config object.
 func (daemon *Daemon) ContainerStats(prefixOrName string, config *ContainerStatsConfig) error {
 func (daemon *Daemon) ContainerStats(prefixOrName string, config *ContainerStatsConfig) error {
-
 	container, err := daemon.Get(prefixOrName)
 	container, err := daemon.Get(prefixOrName)
 	if err != nil {
 	if err != nil {
 		return err
 		return err

+ 16 - 9
docs/reference/commandline/stats.md

@@ -10,24 +10,31 @@ parent = "smn_cli"
 
 
 # stats
 # stats
 
 
-    Usage: docker stats [OPTIONS] CONTAINER [CONTAINER...]
+    Usage: docker stats [OPTIONS] [CONTAINER...]
 
 
     Display a live stream of one or more containers' resource usage statistics
     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
       --help=false       Print usage
       --no-stream=false  Disable streaming stats and only pull the first result
       --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
     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
     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
     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

+ 1 - 0
integration-cli/docker_cli_help_test.go

@@ -206,6 +206,7 @@ func (s *DockerSuite) TestHelpTextVerify(c *check.C) {
 				"login":   "",
 				"login":   "",
 				"logout":  "",
 				"logout":  "",
 				"network": "",
 				"network": "",
+				"stats":   "",
 			}
 			}
 
 
 			if _, ok := noShortUsage[cmd]; !ok {
 			if _, ok := noShortUsage[cmd]; !ok {

+ 79 - 0
integration-cli/docker_cli_stats_test.go

@@ -1,7 +1,9 @@
 package main
 package main
 
 
 import (
 import (
+	"bufio"
 	"os/exec"
 	"os/exec"
+	"regexp"
 	"strings"
 	"strings"
 	"time"
 	"time"
 
 
@@ -48,3 +50,80 @@ func (s *DockerSuite) TestStatsContainerNotFound(c *check.C) {
 	c.Assert(err, checker.NotNil)
 	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))
 	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
+	}
+}

+ 15 - 3
man/docker-stats.1.md

@@ -6,15 +6,19 @@ docker-stats - Display a live stream of one or more containers' resource usage s
 
 
 # SYNOPSIS
 # SYNOPSIS
 **docker stats**
 **docker stats**
+[**-a**|**--all**[=*false*]]
 [**--help**]
 [**--help**]
 [**--no-stream**[=*false*]]
 [**--no-stream**[=*false*]]
-CONTAINER [CONTAINER...]
+[CONTAINER...]
 
 
 # DESCRIPTION
 # DESCRIPTION
 
 
 Display a live stream of one or more containers' resource usage statistics
 Display a live stream of one or more containers' resource usage statistics
 
 
 # OPTIONS
 # OPTIONS
+**-a**, **--all**=*true*|*false*
+   Show all containers. Only running containers are shown by default. The default is *false*.
+
 **--help**
 **--help**
   Print usage statement
   Print usage statement
 
 
@@ -23,9 +27,17 @@ Display a live stream of one or more containers' resource usage statistics
 
 
 # EXAMPLES
 # 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
     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
     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
     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