2016-09-08 13:11:39 -04:00
|
|
|
package container
|
|
|
|
|
|
|
|
import (
|
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"
|
2016-09-08 13:11:39 -04:00
|
|
|
"github.com/docker/docker/api/types"
|
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"
|
2016-09-08 13:11:39 -04:00
|
|
|
"github.com/spf13/cobra"
|
|
|
|
)
|
|
|
|
|
2023-12-20 10:17:21 -05:00
|
|
|
// statsOptions defines options for runStats.
|
2016-09-08 13:11:39 -04:00
|
|
|
type statsOptions struct {
|
2023-12-20 10:17:21 -05:00
|
|
|
// all allows including both running and stopped containers. The default
|
|
|
|
// is to only include running containers.
|
|
|
|
all bool
|
|
|
|
|
|
|
|
// noStream disables streaming stats. If enabled, stats are collected once,
|
|
|
|
// and the result is printed.
|
|
|
|
noStream bool
|
|
|
|
|
|
|
|
// noTrunc disables truncating the output. The default is to truncate
|
|
|
|
// output such as container-IDs.
|
|
|
|
noTrunc bool
|
|
|
|
|
|
|
|
// format is a custom template to use for presenting the stats.
|
|
|
|
// Refer to [flagsHelper.FormatHelp] for accepted formats.
|
|
|
|
format string
|
|
|
|
|
|
|
|
// containers is the list of container names or IDs to include in the stats.
|
|
|
|
// If empty, all containers are included.
|
2016-09-08 13:11:39 -04:00
|
|
|
containers []string
|
|
|
|
}
|
|
|
|
|
2023-12-20 05:23:05 -05:00
|
|
|
// NewStatsCommand creates a new [cobra.Command] for "docker stats".
|
|
|
|
func NewStatsCommand(dockerCLI command.Cli) *cobra.Command {
|
|
|
|
var 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-12-20 05:23:05 -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-12-20 05:23:05 -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
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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.
|
2022-07-13 06:29:49 -04:00
|
|
|
//
|
|
|
|
//nolint:gocyclo
|
2023-12-20 05:23:05 -05:00
|
|
|
func runStats(ctx context.Context, dockerCLI command.Cli, options *statsOptions) error {
|
|
|
|
apiClient := dockerCLI.Client()
|
2016-09-08 13:11:39 -04:00
|
|
|
|
2016-10-25 19:19:14 -04:00
|
|
|
// Get the daemonOSType if not set already
|
|
|
|
if daemonOSType == "" {
|
2023-12-20 05:23:05 -05:00
|
|
|
sv, err := apiClient.ServerVersion(ctx)
|
2016-10-25 19:19:14 -04:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
daemonOSType = sv.Os
|
|
|
|
}
|
|
|
|
|
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-12-20 05:54:12 -05:00
|
|
|
closeChan := make(chan error)
|
2016-09-08 13:11:39 -04:00
|
|
|
cStats := stats{}
|
|
|
|
|
2023-12-20 05:54:12 -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{})
|
2016-09-22 17:04:34 -04:00
|
|
|
eh := command.InitEventHandler()
|
2023-12-20 05:34:34 -05:00
|
|
|
if options.all {
|
|
|
|
eh.Handle(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-12-20 05:23:05 -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
|
|
|
|
2023-09-05 07:43:35 -04:00
|
|
|
eh.Handle(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-12-20 05:23:05 -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
|
|
|
if !options.all {
|
|
|
|
eh.Handle(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{}) {
|
|
|
|
f := filters.NewArgs()
|
|
|
|
f.Add("type", string(events.ContainerEventType))
|
|
|
|
eventChan, errChan := apiClient.Events(ctx, types.EventsOptions{
|
|
|
|
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)
|
|
|
|
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{
|
|
|
|
All: options.all,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
for _, ctr := range cs {
|
|
|
|
s := NewStats(ctr.ID[:12])
|
|
|
|
if cStats.add(s) {
|
|
|
|
waitFirst.Add(1)
|
|
|
|
go collect(ctx, s, apiClient, !options.noStream, waitFirst)
|
|
|
|
}
|
|
|
|
}
|
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-12-20 10:01:08 -05:00
|
|
|
// Create the list of containers, and start collecting stats for all
|
|
|
|
// containers passed.
|
|
|
|
for _, ctr := range options.containers {
|
|
|
|
s := NewStats(ctr)
|
2016-09-08 13:11:39 -04:00
|
|
|
if cStats.add(s) {
|
|
|
|
waitFirst.Add(1)
|
2023-12-20 05:23:05 -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-12-20 05:23:05 -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
|
|
|
}
|
|
|
|
statsCtx := formatter.Context{
|
2023-12-20 05:23:05 -05:00
|
|
|
Output: dockerCLI.Out(),
|
2018-10-23 11:05:44 -04:00
|
|
|
Format: NewStatsFormat(format, daemonOSType),
|
2016-07-18 14:30:15 -04:00
|
|
|
}
|
2016-09-22 08:54:41 -04:00
|
|
|
cleanScreen := func() {
|
2023-12-20 05:23:05 -05:00
|
|
|
if !options.noStream {
|
|
|
|
_, _ = fmt.Fprint(dockerCLI.Out(), "\033[2J")
|
|
|
|
_, _ = fmt.Fprint(dockerCLI.Out(), "\033[H")
|
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 {
|
2016-09-22 08:54:41 -04:00
|
|
|
cleanScreen()
|
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()
|
2023-12-20 05:23:05 -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
|
|
|
}
|
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-12-20 05:23:05 -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
|
|
|
}
|