github.com/olljanat/moby@v1.13.1/cli/command/container/stats.go (about)

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