github.com/xeptore/docker-cli@v20.10.14+incompatible/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 := NewStats(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 := NewStats(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 := NewStats(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  
   160  		// make sure each container get at least one valid stat data
   161  		waitFirst.Wait()
   162  	} else {
   163  		// Artificially send creation events for the containers we were asked to
   164  		// monitor (same code path than we use when monitoring all containers).
   165  		for _, name := range opts.containers {
   166  			s := NewStats(name)
   167  			if cStats.add(s) {
   168  				waitFirst.Add(1)
   169  				go collect(ctx, s, dockerCli.Client(), !opts.noStream, waitFirst)
   170  			}
   171  		}
   172  
   173  		// We don't expect any asynchronous errors: closeChan can be closed.
   174  		close(closeChan)
   175  
   176  		// make sure each container get at least one valid stat data
   177  		waitFirst.Wait()
   178  
   179  		var errs []string
   180  		cStats.mu.Lock()
   181  		for _, c := range cStats.cs {
   182  			if err := c.GetError(); err != nil {
   183  				errs = append(errs, err.Error())
   184  			}
   185  		}
   186  		cStats.mu.Unlock()
   187  		if len(errs) > 0 {
   188  			return errors.New(strings.Join(errs, "\n"))
   189  		}
   190  	}
   191  
   192  	format := opts.format
   193  	if len(format) == 0 {
   194  		if len(dockerCli.ConfigFile().StatsFormat) > 0 {
   195  			format = dockerCli.ConfigFile().StatsFormat
   196  		} else {
   197  			format = formatter.TableFormatKey
   198  		}
   199  	}
   200  	statsCtx := formatter.Context{
   201  		Output: dockerCli.Out(),
   202  		Format: NewStatsFormat(format, daemonOSType),
   203  	}
   204  	cleanScreen := func() {
   205  		if !opts.noStream {
   206  			fmt.Fprint(dockerCli.Out(), "\033[2J")
   207  			fmt.Fprint(dockerCli.Out(), "\033[H")
   208  		}
   209  	}
   210  
   211  	var err error
   212  	ticker := time.NewTicker(500 * time.Millisecond)
   213  	defer ticker.Stop()
   214  	for range ticker.C {
   215  		cleanScreen()
   216  		ccstats := []StatsEntry{}
   217  		cStats.mu.Lock()
   218  		for _, c := range cStats.cs {
   219  			ccstats = append(ccstats, c.GetStatistics())
   220  		}
   221  		cStats.mu.Unlock()
   222  		if err = statsFormatWrite(statsCtx, ccstats, daemonOSType, !opts.noTrunc); err != nil {
   223  			break
   224  		}
   225  		if len(cStats.cs) == 0 && !showAll {
   226  			break
   227  		}
   228  		if opts.noStream {
   229  			break
   230  		}
   231  		select {
   232  		case err, ok := <-closeChan:
   233  			if ok {
   234  				if err != nil {
   235  					// this is suppressing "unexpected EOF" in the cli when the
   236  					// daemon restarts so it shutdowns cleanly
   237  					if err == io.ErrUnexpectedEOF {
   238  						return nil
   239  					}
   240  					return err
   241  				}
   242  			}
   243  		default:
   244  			// just skip
   245  		}
   246  	}
   247  	return err
   248  }