Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions cli/command/container/stats.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}

Expand All @@ -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.
Expand Down Expand Up @@ -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
}
Expand All @@ -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
Expand Down
35 changes: 35 additions & 0 deletions cli/command/container/stats_helpers.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package container

import (
"cmp"
"context"
"encoding/json"
"errors"
"io"
"slices"
"strings"
"sync"
"time"

Expand Down Expand Up @@ -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))
})
}
36 changes: 36 additions & 0 deletions cli/command/container/stats_helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
}
}
6 changes: 5 additions & 1 deletion contrib/completion/bash/docker
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions contrib/completion/fish/docker.fish
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions contrib/completion/zsh/_docker
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
17 changes: 17 additions & 0 deletions docs/reference/commandline/container_stats.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Display a live stream of container(s) resource usage statistics
| [`--format`](#format) | `string` | | Format output using a custom template:<br>'table': Print output in table format with column headers (default)<br>'table TEMPLATE': Print output in table format using the given Go template<br>'json': Print in JSON format<br>'TEMPLATE': Print output using the given Go template.<br>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`) |


<!---MARKER_GEN_END-->
Expand Down Expand Up @@ -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
```

### <a name="sort"></a> 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
```

### <a name="format"></a> Format the output (--format)

The formatting option (`--format`) pretty prints container output
Expand Down
1 change: 1 addition & 0 deletions docs/reference/commandline/stats.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Display a live stream of container(s) resource usage statistics
| `--format` | `string` | | Format output using a custom template:<br>'table': Print output in table format with column headers (default)<br>'table TEMPLATE': Print output in table format using the given Go template<br>'json': Print in JSON format<br>'TEMPLATE': Print output using the given Go template.<br>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`) |


<!---MARKER_GEN_END-->
Expand Down
7 changes: 6 additions & 1 deletion man/src/container/stats.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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"}
{"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"}