github.1git.de/docker/cli@v26.1.3+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/completion"
    14  	"github.com/docker/cli/cli/command/formatter"
    15  	flagsHelper "github.com/docker/cli/cli/flags"
    16  	"github.com/docker/docker/api/types"
    17  	"github.com/docker/docker/api/types/container"
    18  	"github.com/docker/docker/api/types/events"
    19  	"github.com/docker/docker/api/types/filters"
    20  	"github.com/pkg/errors"
    21  	"github.com/sirupsen/logrus"
    22  	"github.com/spf13/cobra"
    23  )
    24  
    25  // StatsOptions defines options for [RunStats].
    26  type StatsOptions struct {
    27  	// All allows including both running and stopped containers. The default
    28  	// is to only include running containers.
    29  	All bool
    30  
    31  	// NoStream disables streaming stats. If enabled, stats are collected once,
    32  	// and the result is printed.
    33  	NoStream bool
    34  
    35  	// NoTrunc disables truncating the output. The default is to truncate
    36  	// output such as container-IDs.
    37  	NoTrunc bool
    38  
    39  	// Format is a custom template to use for presenting the stats.
    40  	// Refer to [flagsHelper.FormatHelp] for accepted formats.
    41  	Format string
    42  
    43  	// Containers is the list of container names or IDs to include in the stats.
    44  	// If empty, all containers are included. It is mutually exclusive with the
    45  	// Filters option, and an error is produced if both are set.
    46  	Containers []string
    47  
    48  	// Filters provides optional filters to filter the list of containers and their
    49  	// associated container-events to include in the stats if no list of containers
    50  	// is set. If no filter is provided, all containers are included. Filters and
    51  	// Containers are currently mutually exclusive, and setting both options
    52  	// produces an error.
    53  	//
    54  	// These filters are used both to collect the initial list of containers and
    55  	// to refresh the list of containers based on container-events, accepted
    56  	// filters are limited to the intersection of filters accepted by "events"
    57  	// and "container list".
    58  	//
    59  	// Currently only "label" / "label=value" filters are accepted. Additional
    60  	// filter options may be added in future (within the constraints described
    61  	// above), but may require daemon-side validation as the list of accepted
    62  	// filters can differ between daemon- and API versions.
    63  	Filters *filters.Args
    64  }
    65  
    66  // NewStatsCommand creates a new [cobra.Command] for "docker stats".
    67  func NewStatsCommand(dockerCLI command.Cli) *cobra.Command {
    68  	options := StatsOptions{}
    69  
    70  	cmd := &cobra.Command{
    71  		Use:   "stats [OPTIONS] [CONTAINER...]",
    72  		Short: "Display a live stream of container(s) resource usage statistics",
    73  		Args:  cli.RequiresMinArgs(0),
    74  		RunE: func(cmd *cobra.Command, args []string) error {
    75  			options.Containers = args
    76  			return RunStats(cmd.Context(), dockerCLI, &options)
    77  		},
    78  		Annotations: map[string]string{
    79  			"aliases": "docker container stats, docker stats",
    80  		},
    81  		ValidArgsFunction: completion.ContainerNames(dockerCLI, false),
    82  	}
    83  
    84  	flags := cmd.Flags()
    85  	flags.BoolVarP(&options.All, "all", "a", false, "Show all containers (default shows just running)")
    86  	flags.BoolVar(&options.NoStream, "no-stream", false, "Disable streaming stats and only pull the first result")
    87  	flags.BoolVar(&options.NoTrunc, "no-trunc", false, "Do not truncate output")
    88  	flags.StringVar(&options.Format, "format", "", flagsHelper.FormatHelp)
    89  	return cmd
    90  }
    91  
    92  // acceptedStatsFilters is the list of filters accepted by [RunStats] (through
    93  // the [StatsOptions.Filters] option).
    94  //
    95  // TODO(thaJeztah): don't hard-code the list of accept filters, and expand
    96  // to the intersection of filters accepted by both "container list" and
    97  // "system events". Validating filters may require an initial API call
    98  // to both endpoints ("container list" and "system events").
    99  var acceptedStatsFilters = map[string]bool{
   100  	"label": true,
   101  }
   102  
   103  // RunStats displays a live stream of resource usage statistics for one or more containers.
   104  // This shows real-time information on CPU usage, memory usage, and network I/O.
   105  //
   106  //nolint:gocyclo
   107  func RunStats(ctx context.Context, dockerCLI command.Cli, options *StatsOptions) error {
   108  	apiClient := dockerCLI.Client()
   109  
   110  	// waitFirst is a WaitGroup to wait first stat data's reach for each container
   111  	waitFirst := &sync.WaitGroup{}
   112  	// closeChan is a non-buffered channel used to collect errors from goroutines.
   113  	closeChan := make(chan error)
   114  	cStats := stats{}
   115  
   116  	showAll := len(options.Containers) == 0
   117  	if showAll {
   118  		// If no names were specified, start a long-running goroutine which
   119  		// monitors container events. We make sure we're subscribed before
   120  		// retrieving the list of running containers to avoid a race where we
   121  		// would "miss" a creation.
   122  		started := make(chan struct{})
   123  
   124  		if options.Filters == nil {
   125  			f := filters.NewArgs()
   126  			options.Filters = &f
   127  		}
   128  
   129  		if err := options.Filters.Validate(acceptedStatsFilters); err != nil {
   130  			return err
   131  		}
   132  
   133  		eh := newEventHandler()
   134  		if options.All {
   135  			eh.setHandler(events.ActionCreate, func(e events.Message) {
   136  				s := NewStats(e.Actor.ID[:12])
   137  				if cStats.add(s) {
   138  					waitFirst.Add(1)
   139  					go collect(ctx, s, apiClient, !options.NoStream, waitFirst)
   140  				}
   141  			})
   142  		}
   143  
   144  		eh.setHandler(events.ActionStart, func(e events.Message) {
   145  			s := NewStats(e.Actor.ID[:12])
   146  			if cStats.add(s) {
   147  				waitFirst.Add(1)
   148  				go collect(ctx, s, apiClient, !options.NoStream, waitFirst)
   149  			}
   150  		})
   151  
   152  		if !options.All {
   153  			eh.setHandler(events.ActionDie, func(e events.Message) {
   154  				cStats.remove(e.Actor.ID[:12])
   155  			})
   156  		}
   157  
   158  		// monitorContainerEvents watches for container creation and removal (only
   159  		// used when calling `docker stats` without arguments).
   160  		monitorContainerEvents := func(started chan<- struct{}, c chan events.Message, stopped <-chan struct{}) {
   161  			// Create a copy of the custom filters so that we don't mutate
   162  			// the original set of filters. Custom filters are used both
   163  			// to list containers and to filter events, but the "type" filter
   164  			// is not valid for filtering containers.
   165  			f := options.Filters.Clone()
   166  			f.Add("type", string(events.ContainerEventType))
   167  			eventChan, errChan := apiClient.Events(ctx, types.EventsOptions{
   168  				Filters: f,
   169  			})
   170  
   171  			// Whether we successfully subscribed to eventChan or not, we can now
   172  			// unblock the main goroutine.
   173  			close(started)
   174  			defer close(c)
   175  
   176  			for {
   177  				select {
   178  				case <-stopped:
   179  					return
   180  				case event := <-eventChan:
   181  					c <- event
   182  				case err := <-errChan:
   183  					closeChan <- err
   184  					return
   185  				}
   186  			}
   187  		}
   188  
   189  		eventChan := make(chan events.Message)
   190  		go eh.watch(eventChan)
   191  		stopped := make(chan struct{})
   192  		go monitorContainerEvents(started, eventChan, stopped)
   193  		defer close(stopped)
   194  		<-started
   195  
   196  		// Fetch the initial list of containers and collect stats for them.
   197  		// After the initial list was collected, we start listening for events
   198  		// to refresh the list of containers.
   199  		cs, err := apiClient.ContainerList(ctx, container.ListOptions{
   200  			All:     options.All,
   201  			Filters: *options.Filters,
   202  		})
   203  		if err != nil {
   204  			return err
   205  		}
   206  		for _, ctr := range cs {
   207  			s := NewStats(ctr.ID[:12])
   208  			if cStats.add(s) {
   209  				waitFirst.Add(1)
   210  				go collect(ctx, s, apiClient, !options.NoStream, waitFirst)
   211  			}
   212  		}
   213  
   214  		// make sure each container get at least one valid stat data
   215  		waitFirst.Wait()
   216  	} else {
   217  		// TODO(thaJeztah): re-implement options.Containers as a filter so that
   218  		// only a single code-path is needed, and custom filters can be combined
   219  		// with a list of container names/IDs.
   220  
   221  		if options.Filters != nil && options.Filters.Len() > 0 {
   222  			return fmt.Errorf("filtering is not supported when specifying a list of containers")
   223  		}
   224  
   225  		// Create the list of containers, and start collecting stats for all
   226  		// containers passed.
   227  		for _, ctr := range options.Containers {
   228  			s := NewStats(ctr)
   229  			if cStats.add(s) {
   230  				waitFirst.Add(1)
   231  				go collect(ctx, s, apiClient, !options.NoStream, waitFirst)
   232  			}
   233  		}
   234  
   235  		// We don't expect any asynchronous errors: closeChan can be closed.
   236  		close(closeChan)
   237  
   238  		// make sure each container get at least one valid stat data
   239  		waitFirst.Wait()
   240  
   241  		var errs []string
   242  		cStats.mu.RLock()
   243  		for _, c := range cStats.cs {
   244  			if err := c.GetError(); err != nil {
   245  				errs = append(errs, err.Error())
   246  			}
   247  		}
   248  		cStats.mu.RUnlock()
   249  		if len(errs) > 0 {
   250  			return errors.New(strings.Join(errs, "\n"))
   251  		}
   252  	}
   253  
   254  	format := options.Format
   255  	if len(format) == 0 {
   256  		if len(dockerCLI.ConfigFile().StatsFormat) > 0 {
   257  			format = dockerCLI.ConfigFile().StatsFormat
   258  		} else {
   259  			format = formatter.TableFormatKey
   260  		}
   261  	}
   262  	if daemonOSType == "" {
   263  		// Get the daemonOSType if not set already. The daemonOSType variable
   264  		// should already be set when collecting stats as part of "collect()",
   265  		// so we unlikely hit this code in practice.
   266  		daemonOSType = dockerCLI.ServerInfo().OSType
   267  	}
   268  	statsCtx := formatter.Context{
   269  		Output: dockerCLI.Out(),
   270  		Format: NewStatsFormat(format, daemonOSType),
   271  	}
   272  	cleanScreen := func() {
   273  		if !options.NoStream {
   274  			_, _ = fmt.Fprint(dockerCLI.Out(), "\033[2J")
   275  			_, _ = fmt.Fprint(dockerCLI.Out(), "\033[H")
   276  		}
   277  	}
   278  
   279  	var err error
   280  	ticker := time.NewTicker(500 * time.Millisecond)
   281  	defer ticker.Stop()
   282  	for range ticker.C {
   283  		cleanScreen()
   284  		var ccStats []StatsEntry
   285  		cStats.mu.RLock()
   286  		for _, c := range cStats.cs {
   287  			ccStats = append(ccStats, c.GetStatistics())
   288  		}
   289  		cStats.mu.RUnlock()
   290  		if err = statsFormatWrite(statsCtx, ccStats, daemonOSType, !options.NoTrunc); err != nil {
   291  			break
   292  		}
   293  		if len(cStats.cs) == 0 && !showAll {
   294  			break
   295  		}
   296  		if options.NoStream {
   297  			break
   298  		}
   299  		select {
   300  		case err, ok := <-closeChan:
   301  			if ok {
   302  				if err != nil {
   303  					// Suppress "unexpected EOF" errors in the CLI so that
   304  					// it shuts down cleanly when the daemon restarts.
   305  					if errors.Is(err, io.ErrUnexpectedEOF) {
   306  						return nil
   307  					}
   308  					return err
   309  				}
   310  			}
   311  		default:
   312  			// just skip
   313  		}
   314  	}
   315  	return err
   316  }
   317  
   318  // newEventHandler initializes and returns an eventHandler
   319  func newEventHandler() *eventHandler {
   320  	return &eventHandler{handlers: make(map[events.Action]func(events.Message))}
   321  }
   322  
   323  // eventHandler allows for registering specific events to setHandler.
   324  type eventHandler struct {
   325  	handlers map[events.Action]func(events.Message)
   326  }
   327  
   328  func (eh *eventHandler) setHandler(action events.Action, handler func(events.Message)) {
   329  	eh.handlers[action] = handler
   330  }
   331  
   332  // watch ranges over the passed in event chan and processes the events based on the
   333  // handlers created for a given action.
   334  // To stop watching, close the event chan.
   335  func (eh *eventHandler) watch(c <-chan events.Message) {
   336  	for e := range c {
   337  		h, exists := eh.handlers[e.Action]
   338  		if !exists {
   339  			continue
   340  		}
   341  		logrus.Debugf("event handler: received event: %v", e)
   342  		go h(e)
   343  	}
   344  }