github.com/AliyunContainerService/cli@v0.0.0-20181009023821-814ced4b30d0/cli/command/container/stats.go (about)

     1  package container
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io"
     7  	"strings"
     8  	"sync"
     9  	"time"
    10  
    11  	"github.com/docker/cli/cli"
    12  	"github.com/docker/cli/cli/command"
    13  	"github.com/docker/cli/cli/command/formatter"
    14  	"github.com/docker/docker/api/types"
    15  	"github.com/docker/docker/api/types/events"
    16  	"github.com/docker/docker/api/types/filters"
    17  	"github.com/pkg/errors"
    18  	"github.com/spf13/cobra"
    19  )
    20  
    21  type statsOptions struct {
    22  	all        bool
    23  	noStream   bool
    24  	noTrunc    bool
    25  	format     string
    26  	containers []string
    27  }
    28  
    29  // NewStatsCommand creates a new cobra.Command for `docker stats`
    30  func NewStatsCommand(dockerCli command.Cli) *cobra.Command {
    31  	var opts statsOptions
    32  
    33  	cmd := &cobra.Command{
    34  		Use:   "stats [OPTIONS] [CONTAINER...]",
    35  		Short: "Display a live stream of container(s) resource usage statistics",
    36  		Args:  cli.RequiresMinArgs(0),
    37  		RunE: func(cmd *cobra.Command, args []string) error {
    38  			opts.containers = args
    39  			return runStats(dockerCli, &opts)
    40  		},
    41  	}
    42  
    43  	flags := cmd.Flags()
    44  	flags.BoolVarP(&opts.all, "all", "a", false, "Show all containers (default shows just running)")
    45  	flags.BoolVar(&opts.noStream, "no-stream", false, "Disable streaming stats and only pull the first result")
    46  	flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate output")
    47  	flags.StringVar(&opts.format, "format", "", "Pretty-print images using a Go template")
    48  	return cmd
    49  }
    50  
    51  // runStats displays a live stream of resource usage statistics for one or more containers.
    52  // This shows real-time information on CPU usage, memory usage, and network I/O.
    53  // nolint: gocyclo
    54  func runStats(dockerCli command.Cli, opts *statsOptions) error {
    55  	showAll := len(opts.containers) == 0
    56  	closeChan := make(chan error)
    57  
    58  	ctx := context.Background()
    59  
    60  	// monitorContainerEvents watches for container creation and removal (only
    61  	// used when calling `docker stats` without arguments).
    62  	monitorContainerEvents := func(started chan<- struct{}, c chan events.Message) {
    63  		f := filters.NewArgs()
    64  		f.Add("type", "container")
    65  		options := types.EventsOptions{
    66  			Filters: f,
    67  		}
    68  
    69  		eventq, errq := dockerCli.Client().Events(ctx, options)
    70  
    71  		// Whether we successfully subscribed to eventq or not, we can now
    72  		// unblock the main goroutine.
    73  		close(started)
    74  
    75  		for {
    76  			select {
    77  			case event := <-eventq:
    78  				c <- event
    79  			case err := <-errq:
    80  				closeChan <- err
    81  				return
    82  			}
    83  		}
    84  	}
    85  
    86  	// Get the daemonOSType if not set already
    87  	if daemonOSType == "" {
    88  		svctx := context.Background()
    89  		sv, err := dockerCli.Client().ServerVersion(svctx)
    90  		if err != nil {
    91  			return err
    92  		}
    93  		daemonOSType = sv.Os
    94  	}
    95  
    96  	// waitFirst is a WaitGroup to wait first stat data's reach for each container
    97  	waitFirst := &sync.WaitGroup{}
    98  
    99  	cStats := stats{}
   100  	// getContainerList simulates creation event for all previously existing
   101  	// containers (only used when calling `docker stats` without arguments).
   102  	getContainerList := func() {
   103  		options := types.ContainerListOptions{
   104  			All: opts.all,
   105  		}
   106  		cs, err := dockerCli.Client().ContainerList(ctx, options)
   107  		if err != nil {
   108  			closeChan <- err
   109  		}
   110  		for _, container := range cs {
   111  			s := formatter.NewContainerStats(container.ID[:12])
   112  			if cStats.add(s) {
   113  				waitFirst.Add(1)
   114  				go collect(ctx, s, dockerCli.Client(), !opts.noStream, waitFirst)
   115  			}
   116  		}
   117  	}
   118  
   119  	if showAll {
   120  		// If no names were specified, start a long running goroutine which
   121  		// monitors container events. We make sure we're subscribed before
   122  		// retrieving the list of running containers to avoid a race where we
   123  		// would "miss" a creation.
   124  		started := make(chan struct{})
   125  		eh := command.InitEventHandler()
   126  		eh.Handle("create", func(e events.Message) {
   127  			if opts.all {
   128  				s := formatter.NewContainerStats(e.ID[:12])
   129  				if cStats.add(s) {
   130  					waitFirst.Add(1)
   131  					go collect(ctx, s, dockerCli.Client(), !opts.noStream, waitFirst)
   132  				}
   133  			}
   134  		})
   135  
   136  		eh.Handle("start", func(e events.Message) {
   137  			s := formatter.NewContainerStats(e.ID[:12])
   138  			if cStats.add(s) {
   139  				waitFirst.Add(1)
   140  				go collect(ctx, s, dockerCli.Client(), !opts.noStream, waitFirst)
   141  			}
   142  		})
   143  
   144  		eh.Handle("die", func(e events.Message) {
   145  			if !opts.all {
   146  				cStats.remove(e.ID[:12])
   147  			}
   148  		})
   149  
   150  		eventChan := make(chan events.Message)
   151  		go eh.Watch(eventChan)
   152  		go monitorContainerEvents(started, eventChan)
   153  		defer close(eventChan)
   154  		<-started
   155  
   156  		// Start a short-lived goroutine to retrieve the initial list of
   157  		// containers.
   158  		getContainerList()
   159  	} else {
   160  		// Artificially send creation events for the containers we were asked to
   161  		// monitor (same code path than we use when monitoring all containers).
   162  		for _, name := range opts.containers {
   163  			s := formatter.NewContainerStats(name)
   164  			if cStats.add(s) {
   165  				waitFirst.Add(1)
   166  				go collect(ctx, s, dockerCli.Client(), !opts.noStream, waitFirst)
   167  			}
   168  		}
   169  
   170  		// We don't expect any asynchronous errors: closeChan can be closed.
   171  		close(closeChan)
   172  
   173  		// Do a quick pause to detect any error with the provided list of
   174  		// container names.
   175  		time.Sleep(1500 * time.Millisecond)
   176  		var errs []string
   177  		cStats.mu.Lock()
   178  		for _, c := range cStats.cs {
   179  			if err := c.GetError(); err != nil {
   180  				errs = append(errs, err.Error())
   181  			}
   182  		}
   183  		cStats.mu.Unlock()
   184  		if len(errs) > 0 {
   185  			return errors.New(strings.Join(errs, "\n"))
   186  		}
   187  	}
   188  
   189  	// before print to screen, make sure each container get at least one valid stat data
   190  	waitFirst.Wait()
   191  	format := opts.format
   192  	if len(format) == 0 {
   193  		if len(dockerCli.ConfigFile().StatsFormat) > 0 {
   194  			format = dockerCli.ConfigFile().StatsFormat
   195  		} else {
   196  			format = formatter.TableFormatKey
   197  		}
   198  	}
   199  	statsCtx := formatter.Context{
   200  		Output: dockerCli.Out(),
   201  		Format: formatter.NewStatsFormat(format, daemonOSType),
   202  	}
   203  	cleanScreen := func() {
   204  		if !opts.noStream {
   205  			fmt.Fprint(dockerCli.Out(), "\033[2J")
   206  			fmt.Fprint(dockerCli.Out(), "\033[H")
   207  		}
   208  	}
   209  
   210  	var err error
   211  	for range time.Tick(500 * time.Millisecond) {
   212  		cleanScreen()
   213  		ccstats := []formatter.StatsEntry{}
   214  		cStats.mu.Lock()
   215  		for _, c := range cStats.cs {
   216  			ccstats = append(ccstats, c.GetStatistics())
   217  		}
   218  		cStats.mu.Unlock()
   219  		if err = formatter.ContainerStatsWrite(statsCtx, ccstats, daemonOSType, !opts.noTrunc); err != nil {
   220  			break
   221  		}
   222  		if len(cStats.cs) == 0 && !showAll {
   223  			break
   224  		}
   225  		if opts.noStream {
   226  			break
   227  		}
   228  		select {
   229  		case err, ok := <-closeChan:
   230  			if ok {
   231  				if err != nil {
   232  					// this is suppressing "unexpected EOF" in the cli when the
   233  					// daemon restarts so it shutdowns cleanly
   234  					if err == io.ErrUnexpectedEOF {
   235  						return nil
   236  					}
   237  					return err
   238  				}
   239  			}
   240  		default:
   241  			// just skip
   242  		}
   243  	}
   244  	return err
   245  }