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