2016-09-08 13:11:39 -04:00
|
|
|
package container
|
|
|
|
|
|
|
|
import (
|
2024-10-30 16:12:58 -04:00
|
|
|
"bytes"
|
2018-05-03 21:02:44 -04:00
|
|
|
"context"
|
2016-09-08 13:11:39 -04:00
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"strings"
|
|
|
|
"sync"
|
|
|
|
"time"
|
|
|
|
|
2017-04-17 18:07:56 -04:00
|
|
|
"github.com/docker/cli/cli"
|
|
|
|
"github.com/docker/cli/cli/command"
|
2022-03-30 09:27:25 -04:00
|
|
|
"github.com/docker/cli/cli/command/completion"
|
2017-04-17 18:07:56 -04:00
|
|
|
"github.com/docker/cli/cli/command/formatter"
|
2021-03-09 18:45:56 -05:00
|
|
|
flagsHelper "github.com/docker/cli/cli/flags"
|
2023-10-13 14:34:32 -04:00
|
|
|
"github.com/docker/docker/api/types/container"
|
2016-09-08 13:11:39 -04:00
|
|
|
"github.com/docker/docker/api/types/events"
|
|
|
|
"github.com/docker/docker/api/types/filters"
|
2017-03-27 21:21:59 -04:00
|
|
|
"github.com/pkg/errors"
|
2024-03-11 07:30:44 -04:00
|
|
|
"github.com/sirupsen/logrus"
|
2016-09-08 13:11:39 -04:00
|
|
|
"github.com/spf13/cobra"
|
|
|
|
)
|
|
|
|
|
2023-11-30 03:37:18 -05:00
|
|
|
// StatsOptions defines options for [RunStats].
|
|
|
|
type StatsOptions struct {
|
|
|
|
// All allows including both running and stopped containers. The default
|
2023-12-20 10:17:21 -05:00
|
|
|
// is to only include running containers.
|
2023-11-30 03:37:18 -05:00
|
|
|
All bool
|
2023-12-20 10:17:21 -05:00
|
|
|
|
2023-11-30 03:37:18 -05:00
|
|
|
// NoStream disables streaming stats. If enabled, stats are collected once,
|
2023-12-20 10:17:21 -05:00
|
|
|
// and the result is printed.
|
2023-11-30 03:37:18 -05:00
|
|
|
NoStream bool
|
2023-12-20 10:17:21 -05:00
|
|
|
|
2023-11-30 03:37:18 -05:00
|
|
|
// NoTrunc disables truncating the output. The default is to truncate
|
2023-12-20 10:17:21 -05:00
|
|
|
// output such as container-IDs.
|
2023-11-30 03:37:18 -05:00
|
|
|
NoTrunc bool
|
2023-12-20 10:17:21 -05:00
|
|
|
|
2023-11-30 03:37:18 -05:00
|
|
|
// Format is a custom template to use for presenting the stats.
|
2023-12-20 10:17:21 -05:00
|
|
|
// Refer to [flagsHelper.FormatHelp] for accepted formats.
|
2023-11-30 03:37:18 -05:00
|
|
|
Format string
|
2023-12-20 10:17:21 -05:00
|
|
|
|
2023-11-30 03:37:18 -05:00
|
|
|
// Containers is the list of container names or IDs to include in the stats.
|
|
|
|
// If empty, all containers are included. It is mutually exclusive with the
|
|
|
|
// 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
|
2016-09-08 13:11:39 -04:00
|
|
|
}
|
|
|
|
|
2023-12-20 05:23:05 -05:00
|
|
|
// NewStatsCommand creates a new [cobra.Command] for "docker stats".
|
|
|
|
func NewStatsCommand(dockerCLI command.Cli) *cobra.Command {
|
2023-11-30 03:37:18 -05:00
|
|
|
options := StatsOptions{}
|
2016-09-08 13:11:39 -04:00
|
|
|
|
|
|
|
cmd := &cobra.Command{
|
|
|
|
Use: "stats [OPTIONS] [CONTAINER...]",
|
|
|
|
Short: "Display a live stream of container(s) resource usage statistics",
|
|
|
|
Args: cli.RequiresMinArgs(0),
|
|
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
2023-11-30 03:37:18 -05:00
|
|
|
options.Containers = args
|
|
|
|
return RunStats(cmd.Context(), dockerCLI, &options)
|
2016-09-08 13:11:39 -04:00
|
|
|
},
|
cli: use custom annotation for aliases
Cobra allows for aliases to be defined for a command, but only allows these
to be defined at the same level (for example, `docker image ls` as alias for
`docker image list`). Our CLI has some commands that are available both as a
top-level shorthand as well as `docker <object> <verb>` subcommands. For example,
`docker ps` is a shorthand for `docker container ps` / `docker container ls`.
This patch introduces a custom "aliases" annotation that can be used to print
all available aliases for a command. While this requires these aliases to be
defined manually, in practice the list of aliases rarely changes, so maintenance
should be minimal.
As a convention, we could consider the first command in this list to be the
canonical command, so that we can use this information to add redirects in
our documentation in future.
Before this patch:
docker images --help
Usage: docker images [OPTIONS] [REPOSITORY[:TAG]]
List images
Options:
-a, --all Show all images (default hides intermediate images)
...
With this patch:
docker images --help
Usage: docker images [OPTIONS] [REPOSITORY[:TAG]]
List images
Aliases:
docker image ls, docker image list, docker images
Options:
-a, --all Show all images (default hides intermediate images)
...
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2022-06-28 04:52:25 -04:00
|
|
|
Annotations: map[string]string{
|
|
|
|
"aliases": "docker container stats, docker stats",
|
|
|
|
},
|
2023-12-20 05:23:05 -05:00
|
|
|
ValidArgsFunction: completion.ContainerNames(dockerCLI, false),
|
2016-09-08 13:11:39 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
flags := cmd.Flags()
|
2023-11-30 03:37:18 -05:00
|
|
|
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.NoTrunc, "no-trunc", false, "Do not truncate output")
|
|
|
|
flags.StringVar(&options.Format, "format", "", flagsHelper.FormatHelp)
|
2016-09-08 13:11:39 -04:00
|
|
|
return cmd
|
|
|
|
}
|
|
|
|
|
2023-11-30 03:37:18 -05:00
|
|
|
// 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.
|
2016-09-08 13:11:39 -04:00
|
|
|
// This shows real-time information on CPU usage, memory usage, and network I/O.
|
2022-07-13 06:29:49 -04:00
|
|
|
//
|
|
|
|
//nolint:gocyclo
|
2023-11-30 03:37:18 -05:00
|
|
|
func RunStats(ctx context.Context, dockerCLI command.Cli, options *StatsOptions) error {
|
2023-12-20 05:23:05 -05:00
|
|
|
apiClient := dockerCLI.Client()
|
2016-09-08 13:11:39 -04:00
|
|
|
|
|
|
|
// waitFirst is a WaitGroup to wait first stat data's reach for each container
|
|
|
|
waitFirst := &sync.WaitGroup{}
|
2023-11-30 03:37:18 -05:00
|
|
|
// closeChan is a non-buffered channel used to collect errors from goroutines.
|
2023-12-20 05:54:12 -05:00
|
|
|
closeChan := make(chan error)
|
2016-09-08 13:11:39 -04:00
|
|
|
cStats := stats{}
|
|
|
|
|
2023-11-30 03:37:18 -05:00
|
|
|
showAll := len(options.Containers) == 0
|
2016-09-08 13:11:39 -04:00
|
|
|
if showAll {
|
2023-12-20 05:23:05 -05:00
|
|
|
// If no names were specified, start a long-running goroutine which
|
2016-09-08 13:11:39 -04:00
|
|
|
// monitors container events. We make sure we're subscribed before
|
|
|
|
// retrieving the list of running containers to avoid a race where we
|
|
|
|
// would "miss" a creation.
|
|
|
|
started := make(chan struct{})
|
2023-11-30 03:37:18 -05:00
|
|
|
|
|
|
|
if options.Filters == nil {
|
|
|
|
f := filters.NewArgs()
|
|
|
|
options.Filters = &f
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := options.Filters.Validate(acceptedStatsFilters); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2024-03-11 07:30:44 -04:00
|
|
|
eh := newEventHandler()
|
2023-11-30 03:37:18 -05:00
|
|
|
if options.All {
|
2024-03-11 07:30:44 -04:00
|
|
|
eh.setHandler(events.ActionCreate, func(e events.Message) {
|
2023-10-13 14:34:32 -04:00
|
|
|
s := NewStats(e.Actor.ID[:12])
|
2016-09-08 13:11:39 -04:00
|
|
|
if cStats.add(s) {
|
|
|
|
waitFirst.Add(1)
|
2023-11-30 03:37:18 -05:00
|
|
|
go collect(ctx, s, apiClient, !options.NoStream, waitFirst)
|
2016-09-08 13:11:39 -04:00
|
|
|
}
|
2023-12-20 05:34:34 -05:00
|
|
|
})
|
|
|
|
}
|
2016-09-08 13:11:39 -04:00
|
|
|
|
2024-03-11 07:30:44 -04:00
|
|
|
eh.setHandler(events.ActionStart, func(e events.Message) {
|
2023-10-13 14:34:32 -04:00
|
|
|
s := NewStats(e.Actor.ID[:12])
|
2016-09-08 13:11:39 -04:00
|
|
|
if cStats.add(s) {
|
|
|
|
waitFirst.Add(1)
|
2023-11-30 03:37:18 -05:00
|
|
|
go collect(ctx, s, apiClient, !options.NoStream, waitFirst)
|
2016-09-08 13:11:39 -04:00
|
|
|
}
|
|
|
|
})
|
|
|
|
|
2023-11-30 03:37:18 -05:00
|
|
|
if !options.All {
|
2024-03-11 07:30:44 -04:00
|
|
|
eh.setHandler(events.ActionDie, func(e events.Message) {
|
2023-10-13 14:34:32 -04:00
|
|
|
cStats.remove(e.Actor.ID[:12])
|
2023-12-20 05:34:34 -05:00
|
|
|
})
|
|
|
|
}
|
2016-09-08 13:11:39 -04:00
|
|
|
|
2023-12-20 05:54:12 -05:00
|
|
|
// monitorContainerEvents watches for container creation and removal (only
|
|
|
|
// used when calling `docker stats` without arguments).
|
|
|
|
monitorContainerEvents := func(started chan<- struct{}, c chan events.Message, stopped <-chan struct{}) {
|
2023-11-30 03:37:18 -05:00
|
|
|
// 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()
|
2023-12-20 05:54:12 -05:00
|
|
|
f.Add("type", string(events.ContainerEventType))
|
2024-06-09 07:54:37 -04:00
|
|
|
eventChan, errChan := apiClient.Events(ctx, events.ListOptions{
|
2023-12-20 05:54:12 -05:00
|
|
|
Filters: f,
|
|
|
|
})
|
|
|
|
|
|
|
|
// Whether we successfully subscribed to eventChan or not, we can now
|
|
|
|
// unblock the main goroutine.
|
|
|
|
close(started)
|
|
|
|
defer close(c)
|
|
|
|
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case <-stopped:
|
|
|
|
return
|
|
|
|
case event := <-eventChan:
|
|
|
|
c <- event
|
|
|
|
case err := <-errChan:
|
|
|
|
closeChan <- err
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-09-08 13:11:39 -04:00
|
|
|
eventChan := make(chan events.Message)
|
2024-03-11 07:30:44 -04:00
|
|
|
go eh.watch(eventChan)
|
2022-05-10 06:10:12 -04:00
|
|
|
stopped := make(chan struct{})
|
|
|
|
go monitorContainerEvents(started, eventChan, stopped)
|
|
|
|
defer close(stopped)
|
2016-09-08 13:11:39 -04:00
|
|
|
<-started
|
|
|
|
|
2023-12-20 10:01:08 -05:00
|
|
|
// Fetch the initial list of containers and collect stats for them.
|
|
|
|
// After the initial list was collected, we start listening for events
|
|
|
|
// to refresh the list of containers.
|
|
|
|
cs, err := apiClient.ContainerList(ctx, container.ListOptions{
|
2023-11-30 03:37:18 -05:00
|
|
|
All: options.All,
|
|
|
|
Filters: *options.Filters,
|
2023-12-20 10:01:08 -05:00
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
for _, ctr := range cs {
|
|
|
|
s := NewStats(ctr.ID[:12])
|
|
|
|
if cStats.add(s) {
|
|
|
|
waitFirst.Add(1)
|
2023-11-30 03:37:18 -05:00
|
|
|
go collect(ctx, s, apiClient, !options.NoStream, waitFirst)
|
2023-12-20 10:01:08 -05:00
|
|
|
}
|
|
|
|
}
|
2020-11-10 11:46:53 -05:00
|
|
|
|
2020-10-12 04:32:53 -04:00
|
|
|
// make sure each container get at least one valid stat data
|
|
|
|
waitFirst.Wait()
|
2016-09-08 13:11:39 -04:00
|
|
|
} else {
|
2023-11-30 03:37:18 -05:00
|
|
|
// 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 {
|
linting: fmt.Errorf can be replaced with errors.New (perfsprint)
internal/test/cli.go:175:14: fmt.Errorf can be replaced with errors.New (perfsprint)
return nil, fmt.Errorf("no notary client available unless defined")
^
cli/command/cli.go:318:29: fmt.Errorf can be replaced with errors.New (perfsprint)
return docker.Endpoint{}, fmt.Errorf("no context store initialized")
^
cli/command/container/attach.go:161:11: fmt.Errorf can be replaced with errors.New (perfsprint)
return fmt.Errorf(result.Error.Message)
^
cli/command/container/opts.go:577:16: fmt.Errorf can be replaced with errors.New (perfsprint)
return nil, fmt.Errorf("--health-start-period cannot be negative")
^
cli/command/container/opts.go:580:16: fmt.Errorf can be replaced with errors.New (perfsprint)
return nil, fmt.Errorf("--health-start-interval cannot be negative")
^
cli/command/container/stats.go:221:11: fmt.Errorf can be replaced with errors.New (perfsprint)
return fmt.Errorf("filtering is not supported when specifying a list of containers")
^
cli/command/container/attach_test.go:82:17: fmt.Errorf can be replaced with errors.New (perfsprint)
expectedErr = fmt.Errorf("unexpected error")
^
cli/command/container/create_test.go:234:40: fmt.Errorf can be replaced with errors.New (perfsprint)
return container.CreateResponse{}, fmt.Errorf("shouldn't try to pull image")
^
cli/command/container/list_test.go:150:17: fmt.Errorf can be replaced with errors.New (perfsprint)
return nil, fmt.Errorf("error listing containers")
^
cli/command/container/rm_test.go:40:31: fmt.Errorf can be replaced with errors.New (perfsprint)
return errdefs.NotFound(fmt.Errorf("Error: no such container: " + container))
^
cli/command/container/run_test.go:138:40: fmt.Errorf can be replaced with errors.New (perfsprint)
return container.CreateResponse{}, fmt.Errorf("shouldn't try to pull image")
^
cli/command/image/pull_test.go:115:49: fmt.Errorf can be replaced with errors.New (perfsprint)
return io.NopCloser(strings.NewReader("")), fmt.Errorf("shouldn't try to pull image")
^
cli/command/network/connect.go:88:16: fmt.Errorf can be replaced with errors.New (perfsprint)
return nil, fmt.Errorf("invalid key/value pair format in driver options")
^
cli/command/plugin/create_test.go:96:11: fmt.Errorf can be replaced with errors.New (perfsprint)
return fmt.Errorf("Error creating plugin")
^
cli/command/plugin/disable_test.go:32:12: fmt.Errorf can be replaced with errors.New (perfsprint)
return fmt.Errorf("Error disabling plugin")
^
cli/command/plugin/enable_test.go:32:12: fmt.Errorf can be replaced with errors.New (perfsprint)
return fmt.Errorf("failed to enable plugin")
^
cli/command/plugin/inspect_test.go:55:22: fmt.Errorf can be replaced with errors.New (perfsprint)
return nil, nil, fmt.Errorf("error inspecting plugin")
^
cli/command/plugin/install_test.go:43:17: fmt.Errorf can be replaced with errors.New (perfsprint)
return nil, fmt.Errorf("Error installing plugin")
^
cli/command/plugin/install_test.go:51:17: fmt.Errorf can be replaced with errors.New (perfsprint)
return nil, fmt.Errorf("(image) when fetching")
^
cli/command/plugin/install_test.go:95:17: fmt.Errorf can be replaced with errors.New (perfsprint)
return nil, fmt.Errorf("should not try to install plugin")
^
cli/command/plugin/list_test.go:35:41: fmt.Errorf can be replaced with errors.New (perfsprint)
return types.PluginsListResponse{}, fmt.Errorf("error listing plugins")
^
cli/command/plugin/remove_test.go:27:12: fmt.Errorf can be replaced with errors.New (perfsprint)
return fmt.Errorf("Error removing plugin")
^
cli/command/registry/login_test.go:36:46: fmt.Errorf can be replaced with errors.New (perfsprint)
return registrytypes.AuthenticateOKBody{}, fmt.Errorf("Invalid Username or Password")
^
cli/command/registry/login_test.go:44:46: fmt.Errorf can be replaced with errors.New (perfsprint)
return registrytypes.AuthenticateOKBody{}, fmt.Errorf(errUnknownUser)
^
cli/command/system/info.go:190:10: fmt.Errorf can be replaced with errors.New (perfsprint)
return fmt.Errorf("errors pretty printing info")
^
cli/command/system/prune.go:77:10: fmt.Errorf can be replaced with errors.New (perfsprint)
return fmt.Errorf(`ERROR: The "until" filter is not supported with "--volumes"`)
^
cli/command/system/version_test.go:19:28: fmt.Errorf can be replaced with errors.New (perfsprint)
return types.Version{}, fmt.Errorf("no server")
^
cli/command/trust/key_load.go:112:22: fmt.Errorf can be replaced with errors.New (perfsprint)
return []byte{}, fmt.Errorf("could not decrypt key")
^
cli/command/trust/revoke.go:44:10: fmt.Errorf can be replaced with errors.New (perfsprint)
return fmt.Errorf("cannot use a digest reference for IMAGE:TAG")
^
cli/command/trust/revoke.go:105:10: fmt.Errorf can be replaced with errors.New (perfsprint)
return fmt.Errorf("no signed tags to remove")
^
cli/command/trust/signer_add.go:56:10: fmt.Errorf can be replaced with errors.New (perfsprint)
return fmt.Errorf("releases is a reserved keyword, please use a different signer name")
^
cli/command/trust/signer_add.go:60:10: fmt.Errorf can be replaced with errors.New (perfsprint)
return fmt.Errorf("path to a public key must be provided using the `--key` flag")
^
opts/config.go:71:10: fmt.Errorf can be replaced with errors.New (perfsprint)
return fmt.Errorf("source is required")
^
opts/mount.go:168:10: fmt.Errorf can be replaced with errors.New (perfsprint)
return fmt.Errorf("type is required")
^
opts/mount.go:172:10: fmt.Errorf can be replaced with errors.New (perfsprint)
return fmt.Errorf("target is required")
^
opts/network.go:90:11: fmt.Errorf can be replaced with errors.New (perfsprint)
return fmt.Errorf("network name/id is not specified")
^
opts/network.go:129:18: fmt.Errorf can be replaced with errors.New (perfsprint)
return "", "", fmt.Errorf("invalid key value pair format in driver options")
^
opts/opts.go:404:13: fmt.Errorf can be replaced with errors.New (perfsprint)
return 0, fmt.Errorf("value is too precise")
^
opts/opts.go:412:18: fmt.Errorf can be replaced with errors.New (perfsprint)
return "", "", fmt.Errorf("empty string specified for links")
^
opts/parse.go:84:37: fmt.Errorf can be replaced with errors.New (perfsprint)
return container.RestartPolicy{}, fmt.Errorf("invalid restart policy format: no policy provided before colon")
^
opts/parse.go:89:38: fmt.Errorf can be replaced with errors.New (perfsprint)
return container.RestartPolicy{}, fmt.Errorf("invalid restart policy format: maximum retry count must be an integer")
^
opts/port.go:105:13: fmt.Errorf can be replaced with errors.New (perfsprint)
return fmt.Errorf("hostip is not supported")
^
opts/secret.go:70:10: fmt.Errorf can be replaced with errors.New (perfsprint)
return fmt.Errorf("source is required")
^
opts/env_test.go:57:11: fmt.Errorf can be replaced with errors.New (perfsprint)
err: fmt.Errorf("invalid environment variable: =a"),
^
opts/env_test.go:93:11: fmt.Errorf can be replaced with errors.New (perfsprint)
err: fmt.Errorf("invalid environment variable: ="),
^
cli-plugins/manager/error_test.go:16:11: fmt.Errorf can be replaced with errors.New (perfsprint)
inner := fmt.Errorf("testing")
^
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-06-10 14:47:07 -04:00
|
|
|
return errors.New("filtering is not supported when specifying a list of containers")
|
2023-11-30 03:37:18 -05:00
|
|
|
}
|
|
|
|
|
2023-12-20 10:01:08 -05:00
|
|
|
// Create the list of containers, and start collecting stats for all
|
|
|
|
// containers passed.
|
2023-11-30 03:37:18 -05:00
|
|
|
for _, ctr := range options.Containers {
|
2023-12-20 10:01:08 -05:00
|
|
|
s := NewStats(ctr)
|
2016-09-08 13:11:39 -04:00
|
|
|
if cStats.add(s) {
|
|
|
|
waitFirst.Add(1)
|
2023-11-30 03:37:18 -05:00
|
|
|
go collect(ctx, s, apiClient, !options.NoStream, waitFirst)
|
2016-09-08 13:11:39 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// We don't expect any asynchronous errors: closeChan can be closed.
|
|
|
|
close(closeChan)
|
|
|
|
|
2020-10-12 04:32:53 -04:00
|
|
|
// make sure each container get at least one valid stat data
|
|
|
|
waitFirst.Wait()
|
2020-11-10 11:46:53 -05:00
|
|
|
|
2016-09-08 13:11:39 -04:00
|
|
|
var errs []string
|
2022-03-28 05:29:06 -04:00
|
|
|
cStats.mu.RLock()
|
2016-09-08 13:11:39 -04:00
|
|
|
for _, c := range cStats.cs {
|
2016-12-16 15:17:39 -05:00
|
|
|
if err := c.GetError(); err != nil {
|
|
|
|
errs = append(errs, err.Error())
|
2016-09-08 13:11:39 -04:00
|
|
|
}
|
|
|
|
}
|
2022-03-28 05:29:06 -04:00
|
|
|
cStats.mu.RUnlock()
|
2016-09-08 13:11:39 -04:00
|
|
|
if len(errs) > 0 {
|
2016-12-24 19:05:37 -05:00
|
|
|
return errors.New(strings.Join(errs, "\n"))
|
2016-09-08 13:11:39 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-11-30 03:37:18 -05:00
|
|
|
format := options.Format
|
2016-10-28 14:48:25 -04:00
|
|
|
if len(format) == 0 {
|
2023-12-20 05:23:05 -05:00
|
|
|
if len(dockerCLI.ConfigFile().StatsFormat) > 0 {
|
|
|
|
format = dockerCLI.ConfigFile().StatsFormat
|
2016-10-28 14:48:25 -04:00
|
|
|
} else {
|
|
|
|
format = formatter.TableFormatKey
|
|
|
|
}
|
2016-07-18 14:30:15 -04:00
|
|
|
}
|
2024-03-11 06:05:56 -04:00
|
|
|
if daemonOSType == "" {
|
|
|
|
// Get the daemonOSType if not set already. The daemonOSType variable
|
|
|
|
// should already be set when collecting stats as part of "collect()",
|
|
|
|
// so we unlikely hit this code in practice.
|
|
|
|
daemonOSType = dockerCLI.ServerInfo().OSType
|
|
|
|
}
|
2024-10-30 16:12:58 -04:00
|
|
|
|
|
|
|
// Buffer to store formatted stats text.
|
|
|
|
// Once formatted, it will be printed in one write to avoid screen flickering.
|
|
|
|
var statsTextBuffer bytes.Buffer
|
|
|
|
|
2016-07-18 14:30:15 -04:00
|
|
|
statsCtx := formatter.Context{
|
2024-10-30 16:12:58 -04:00
|
|
|
Output: &statsTextBuffer,
|
2018-10-23 11:05:44 -04:00
|
|
|
Format: NewStatsFormat(format, daemonOSType),
|
2016-07-18 14:30:15 -04:00
|
|
|
}
|
2016-09-08 13:11:39 -04:00
|
|
|
|
2016-07-18 14:30:15 -04:00
|
|
|
var err error
|
2019-04-02 05:11:36 -04:00
|
|
|
ticker := time.NewTicker(500 * time.Millisecond)
|
|
|
|
defer ticker.Stop()
|
|
|
|
for range ticker.C {
|
2023-12-20 05:23:05 -05:00
|
|
|
var ccStats []StatsEntry
|
2022-03-28 05:29:06 -04:00
|
|
|
cStats.mu.RLock()
|
2016-09-22 08:54:41 -04:00
|
|
|
for _, c := range cStats.cs {
|
2023-12-20 05:23:05 -05:00
|
|
|
ccStats = append(ccStats, c.GetStatistics())
|
2016-09-22 08:54:41 -04:00
|
|
|
}
|
2022-03-28 05:29:06 -04:00
|
|
|
cStats.mu.RUnlock()
|
2024-10-30 16:12:58 -04:00
|
|
|
|
|
|
|
if !options.NoStream {
|
|
|
|
// Start by clearing the screen and moving the cursor to the top-left
|
|
|
|
_, _ = fmt.Fprint(&statsTextBuffer, "\033[2J\033[H")
|
|
|
|
}
|
|
|
|
|
2023-11-30 03:37:18 -05:00
|
|
|
if err = statsFormatWrite(statsCtx, ccStats, daemonOSType, !options.NoTrunc); err != nil {
|
2016-07-18 14:30:15 -04:00
|
|
|
break
|
2016-09-08 13:11:39 -04:00
|
|
|
}
|
2024-10-30 16:12:58 -04:00
|
|
|
|
|
|
|
_, _ = fmt.Fprint(dockerCLI.Out(), statsTextBuffer.String())
|
|
|
|
|
|
|
|
statsTextBuffer.Reset()
|
|
|
|
|
2016-09-22 08:54:41 -04:00
|
|
|
if len(cStats.cs) == 0 && !showAll {
|
2016-07-18 14:30:15 -04:00
|
|
|
break
|
2016-09-08 13:11:39 -04:00
|
|
|
}
|
2023-11-30 03:37:18 -05:00
|
|
|
if options.NoStream {
|
2016-09-08 13:11:39 -04:00
|
|
|
break
|
|
|
|
}
|
|
|
|
select {
|
|
|
|
case err, ok := <-closeChan:
|
|
|
|
if ok {
|
|
|
|
if err != nil {
|
2023-12-20 05:23:05 -05:00
|
|
|
// Suppress "unexpected EOF" errors in the CLI so that
|
|
|
|
// it shuts down cleanly when the daemon restarts.
|
|
|
|
if errors.Is(err, io.ErrUnexpectedEOF) {
|
2016-09-08 13:11:39 -04:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
default:
|
|
|
|
// just skip
|
|
|
|
}
|
|
|
|
}
|
2016-07-18 14:30:15 -04:00
|
|
|
return err
|
2016-09-08 13:11:39 -04:00
|
|
|
}
|
2024-03-11 07:30:44 -04:00
|
|
|
|
|
|
|
// newEventHandler initializes and returns an eventHandler
|
|
|
|
func newEventHandler() *eventHandler {
|
|
|
|
return &eventHandler{handlers: make(map[events.Action]func(events.Message))}
|
|
|
|
}
|
|
|
|
|
|
|
|
// eventHandler allows for registering specific events to setHandler.
|
|
|
|
type eventHandler struct {
|
|
|
|
handlers map[events.Action]func(events.Message)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (eh *eventHandler) setHandler(action events.Action, handler func(events.Message)) {
|
|
|
|
eh.handlers[action] = handler
|
|
|
|
}
|
|
|
|
|
|
|
|
// watch ranges over the passed in event chan and processes the events based on the
|
|
|
|
// handlers created for a given action.
|
|
|
|
// To stop watching, close the event chan.
|
|
|
|
func (eh *eventHandler) watch(c <-chan events.Message) {
|
|
|
|
for e := range c {
|
|
|
|
h, exists := eh.handlers[e.Action]
|
|
|
|
if !exists {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
logrus.Debugf("event handler: received event: %v", e)
|
|
|
|
go h(e)
|
|
|
|
}
|
|
|
|
}
|