123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233 |
- package container
- import (
- "fmt"
- "io"
- "strings"
- "sync"
- "text/tabwriter"
- "time"
- "golang.org/x/net/context"
- "github.com/Sirupsen/logrus"
- "github.com/docker/docker/api/client"
- "github.com/docker/docker/api/client/system"
- "github.com/docker/docker/cli"
- "github.com/docker/engine-api/types"
- "github.com/docker/engine-api/types/events"
- "github.com/docker/engine-api/types/filters"
- "github.com/spf13/cobra"
- )
- type statsOptions struct {
- all bool
- noStream bool
- containers []string
- }
- // NewStatsCommand creates a new cobra.Command for `docker stats`
- func NewStatsCommand(dockerCli *client.DockerCli) *cobra.Command {
- var opts statsOptions
- cmd := &cobra.Command{
- Use: "stats [OPTIONS] [CONTAINER...]",
- Short: "Display a live stream of container(s) resource usage statistics",
- Args: cli.RequiresMinArgs(0),
- RunE: func(cmd *cobra.Command, args []string) error {
- opts.containers = args
- return runStats(dockerCli, &opts)
- },
- }
- flags := cmd.Flags()
- flags.BoolVarP(&opts.all, "all", "a", false, "Show all containers (default shows just running)")
- flags.BoolVar(&opts.noStream, "no-stream", false, "Disable streaming stats and only pull the first result")
- return cmd
- }
- // runStats displays a live stream of resource usage statistics for one or more containers.
- // This shows real-time information on CPU usage, memory usage, and network I/O.
- func runStats(dockerCli *client.DockerCli, opts *statsOptions) error {
- showAll := len(opts.containers) == 0
- closeChan := make(chan error)
- ctx := context.Background()
- // monitorContainerEvents watches for container creation and removal (only
- // used when calling `docker stats` without arguments).
- monitorContainerEvents := func(started chan<- struct{}, c chan events.Message) {
- f := filters.NewArgs()
- f.Add("type", "container")
- options := types.EventsOptions{
- Filters: f,
- }
- resBody, err := dockerCli.Client().Events(ctx, options)
- // Whether we successfully subscribed to events or not, we can now
- // unblock the main goroutine.
- close(started)
- if err != nil {
- closeChan <- err
- return
- }
- defer resBody.Close()
- system.DecodeEvents(resBody, func(event events.Message, err error) error {
- if err != nil {
- closeChan <- err
- return nil
- }
- c <- event
- return nil
- })
- }
- // waitFirst is a WaitGroup to wait first stat data's reach for each container
- waitFirst := &sync.WaitGroup{}
- cStats := stats{}
- // getContainerList simulates creation event for all previously existing
- // containers (only used when calling `docker stats` without arguments).
- getContainerList := func() {
- options := types.ContainerListOptions{
- All: opts.all,
- }
- cs, err := dockerCli.Client().ContainerList(ctx, options)
- if err != nil {
- closeChan <- err
- }
- for _, container := range cs {
- s := &containerStats{Name: container.ID[:12]}
- if cStats.add(s) {
- waitFirst.Add(1)
- go s.Collect(ctx, dockerCli.Client(), !opts.noStream, waitFirst)
- }
- }
- }
- if showAll {
- // If no names were specified, start a long running goroutine which
- // monitors container events. We make sure we're subscribed before
- // retrieving the list of running containers to avoid a race where we
- // would "miss" a creation.
- started := make(chan struct{})
- eh := system.InitEventHandler()
- eh.Handle("create", func(e events.Message) {
- if opts.all {
- s := &containerStats{Name: e.ID[:12]}
- if cStats.add(s) {
- waitFirst.Add(1)
- go s.Collect(ctx, dockerCli.Client(), !opts.noStream, waitFirst)
- }
- }
- })
- eh.Handle("start", func(e events.Message) {
- s := &containerStats{Name: e.ID[:12]}
- if cStats.add(s) {
- waitFirst.Add(1)
- go s.Collect(ctx, dockerCli.Client(), !opts.noStream, waitFirst)
- }
- })
- eh.Handle("die", func(e events.Message) {
- if !opts.all {
- cStats.remove(e.ID[:12])
- }
- })
- eventChan := make(chan events.Message)
- go eh.Watch(eventChan)
- go monitorContainerEvents(started, eventChan)
- defer close(eventChan)
- <-started
- // Start a short-lived goroutine to retrieve the initial list of
- // containers.
- getContainerList()
- } else {
- // Artificially send creation events for the containers we were asked to
- // monitor (same code path than we use when monitoring all containers).
- for _, name := range opts.containers {
- s := &containerStats{Name: name}
- if cStats.add(s) {
- waitFirst.Add(1)
- go s.Collect(ctx, dockerCli.Client(), !opts.noStream, waitFirst)
- }
- }
- // We don't expect any asynchronous errors: closeChan can be closed.
- close(closeChan)
- // Do a quick pause to detect any error with the provided list of
- // container names.
- time.Sleep(1500 * time.Millisecond)
- var errs []string
- 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, ", "))
- }
- }
- // before print to screen, make sure each container get at least one valid stat data
- waitFirst.Wait()
- w := tabwriter.NewWriter(dockerCli.Out(), 20, 1, 3, ' ', 0)
- printHeader := func() {
- if !opts.noStream {
- fmt.Fprint(dockerCli.Out(), "\033[2J")
- fmt.Fprint(dockerCli.Out(), "\033[H")
- }
- io.WriteString(w, "CONTAINER\tCPU %\tMEM USAGE / LIMIT\tMEM %\tNET I/O\tBLOCK I/O\tPIDS\n")
- }
- for range time.Tick(500 * time.Millisecond) {
- printHeader()
- toRemove := []string{}
- cStats.mu.Lock()
- for _, s := range cStats.cs {
- if err := s.Display(w); err != nil && !opts.noStream {
- logrus.Debugf("stats: got error for %s: %v", s.Name, err)
- if err == io.EOF {
- toRemove = append(toRemove, s.Name)
- }
- }
- }
- cStats.mu.Unlock()
- for _, name := range toRemove {
- cStats.remove(name)
- }
- if len(cStats.cs) == 0 && !showAll {
- return nil
- }
- w.Flush()
- if opts.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 shutdowns cleanly
- if err == io.ErrUnexpectedEOF {
- return nil
- }
- return err
- }
- }
- default:
- // just skip
- }
- }
- return nil
- }
|