diff --git a/cli/command/container/stats.go b/cli/command/container/stats.go index c0370f95e5f2..91d4945ed9ef 100644 --- a/cli/command/container/stats.go +++ b/cli/command/container/stats.go @@ -37,6 +37,11 @@ type StatsOptions struct { // output such as container-IDs. NoTrunc bool + // Sort is the column to sort the output by. Accepted values are "cpu", "mem", + // "mem%", "net", "block", and "pids" (descending), and "name" and "id" (ascending). If empty, + // containers are printed in the order they were added. + Sort string + // Format is a custom template to use for presenting the stats. // Refer to [flagsHelper.FormatHelp] for accepted formats. Format string @@ -88,6 +93,8 @@ func newStatsCommand(dockerCLI command.Cli) *cobra.Command { 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) + flags.StringVar(&options.Sort, "sort", "", `Sort output by column ("cpu", "mem", "mem%", "net", "block", "name", "id", "pids")`) + _ = cmd.RegisterFlagCompletionFunc("sort", completion.FromList("cpu", "mem", "mem%", "net", "block", "name", "id", "pids")) return cmd } @@ -107,6 +114,11 @@ var acceptedStatsFilters = map[string]bool{ // //nolint:gocyclo func RunStats(ctx context.Context, dockerCLI command.Cli, options *StatsOptions) error { + options.Sort = normalizeStatsSortKey(options.Sort) + if _, ok := statsSortKeys[options.Sort]; options.Sort != "" && !ok { + return errdefs.ErrInvalidArgument.WithMessage("invalid sort key '" + options.Sort + "': must be one of cpu, mem, mem%, net, block, name, id, pids") + } + apiClient := dockerCLI.Client() // Get the daemonOSType to handle platform-specific stats fields. @@ -307,6 +319,7 @@ func RunStats(ctx context.Context, dockerCLI command.Cli, options *StatsOptions) for _, c := range statsList { ccStats = append(ccStats, c.GetStatistics()) } + sortStats(ccStats, options.Sort) if err := statsFormatWrite(statsCtx, ccStats, daemonOSType, !options.NoTrunc); err != nil { return err } @@ -331,6 +344,7 @@ func RunStats(ctx context.Context, dockerCLI command.Cli, options *StatsOptions) for _, c := range statsList { ccStats = append(ccStats, c.GetStatistics()) } + sortStats(ccStats, options.Sort) if err := statsFormatWrite(statsCtx, ccStats, daemonOSType, !options.NoTrunc); err != nil { return err diff --git a/cli/command/container/stats_helpers.go b/cli/command/container/stats_helpers.go index 66571b530942..0d6c9fe6f390 100644 --- a/cli/command/container/stats_helpers.go +++ b/cli/command/container/stats_helpers.go @@ -1,10 +1,13 @@ package container import ( + "cmp" "context" "encoding/json" "errors" "io" + "slices" + "strings" "sync" "time" @@ -271,3 +274,35 @@ func calculateMemPercentUnixNoCache(limit float64, usedNoCache float64) float64 } return 0 } + +// statsSortKeys maps the values accepted by "docker stats --sort" to a +// comparison function. Resource columns sort in descending order (highest +// usage first), and "name" sorts in ascending order. +var statsSortKeys = map[string]func(a, b StatsEntry) int{ + "cpu": func(a, b StatsEntry) int { return cmp.Compare(b.CPUPercentage, a.CPUPercentage) }, + "mem": func(a, b StatsEntry) int { return cmp.Compare(b.Memory, a.Memory) }, + "mem%": func(a, b StatsEntry) int { return cmp.Compare(b.MemoryPercentage, a.MemoryPercentage) }, + "net": func(a, b StatsEntry) int { return cmp.Compare(b.NetworkRx+b.NetworkTx, a.NetworkRx+a.NetworkTx) }, + "block": func(a, b StatsEntry) int { return cmp.Compare(b.BlockRead+b.BlockWrite, a.BlockRead+a.BlockWrite) }, + "pids": func(a, b StatsEntry) int { return cmp.Compare(b.PidsCurrent, a.PidsCurrent) }, + "name": func(a, b StatsEntry) int { return strings.Compare(a.Name, b.Name) }, + "id": func(a, b StatsEntry) int { return strings.Compare(a.ID, b.ID) }, +} + +func normalizeStatsSortKey(key string) string { + return strings.ToLower(key) +} + +// sortStats sorts entries in place by the given key. Entries with equal +// values are ordered by container so that output is stable between frames. +// An empty key leaves the entries in their original order. +func sortStats(entries []StatsEntry, key string) { + key = normalizeStatsSortKey(key) + compare, ok := statsSortKeys[key] + if !ok { + return + } + slices.SortFunc(entries, func(a, b StatsEntry) int { + return cmp.Or(compare(a, b), strings.Compare(a.Container, b.Container)) + }) +} diff --git a/cli/command/container/stats_helpers_test.go b/cli/command/container/stats_helpers_test.go index 081b6be52012..49f0eaf95e20 100644 --- a/cli/command/container/stats_helpers_test.go +++ b/cli/command/container/stats_helpers_test.go @@ -61,3 +61,39 @@ func inDelta(x, y, delta float64) func() (bool, string) { return true, "" } } + +func TestSortStats(t *testing.T) { + entries := func() []StatsEntry { + return []StatsEntry{ + {Container: "a", ID: "3", Name: "web", CPUPercentage: 10, Memory: 300, MemoryPercentage: 30, NetworkRx: 10, NetworkTx: 30, BlockRead: 20, BlockWrite: 10, PidsCurrent: 2}, + {Container: "b", ID: "2", Name: "db", CPUPercentage: 50, Memory: 100, MemoryPercentage: 10, NetworkRx: 50, NetworkTx: 10, BlockRead: 10, BlockWrite: 50, PidsCurrent: 9}, + {Container: "c", ID: "1", Name: "cache", CPUPercentage: 10, Memory: 200, MemoryPercentage: 20, NetworkRx: 20, NetworkTx: 20, BlockRead: 30, BlockWrite: 20, PidsCurrent: 5}, + } + } + containers := func(s []StatsEntry) []string { + out := make([]string, 0, len(s)) + for _, e := range s { + out = append(out, e.Container) + } + return out + } + + for key, expected := range map[string][]string{ + "": {"a", "b", "c"}, // unsorted: original order + "cpu": {"b", "a", "c"}, // descending, tie broken by container + "mem": {"a", "c", "b"}, + "mem%": {"a", "c", "b"}, + "net": {"b", "a", "c"}, + "block": {"b", "c", "a"}, + "pids": {"b", "c", "a"}, + "name": {"c", "b", "a"}, // ascending + "id": {"c", "b", "a"}, // ascending + "CPU": {"b", "a", "c"}, // case-insensitive + } { + t.Run(key, func(t *testing.T) { + s := entries() + sortStats(s, key) + assert.DeepEqual(t, containers(s), expected) + }) + } +} diff --git a/contrib/completion/bash/docker b/contrib/completion/bash/docker index b9b7a28ec263..17d0c9685a49 100644 --- a/contrib/completion/bash/docker +++ b/contrib/completion/bash/docker @@ -2241,11 +2241,15 @@ _docker_container_stats() { --format) return ;; + --sort) + COMPREPLY=( $( compgen -W "cpu mem mem% net block name id pids" -- "$cur" ) ) + return + ;; esac case "$cur" in -*) - COMPREPLY=( $( compgen -W "--all -a --format --help --no-stream --no-trunc" -- "$cur" ) ) + COMPREPLY=( $( compgen -W "--all -a --format --help --no-stream --no-trunc --sort" -- "$cur" ) ) ;; *) __docker_complete_containers_running diff --git a/contrib/completion/fish/docker.fish b/contrib/completion/fish/docker.fish index bf1dd38b3b0c..9b9a7f45e08e 100644 --- a/contrib/completion/fish/docker.fish +++ b/contrib/completion/fish/docker.fish @@ -524,6 +524,7 @@ complete -c docker -A -f -n '__fish_seen_subcommand_from start' -a '(__fish_prin complete -c docker -f -n '__fish_docker_no_subcommand' -a stats -d "Display a live stream of one or more containers' resource usage statistics" complete -c docker -A -f -n '__fish_seen_subcommand_from stats' -l help -d 'Print usage' complete -c docker -A -f -n '__fish_seen_subcommand_from stats' -l no-stream -d 'Disable streaming stats and only pull the first result' +complete -c docker -A -f -n '__fish_seen_subcommand_from stats' -l sort -a 'cpu mem mem% net block name id pids' -d 'Sort output by column' complete -c docker -A -f -n '__fish_seen_subcommand_from stats' -a '(__fish_print_docker_containers running)' -d "Container" # stop diff --git a/contrib/completion/zsh/_docker b/contrib/completion/zsh/_docker index 1efb30f86a8a..4a999ca6a7de 100644 --- a/contrib/completion/zsh/_docker +++ b/contrib/completion/zsh/_docker @@ -908,6 +908,7 @@ __docker_container_subcommand() { "($help)--format=[Format the output using the given Go template]:template: " \ "($help)--no-stream[Disable streaming stats and only pull the first result]" \ "($help)--no-trunc[Do not truncate output]" \ + "($help)--sort=[Sort output by column]:column:(cpu mem mem% net block name id pids)" \ "($help -)*:containers:__docker_complete_running_containers" && ret=0 ;; (stop) diff --git a/docs/reference/commandline/container_stats.md b/docs/reference/commandline/container_stats.md index c1a9a44e9134..5abf5760f039 100644 --- a/docs/reference/commandline/container_stats.md +++ b/docs/reference/commandline/container_stats.md @@ -15,6 +15,7 @@ Display a live stream of container(s) resource usage statistics | [`--format`](#format) | `string` | | Format output using a custom template:
'table': Print output in table format with column headers (default)
'table TEMPLATE': Print output in table format using the given Go template
'json': Print in JSON format
'TEMPLATE': Print output using the given Go template.
Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates | | `--no-stream` | `bool` | | Disable streaming stats and only pull the first result | | `--no-trunc` | `bool` | | Do not truncate output | +| [`--sort`](#sort) | `string` | | Sort output by column (`cpu`, `mem`, `mem%`, `net`, `block`, `name`, `id`, `pids`) | @@ -129,6 +130,22 @@ CONTAINER ID NAME CPU % PRIV WORKING SET 9db7aa4d986d mad_wilson 9.59% 40.09 MiB 27.6 kB / 8.81 kB 17 MB / 20.1 MB ``` +### Sort the output (--sort) + +Use the `--sort` option to order containers by a column, similar to +`kubectl top --sort-by`. Resource columns (`cpu`, `mem`, `mem%`, `net`, `block`, +`pids`) sort in descending order so the busiest containers appear first; `name` +and `id` sort in ascending order. + +```console +$ docker stats --no-stream --sort mem + +CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS +67b2525d8ad1 db 0.31% 412.6MiB / 1.952GiB 20.64% 2.48kB / 0B 4.11MB / 0B 12 +b95a83497c91 web 0.28% 35.63MiB / 1.952GiB 1.78% 916B / 0B 147kB / 0B 9 +e5c383697914 cache 0.00% 5.196MiB / 1.952GiB 0.26% 71.2kB / 0B 770kB / 0B 1 +``` + ### Format the output (--format) The formatting option (`--format`) pretty prints container output diff --git a/docs/reference/commandline/stats.md b/docs/reference/commandline/stats.md index efb8cd129155..e64b667b894e 100644 --- a/docs/reference/commandline/stats.md +++ b/docs/reference/commandline/stats.md @@ -15,6 +15,7 @@ Display a live stream of container(s) resource usage statistics | `--format` | `string` | | Format output using a custom template:
'table': Print output in table format with column headers (default)
'table TEMPLATE': Print output in table format using the given Go template
'json': Print in JSON format
'TEMPLATE': Print output using the given Go template.
Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates | | `--no-stream` | `bool` | | Disable streaming stats and only pull the first result | | `--no-trunc` | `bool` | | Do not truncate output | +| `--sort` | `string` | | Sort output by column (`cpu`, `mem`, `mem%`, `net`, `block`, `name`, `id`, `pids`) | diff --git a/man/src/container/stats.md b/man/src/container/stats.md index f824417ff23f..d243061284ca 100644 --- a/man/src/container/stats.md +++ b/man/src/container/stats.md @@ -1,5 +1,10 @@ Display a live stream of one or more containers' resource usage statistics +# SORT + +Use `--sort` to order output by `cpu`, `mem`, `mem%`, `net`, `block`, `pids`, +`name`, or `id`. Resource values sort descending; names and IDs sort ascending. + # Format Format the output using the given Go template. @@ -45,4 +50,4 @@ Running `docker container stats` with customized format on all (Running and Stop Running `docker container stats` in JSON format. $ docker container stats --format json - {"BlockIO":"43.9MB / 0B","CPUPerc":"0.00%","Container":"14c505d03da8","ID":"14c505d03da8","MemPerc":"0.21%","MemUsage":"4.242MiB / 1.944GiB","Name":"registry","NetIO":"4.17kB / 0B","PIDs":"13"} \ No newline at end of file + {"BlockIO":"43.9MB / 0B","CPUPerc":"0.00%","Container":"14c505d03da8","ID":"14c505d03da8","MemPerc":"0.21%","MemUsage":"4.242MiB / 1.944GiB","Name":"registry","NetIO":"4.17kB / 0B","PIDs":"13"}