github.1git.de/docker/cli@v26.1.3+incompatible/cli/command/container/stats.go (about) 1 package container 2 3 import ( 4 "context" 5 "fmt" 6 "io" 7 "strings" 8 "sync" 9 "time" 10 11 "github.com/docker/cli/cli" 12 "github.com/docker/cli/cli/command" 13 "github.com/docker/cli/cli/command/completion" 14 "github.com/docker/cli/cli/command/formatter" 15 flagsHelper "github.com/docker/cli/cli/flags" 16 "github.com/docker/docker/api/types" 17 "github.com/docker/docker/api/types/container" 18 "github.com/docker/docker/api/types/events" 19 "github.com/docker/docker/api/types/filters" 20 "github.com/pkg/errors" 21 "github.com/sirupsen/logrus" 22 "github.com/spf13/cobra" 23 ) 24 25 // StatsOptions defines options for [RunStats]. 26 type StatsOptions struct { 27 // All allows including both running and stopped containers. The default 28 // is to only include running containers. 29 All bool 30 31 // NoStream disables streaming stats. If enabled, stats are collected once, 32 // and the result is printed. 33 NoStream bool 34 35 // NoTrunc disables truncating the output. The default is to truncate 36 // output such as container-IDs. 37 NoTrunc bool 38 39 // Format is a custom template to use for presenting the stats. 40 // Refer to [flagsHelper.FormatHelp] for accepted formats. 41 Format string 42 43 // Containers is the list of container names or IDs to include in the stats. 44 // If empty, all containers are included. It is mutually exclusive with the 45 // Filters option, and an error is produced if both are set. 46 Containers []string 47 48 // Filters provides optional filters to filter the list of containers and their 49 // associated container-events to include in the stats if no list of containers 50 // is set. If no filter is provided, all containers are included. Filters and 51 // Containers are currently mutually exclusive, and setting both options 52 // produces an error. 53 // 54 // These filters are used both to collect the initial list of containers and 55 // to refresh the list of containers based on container-events, accepted 56 // filters are limited to the intersection of filters accepted by "events" 57 // and "container list". 58 // 59 // Currently only "label" / "label=value" filters are accepted. Additional 60 // filter options may be added in future (within the constraints described 61 // above), but may require daemon-side validation as the list of accepted 62 // filters can differ between daemon- and API versions. 63 Filters *filters.Args 64 } 65 66 // NewStatsCommand creates a new [cobra.Command] for "docker stats". 67 func NewStatsCommand(dockerCLI command.Cli) *cobra.Command { 68 options := StatsOptions{} 69 70 cmd := &cobra.Command{ 71 Use: "stats [OPTIONS] [CONTAINER...]", 72 Short: "Display a live stream of container(s) resource usage statistics", 73 Args: cli.RequiresMinArgs(0), 74 RunE: func(cmd *cobra.Command, args []string) error { 75 options.Containers = args 76 return RunStats(cmd.Context(), dockerCLI, &options) 77 }, 78 Annotations: map[string]string{ 79 "aliases": "docker container stats, docker stats", 80 }, 81 ValidArgsFunction: completion.ContainerNames(dockerCLI, false), 82 } 83 84 flags := cmd.Flags() 85 flags.BoolVarP(&options.All, "all", "a", false, "Show all containers (default shows just running)") 86 flags.BoolVar(&options.NoStream, "no-stream", false, "Disable streaming stats and only pull the first result") 87 flags.BoolVar(&options.NoTrunc, "no-trunc", false, "Do not truncate output") 88 flags.StringVar(&options.Format, "format", "", flagsHelper.FormatHelp) 89 return cmd 90 } 91 92 // acceptedStatsFilters is the list of filters accepted by [RunStats] (through 93 // the [StatsOptions.Filters] option). 94 // 95 // TODO(thaJeztah): don't hard-code the list of accept filters, and expand 96 // to the intersection of filters accepted by both "container list" and 97 // "system events". Validating filters may require an initial API call 98 // to both endpoints ("container list" and "system events"). 99 var acceptedStatsFilters = map[string]bool{ 100 "label": true, 101 } 102 103 // RunStats displays a live stream of resource usage statistics for one or more containers. 104 // This shows real-time information on CPU usage, memory usage, and network I/O. 105 // 106 //nolint:gocyclo 107 func RunStats(ctx context.Context, dockerCLI command.Cli, options *StatsOptions) error { 108 apiClient := dockerCLI.Client() 109 110 // waitFirst is a WaitGroup to wait first stat data's reach for each container 111 waitFirst := &sync.WaitGroup{} 112 // closeChan is a non-buffered channel used to collect errors from goroutines. 113 closeChan := make(chan error) 114 cStats := stats{} 115 116 showAll := len(options.Containers) == 0 117 if showAll { 118 // If no names were specified, start a long-running goroutine which 119 // monitors container events. We make sure we're subscribed before 120 // retrieving the list of running containers to avoid a race where we 121 // would "miss" a creation. 122 started := make(chan struct{}) 123 124 if options.Filters == nil { 125 f := filters.NewArgs() 126 options.Filters = &f 127 } 128 129 if err := options.Filters.Validate(acceptedStatsFilters); err != nil { 130 return err 131 } 132 133 eh := newEventHandler() 134 if options.All { 135 eh.setHandler(events.ActionCreate, func(e events.Message) { 136 s := NewStats(e.Actor.ID[:12]) 137 if cStats.add(s) { 138 waitFirst.Add(1) 139 go collect(ctx, s, apiClient, !options.NoStream, waitFirst) 140 } 141 }) 142 } 143 144 eh.setHandler(events.ActionStart, func(e events.Message) { 145 s := NewStats(e.Actor.ID[:12]) 146 if cStats.add(s) { 147 waitFirst.Add(1) 148 go collect(ctx, s, apiClient, !options.NoStream, waitFirst) 149 } 150 }) 151 152 if !options.All { 153 eh.setHandler(events.ActionDie, func(e events.Message) { 154 cStats.remove(e.Actor.ID[:12]) 155 }) 156 } 157 158 // monitorContainerEvents watches for container creation and removal (only 159 // used when calling `docker stats` without arguments). 160 monitorContainerEvents := func(started chan<- struct{}, c chan events.Message, stopped <-chan struct{}) { 161 // Create a copy of the custom filters so that we don't mutate 162 // the original set of filters. Custom filters are used both 163 // to list containers and to filter events, but the "type" filter 164 // is not valid for filtering containers. 165 f := options.Filters.Clone() 166 f.Add("type", string(events.ContainerEventType)) 167 eventChan, errChan := apiClient.Events(ctx, types.EventsOptions{ 168 Filters: f, 169 }) 170 171 // Whether we successfully subscribed to eventChan or not, we can now 172 // unblock the main goroutine. 173 close(started) 174 defer close(c) 175 176 for { 177 select { 178 case <-stopped: 179 return 180 case event := <-eventChan: 181 c <- event 182 case err := <-errChan: 183 closeChan <- err 184 return 185 } 186 } 187 } 188 189 eventChan := make(chan events.Message) 190 go eh.watch(eventChan) 191 stopped := make(chan struct{}) 192 go monitorContainerEvents(started, eventChan, stopped) 193 defer close(stopped) 194 <-started 195 196 // Fetch the initial list of containers and collect stats for them. 197 // After the initial list was collected, we start listening for events 198 // to refresh the list of containers. 199 cs, err := apiClient.ContainerList(ctx, container.ListOptions{ 200 All: options.All, 201 Filters: *options.Filters, 202 }) 203 if err != nil { 204 return err 205 } 206 for _, ctr := range cs { 207 s := NewStats(ctr.ID[:12]) 208 if cStats.add(s) { 209 waitFirst.Add(1) 210 go collect(ctx, s, apiClient, !options.NoStream, waitFirst) 211 } 212 } 213 214 // make sure each container get at least one valid stat data 215 waitFirst.Wait() 216 } else { 217 // TODO(thaJeztah): re-implement options.Containers as a filter so that 218 // only a single code-path is needed, and custom filters can be combined 219 // with a list of container names/IDs. 220 221 if options.Filters != nil && options.Filters.Len() > 0 { 222 return fmt.Errorf("filtering is not supported when specifying a list of containers") 223 } 224 225 // Create the list of containers, and start collecting stats for all 226 // containers passed. 227 for _, ctr := range options.Containers { 228 s := NewStats(ctr) 229 if cStats.add(s) { 230 waitFirst.Add(1) 231 go collect(ctx, s, apiClient, !options.NoStream, waitFirst) 232 } 233 } 234 235 // We don't expect any asynchronous errors: closeChan can be closed. 236 close(closeChan) 237 238 // make sure each container get at least one valid stat data 239 waitFirst.Wait() 240 241 var errs []string 242 cStats.mu.RLock() 243 for _, c := range cStats.cs { 244 if err := c.GetError(); err != nil { 245 errs = append(errs, err.Error()) 246 } 247 } 248 cStats.mu.RUnlock() 249 if len(errs) > 0 { 250 return errors.New(strings.Join(errs, "\n")) 251 } 252 } 253 254 format := options.Format 255 if len(format) == 0 { 256 if len(dockerCLI.ConfigFile().StatsFormat) > 0 { 257 format = dockerCLI.ConfigFile().StatsFormat 258 } else { 259 format = formatter.TableFormatKey 260 } 261 } 262 if daemonOSType == "" { 263 // Get the daemonOSType if not set already. The daemonOSType variable 264 // should already be set when collecting stats as part of "collect()", 265 // so we unlikely hit this code in practice. 266 daemonOSType = dockerCLI.ServerInfo().OSType 267 } 268 statsCtx := formatter.Context{ 269 Output: dockerCLI.Out(), 270 Format: NewStatsFormat(format, daemonOSType), 271 } 272 cleanScreen := func() { 273 if !options.NoStream { 274 _, _ = fmt.Fprint(dockerCLI.Out(), "\033[2J") 275 _, _ = fmt.Fprint(dockerCLI.Out(), "\033[H") 276 } 277 } 278 279 var err error 280 ticker := time.NewTicker(500 * time.Millisecond) 281 defer ticker.Stop() 282 for range ticker.C { 283 cleanScreen() 284 var ccStats []StatsEntry 285 cStats.mu.RLock() 286 for _, c := range cStats.cs { 287 ccStats = append(ccStats, c.GetStatistics()) 288 } 289 cStats.mu.RUnlock() 290 if err = statsFormatWrite(statsCtx, ccStats, daemonOSType, !options.NoTrunc); err != nil { 291 break 292 } 293 if len(cStats.cs) == 0 && !showAll { 294 break 295 } 296 if options.NoStream { 297 break 298 } 299 select { 300 case err, ok := <-closeChan: 301 if ok { 302 if err != nil { 303 // Suppress "unexpected EOF" errors in the CLI so that 304 // it shuts down cleanly when the daemon restarts. 305 if errors.Is(err, io.ErrUnexpectedEOF) { 306 return nil 307 } 308 return err 309 } 310 } 311 default: 312 // just skip 313 } 314 } 315 return err 316 } 317 318 // newEventHandler initializes and returns an eventHandler 319 func newEventHandler() *eventHandler { 320 return &eventHandler{handlers: make(map[events.Action]func(events.Message))} 321 } 322 323 // eventHandler allows for registering specific events to setHandler. 324 type eventHandler struct { 325 handlers map[events.Action]func(events.Message) 326 } 327 328 func (eh *eventHandler) setHandler(action events.Action, handler func(events.Message)) { 329 eh.handlers[action] = handler 330 } 331 332 // watch ranges over the passed in event chan and processes the events based on the 333 // handlers created for a given action. 334 // To stop watching, close the event chan. 335 func (eh *eventHandler) watch(c <-chan events.Message) { 336 for e := range c { 337 h, exists := eh.handlers[e.Action] 338 if !exists { 339 continue 340 } 341 logrus.Debugf("event handler: received event: %v", e) 342 go h(e) 343 } 344 }