export RunStat, StatsOptions, and add Filters option

The filter option is not currently exposed on the command-line,
but can be added as a flag in future. It will be used by compose
to filter the list of containers to include based on compose
labels.

Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
This commit is contained in:
Nicolas De Loof 2023-11-30 09:37:18 +01:00 committed by Sebastiaan van Stijn
parent 8b53402125
commit 46355a1941
No known key found for this signature in database
GPG Key ID: 76698F39D527CE8C
1 changed files with 89 additions and 36 deletions

View File

@ -21,40 +21,58 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
// statsOptions defines options for runStats. // StatsOptions defines options for [RunStats].
type statsOptions struct { type StatsOptions struct {
// all allows including both running and stopped containers. The default // All allows including both running and stopped containers. The default
// is to only include running containers. // is to only include running containers.
all bool All bool
// noStream disables streaming stats. If enabled, stats are collected once, // NoStream disables streaming stats. If enabled, stats are collected once,
// and the result is printed. // and the result is printed.
noStream bool NoStream bool
// noTrunc disables truncating the output. The default is to truncate // NoTrunc disables truncating the output. The default is to truncate
// output such as container-IDs. // output such as container-IDs.
noTrunc bool NoTrunc bool
// format is a custom template to use for presenting the stats. // Format is a custom template to use for presenting the stats.
// Refer to [flagsHelper.FormatHelp] for accepted formats. // Refer to [flagsHelper.FormatHelp] for accepted formats.
format string Format string
// containers is the list of container names or IDs to include in the stats. // Containers is the list of container names or IDs to include in the stats.
// If empty, all containers are included. // If empty, all containers are included. It is mutually exclusive with the
containers []string // Filters option, and an error is produced if both are set.
Containers []string
// Filters provides optional filters to filter the list of containers and their
// associated container-events to include in the stats if no list of containers
// is set. If no filter is provided, all containers are included. Filters and
// Containers are currently mutually exclusive, and setting both options
// produces an error.
//
// These filters are used both to collect the initial list of containers and
// to refresh the list of containers based on container-events, accepted
// filters are limited to the intersection of filters accepted by "events"
// and "container list".
//
// Currently only "label" / "label=value" filters are accepted. Additional
// filter options may be added in future (within the constraints described
// above), but may require daemon-side validation as the list of accepted
// filters can differ between daemon- and API versions.
Filters *filters.Args
} }
// NewStatsCommand creates a new [cobra.Command] for "docker stats". // NewStatsCommand creates a new [cobra.Command] for "docker stats".
func NewStatsCommand(dockerCLI command.Cli) *cobra.Command { func NewStatsCommand(dockerCLI command.Cli) *cobra.Command {
var options statsOptions options := StatsOptions{}
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "stats [OPTIONS] [CONTAINER...]", Use: "stats [OPTIONS] [CONTAINER...]",
Short: "Display a live stream of container(s) resource usage statistics", Short: "Display a live stream of container(s) resource usage statistics",
Args: cli.RequiresMinArgs(0), Args: cli.RequiresMinArgs(0),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
options.containers = args options.Containers = args
return runStats(cmd.Context(), dockerCLI, &options) return RunStats(cmd.Context(), dockerCLI, &options)
}, },
Annotations: map[string]string{ Annotations: map[string]string{
"aliases": "docker container stats, docker stats", "aliases": "docker container stats, docker stats",
@ -63,18 +81,29 @@ func NewStatsCommand(dockerCLI command.Cli) *cobra.Command {
} }
flags := cmd.Flags() flags := cmd.Flags()
flags.BoolVarP(&options.all, "all", "a", false, "Show all containers (default shows just running)") flags.BoolVarP(&options.All, "all", "a", false, "Show all containers (default shows just running)")
flags.BoolVar(&options.noStream, "no-stream", false, "Disable streaming stats and only pull the first result") flags.BoolVar(&options.NoStream, "no-stream", false, "Disable streaming stats and only pull the first result")
flags.BoolVar(&options.noTrunc, "no-trunc", false, "Do not truncate output") flags.BoolVar(&options.NoTrunc, "no-trunc", false, "Do not truncate output")
flags.StringVar(&options.format, "format", "", flagsHelper.FormatHelp) flags.StringVar(&options.Format, "format", "", flagsHelper.FormatHelp)
return cmd return cmd
} }
// runStats displays a live stream of resource usage statistics for one or more containers. // acceptedStatsFilters is the list of filters accepted by [RunStats] (through
// the [StatsOptions.Filters] option).
//
// TODO(thaJeztah): don't hard-code the list of accept filters, and expand
// to the intersection of filters accepted by both "container list" and
// "system events". Validating filters may require an initial API call
// to both endpoints ("container list" and "system events").
var acceptedStatsFilters = map[string]bool{
"label": true,
}
// 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. // This shows real-time information on CPU usage, memory usage, and network I/O.
// //
//nolint:gocyclo //nolint:gocyclo
func runStats(ctx context.Context, dockerCLI command.Cli, options *statsOptions) error { func RunStats(ctx context.Context, dockerCLI command.Cli, options *StatsOptions) error {
apiClient := dockerCLI.Client() apiClient := dockerCLI.Client()
// Get the daemonOSType if not set already // Get the daemonOSType if not set already
@ -88,23 +117,34 @@ func runStats(ctx context.Context, dockerCLI command.Cli, options *statsOptions)
// waitFirst is a WaitGroup to wait first stat data's reach for each container // waitFirst is a WaitGroup to wait first stat data's reach for each container
waitFirst := &sync.WaitGroup{} waitFirst := &sync.WaitGroup{}
// closeChan is a non-buffered channel used to collect errors from goroutines.
closeChan := make(chan error) closeChan := make(chan error)
cStats := stats{} cStats := stats{}
showAll := len(options.containers) == 0 showAll := len(options.Containers) == 0
if showAll { if showAll {
// If no names were specified, start a long-running goroutine which // If no names were specified, start a long-running goroutine which
// monitors container events. We make sure we're subscribed before // monitors container events. We make sure we're subscribed before
// retrieving the list of running containers to avoid a race where we // retrieving the list of running containers to avoid a race where we
// would "miss" a creation. // would "miss" a creation.
started := make(chan struct{}) started := make(chan struct{})
if options.Filters == nil {
f := filters.NewArgs()
options.Filters = &f
}
if err := options.Filters.Validate(acceptedStatsFilters); err != nil {
return err
}
eh := command.InitEventHandler() eh := command.InitEventHandler()
if options.all { if options.All {
eh.Handle(events.ActionCreate, func(e events.Message) { eh.Handle(events.ActionCreate, func(e events.Message) {
s := NewStats(e.Actor.ID[:12]) s := NewStats(e.Actor.ID[:12])
if cStats.add(s) { if cStats.add(s) {
waitFirst.Add(1) waitFirst.Add(1)
go collect(ctx, s, apiClient, !options.noStream, waitFirst) go collect(ctx, s, apiClient, !options.NoStream, waitFirst)
} }
}) })
} }
@ -113,11 +153,11 @@ func runStats(ctx context.Context, dockerCLI command.Cli, options *statsOptions)
s := NewStats(e.Actor.ID[:12]) s := NewStats(e.Actor.ID[:12])
if cStats.add(s) { if cStats.add(s) {
waitFirst.Add(1) waitFirst.Add(1)
go collect(ctx, s, apiClient, !options.noStream, waitFirst) go collect(ctx, s, apiClient, !options.NoStream, waitFirst)
} }
}) })
if !options.all { if !options.All {
eh.Handle(events.ActionDie, func(e events.Message) { eh.Handle(events.ActionDie, func(e events.Message) {
cStats.remove(e.Actor.ID[:12]) cStats.remove(e.Actor.ID[:12])
}) })
@ -126,7 +166,11 @@ func runStats(ctx context.Context, dockerCLI command.Cli, options *statsOptions)
// monitorContainerEvents watches for container creation and removal (only // monitorContainerEvents watches for container creation and removal (only
// used when calling `docker stats` without arguments). // used when calling `docker stats` without arguments).
monitorContainerEvents := func(started chan<- struct{}, c chan events.Message, stopped <-chan struct{}) { monitorContainerEvents := func(started chan<- struct{}, c chan events.Message, stopped <-chan struct{}) {
f := filters.NewArgs() // Create a copy of the custom filters so that we don't mutate
// the original set of filters. Custom filters are used both
// to list containers and to filter events, but the "type" filter
// is not valid for filtering containers.
f := options.Filters.Clone()
f.Add("type", string(events.ContainerEventType)) f.Add("type", string(events.ContainerEventType))
eventChan, errChan := apiClient.Events(ctx, types.EventsOptions{ eventChan, errChan := apiClient.Events(ctx, types.EventsOptions{
Filters: f, Filters: f,
@ -161,7 +205,8 @@ func runStats(ctx context.Context, dockerCLI command.Cli, options *statsOptions)
// After the initial list was collected, we start listening for events // After the initial list was collected, we start listening for events
// to refresh the list of containers. // to refresh the list of containers.
cs, err := apiClient.ContainerList(ctx, container.ListOptions{ cs, err := apiClient.ContainerList(ctx, container.ListOptions{
All: options.all, All: options.All,
Filters: *options.Filters,
}) })
if err != nil { if err != nil {
return err return err
@ -170,20 +215,28 @@ func runStats(ctx context.Context, dockerCLI command.Cli, options *statsOptions)
s := NewStats(ctr.ID[:12]) s := NewStats(ctr.ID[:12])
if cStats.add(s) { if cStats.add(s) {
waitFirst.Add(1) waitFirst.Add(1)
go collect(ctx, s, apiClient, !options.noStream, waitFirst) go collect(ctx, s, apiClient, !options.NoStream, waitFirst)
} }
} }
// make sure each container get at least one valid stat data // make sure each container get at least one valid stat data
waitFirst.Wait() waitFirst.Wait()
} else { } else {
// TODO(thaJeztah): re-implement options.Containers as a filter so that
// only a single code-path is needed, and custom filters can be combined
// with a list of container names/IDs.
if options.Filters != nil && options.Filters.Len() > 0 {
return fmt.Errorf("filtering is not supported when specifying a list of containers")
}
// Create the list of containers, and start collecting stats for all // Create the list of containers, and start collecting stats for all
// containers passed. // containers passed.
for _, ctr := range options.containers { for _, ctr := range options.Containers {
s := NewStats(ctr) s := NewStats(ctr)
if cStats.add(s) { if cStats.add(s) {
waitFirst.Add(1) waitFirst.Add(1)
go collect(ctx, s, apiClient, !options.noStream, waitFirst) go collect(ctx, s, apiClient, !options.NoStream, waitFirst)
} }
} }
@ -206,7 +259,7 @@ func runStats(ctx context.Context, dockerCLI command.Cli, options *statsOptions)
} }
} }
format := options.format format := options.Format
if len(format) == 0 { if len(format) == 0 {
if len(dockerCLI.ConfigFile().StatsFormat) > 0 { if len(dockerCLI.ConfigFile().StatsFormat) > 0 {
format = dockerCLI.ConfigFile().StatsFormat format = dockerCLI.ConfigFile().StatsFormat
@ -219,7 +272,7 @@ func runStats(ctx context.Context, dockerCLI command.Cli, options *statsOptions)
Format: NewStatsFormat(format, daemonOSType), Format: NewStatsFormat(format, daemonOSType),
} }
cleanScreen := func() { cleanScreen := func() {
if !options.noStream { if !options.NoStream {
_, _ = fmt.Fprint(dockerCLI.Out(), "\033[2J") _, _ = fmt.Fprint(dockerCLI.Out(), "\033[2J")
_, _ = fmt.Fprint(dockerCLI.Out(), "\033[H") _, _ = fmt.Fprint(dockerCLI.Out(), "\033[H")
} }
@ -236,13 +289,13 @@ func runStats(ctx context.Context, dockerCLI command.Cli, options *statsOptions)
ccStats = append(ccStats, c.GetStatistics()) ccStats = append(ccStats, c.GetStatistics())
} }
cStats.mu.RUnlock() cStats.mu.RUnlock()
if err = statsFormatWrite(statsCtx, ccStats, daemonOSType, !options.noTrunc); err != nil { if err = statsFormatWrite(statsCtx, ccStats, daemonOSType, !options.NoTrunc); err != nil {
break break
} }
if len(cStats.cs) == 0 && !showAll { if len(cStats.cs) == 0 && !showAll {
break break
} }
if options.noStream { if options.NoStream {
break break
} }
select { select {