github.com/containerd/nerdctl/v2@v2.0.0-beta.5.0.20240520001846-b5758f54fa28/pkg/cmd/container/stats.go (about) 1 /* 2 Copyright The containerd Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package container 18 19 import ( 20 "bytes" 21 "context" 22 "errors" 23 "fmt" 24 "strings" 25 "sync" 26 "text/tabwriter" 27 "text/template" 28 "time" 29 30 "github.com/containerd/containerd" 31 eventstypes "github.com/containerd/containerd/api/events" 32 "github.com/containerd/containerd/errdefs" 33 "github.com/containerd/containerd/events" 34 "github.com/containerd/log" 35 "github.com/containerd/nerdctl/v2/pkg/api/types" 36 "github.com/containerd/nerdctl/v2/pkg/clientutil" 37 "github.com/containerd/nerdctl/v2/pkg/containerinspector" 38 "github.com/containerd/nerdctl/v2/pkg/eventutil" 39 "github.com/containerd/nerdctl/v2/pkg/formatter" 40 "github.com/containerd/nerdctl/v2/pkg/idutil/containerwalker" 41 "github.com/containerd/nerdctl/v2/pkg/infoutil" 42 "github.com/containerd/nerdctl/v2/pkg/labels" 43 "github.com/containerd/nerdctl/v2/pkg/rootlessutil" 44 "github.com/containerd/nerdctl/v2/pkg/statsutil" 45 "github.com/containerd/typeurl/v2" 46 ) 47 48 type stats struct { 49 mu sync.Mutex 50 cs []*statsutil.Stats 51 } 52 53 // add is from https://github.com/docker/cli/blob/3fb4fb83dfb5db0c0753a8316f21aea54dab32c5/cli/command/container/stats_helpers.go#L26-L34 54 func (s *stats) add(cs *statsutil.Stats) bool { 55 s.mu.Lock() 56 defer s.mu.Unlock() 57 if _, exists := s.isKnownContainer(cs.Container); !exists { 58 s.cs = append(s.cs, cs) 59 return true 60 } 61 return false 62 } 63 64 // remove is from https://github.com/docker/cli/blob/3fb4fb83dfb5db0c0753a8316f21aea54dab32c5/cli/command/container/stats_helpers.go#L36-L42 65 func (s *stats) remove(id string) { 66 s.mu.Lock() 67 if i, exists := s.isKnownContainer(id); exists { 68 s.cs = append(s.cs[:i], s.cs[i+1:]...) 69 } 70 s.mu.Unlock() 71 } 72 73 // isKnownContainer is from https://github.com/docker/cli/blob/3fb4fb83dfb5db0c0753a8316f21aea54dab32c5/cli/command/container/stats_helpers.go#L44-L51 74 func (s *stats) isKnownContainer(cid string) (int, bool) { 75 for i, c := range s.cs { 76 if c.Container == cid { 77 return i, true 78 } 79 } 80 return -1, false 81 } 82 83 // Stats displays a live stream of container(s) resource usage statistics. 84 func Stats(ctx context.Context, client *containerd.Client, containerIds []string, options types.ContainerStatsOptions) error { 85 // NOTE: rootless container does not rely on cgroupv1. 86 // more details about possible ways to resolve this concern: #223 87 if rootlessutil.IsRootless() && infoutil.CgroupsVersion() == "1" { 88 return errors.New("stats requires cgroup v2 for rootless containers, see https://rootlesscontaine.rs/getting-started/common/cgroup2/") 89 } 90 91 showAll := len(containerIds) == 0 92 closeChan := make(chan error) 93 94 var err error 95 var w = options.Stdout 96 var tmpl *template.Template 97 switch options.Format { 98 case "", "table": 99 w = tabwriter.NewWriter(options.Stdout, 10, 1, 3, ' ', 0) 100 case "raw": 101 return errors.New("unsupported format: \"raw\"") 102 default: 103 tmpl, err = formatter.ParseTemplate(options.Format) 104 if err != nil { 105 return err 106 } 107 } 108 109 // waitFirst is a WaitGroup to wait first stat data's reach for each container 110 waitFirst := &sync.WaitGroup{} 111 cStats := stats{} 112 113 monitorContainerEvents := func(started chan<- struct{}, c chan *events.Envelope) { 114 eventsClient := client.EventService() 115 eventsCh, errCh := eventsClient.Subscribe(ctx) 116 117 // Whether we successfully subscribed to eventsCh or not, we can now 118 // unblock the main goroutine. 119 close(started) 120 121 for { 122 select { 123 case event := <-eventsCh: 124 c <- event 125 case err = <-errCh: 126 closeChan <- err 127 return 128 } 129 } 130 131 } 132 133 // getContainerList get all existing containers (only used when calling `nerdctl stats` without arguments). 134 getContainerList := func() { 135 containers, err := client.Containers(ctx) 136 if err != nil { 137 closeChan <- err 138 } 139 140 for _, c := range containers { 141 cStatus := formatter.ContainerStatus(ctx, c) 142 if !options.All { 143 if !strings.HasPrefix(cStatus, "Up") { 144 continue 145 } 146 } 147 s := statsutil.NewStats(c.ID()) 148 if cStats.add(s) { 149 waitFirst.Add(1) 150 go collect(ctx, options.GOptions, s, waitFirst, c.ID(), !options.NoStream) 151 } 152 } 153 } 154 155 if showAll { 156 started := make(chan struct{}) 157 var ( 158 datacc *eventstypes.ContainerCreate 159 datacd *eventstypes.ContainerDelete 160 ) 161 162 eh := eventutil.InitEventHandler() 163 eh.Handle("/containers/create", func(e events.Envelope) { 164 if e.Event != nil { 165 anydata, err := typeurl.UnmarshalAny(e.Event) 166 if err != nil { 167 // just skip 168 return 169 } 170 switch v := anydata.(type) { 171 case *eventstypes.ContainerCreate: 172 datacc = v 173 default: 174 // just skip 175 return 176 } 177 } 178 s := statsutil.NewStats(datacc.ID) 179 if cStats.add(s) { 180 waitFirst.Add(1) 181 go collect(ctx, options.GOptions, s, waitFirst, datacc.ID, !options.NoStream) 182 } 183 }) 184 185 eh.Handle("/containers/delete", func(e events.Envelope) { 186 if e.Event != nil { 187 anydata, err := typeurl.UnmarshalAny(e.Event) 188 if err != nil { 189 // just skip 190 return 191 } 192 switch v := anydata.(type) { 193 case *eventstypes.ContainerDelete: 194 datacd = v 195 default: 196 // just skip 197 return 198 } 199 } 200 cStats.remove(datacd.ID) 201 }) 202 203 eventChan := make(chan *events.Envelope) 204 205 go eh.Watch(eventChan) 206 go monitorContainerEvents(started, eventChan) 207 208 defer close(eventChan) 209 <-started 210 211 // Start a goroutine to retrieve the initial list of containers stats. 212 getContainerList() 213 214 // make sure each container get at least one valid stat data 215 waitFirst.Wait() 216 217 } else { 218 walker := &containerwalker.ContainerWalker{ 219 Client: client, 220 OnFound: func(ctx context.Context, found containerwalker.Found) error { 221 s := statsutil.NewStats(found.Container.ID()) 222 if cStats.add(s) { 223 waitFirst.Add(1) 224 go collect(ctx, options.GOptions, s, waitFirst, found.Container.ID(), !options.NoStream) 225 } 226 return nil 227 }, 228 } 229 230 if err := walker.WalkAll(ctx, containerIds, false); err != nil { 231 return err 232 } 233 234 // make sure each container get at least one valid stat data 235 waitFirst.Wait() 236 237 } 238 239 cleanScreen := func() { 240 if !options.NoStream { 241 fmt.Fprint(options.Stdout, "\033[2J") 242 fmt.Fprint(options.Stdout, "\033[H") 243 } 244 } 245 246 ticker := time.NewTicker(500 * time.Millisecond) 247 defer ticker.Stop() 248 249 // firstTick is for creating distant CPU readings. 250 // firstTick stats are not displayed. 251 var firstTick = true 252 for range ticker.C { 253 cleanScreen() 254 ccstats := []statsutil.StatsEntry{} 255 cStats.mu.Lock() 256 for _, c := range cStats.cs { 257 if err := c.GetError(); err != nil { 258 fmt.Fprintf(options.Stderr, "unable to get stat entry: %s\n", err) 259 } 260 ccstats = append(ccstats, c.GetStatistics()) 261 } 262 cStats.mu.Unlock() 263 264 if !firstTick { 265 // print header for every tick 266 if options.Format == "" || options.Format == "table" { 267 fmt.Fprintln(w, "CONTAINER ID\tNAME\tCPU %\tMEM USAGE / LIMIT\tMEM %\tNET I/O\tBLOCK I/O\tPIDS") 268 } 269 } 270 271 for _, c := range ccstats { 272 if c.ID == "" { 273 continue 274 } 275 rc := statsutil.RenderEntry(&c, options.NoTrunc) 276 if !firstTick { 277 if tmpl != nil { 278 var b bytes.Buffer 279 if err := tmpl.Execute(&b, rc); err != nil { 280 break 281 } 282 if _, err = fmt.Fprintln(options.Stdout, b.String()); err != nil { 283 break 284 } 285 } else { 286 if _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n", 287 rc.ID, 288 rc.Name, 289 rc.CPUPerc, 290 rc.MemUsage, 291 rc.MemPerc, 292 rc.NetIO, 293 rc.BlockIO, 294 rc.PIDs, 295 ); err != nil { 296 break 297 } 298 } 299 } 300 } 301 if f, ok := w.(formatter.Flusher); ok { 302 f.Flush() 303 } 304 305 if len(cStats.cs) == 0 && !showAll { 306 break 307 } 308 if options.NoStream && !firstTick { 309 break 310 } 311 select { 312 case err, ok := <-closeChan: 313 if ok { 314 if err != nil { 315 return err 316 } 317 } 318 default: 319 // just skip 320 } 321 firstTick = false 322 } 323 324 return err 325 } 326 327 func collect(ctx context.Context, globalOptions types.GlobalCommandOptions, s *statsutil.Stats, waitFirst *sync.WaitGroup, id string, noStream bool) { 328 log.G(ctx).Debugf("collecting stats for %s", s.Container) 329 var ( 330 getFirst = true 331 u = make(chan error, 1) 332 ) 333 334 defer func() { 335 // if error happens and we get nothing of stats, release wait group whatever 336 if getFirst { 337 getFirst = false 338 waitFirst.Done() 339 } 340 }() 341 client, ctx, cancel, err := clientutil.NewClient(ctx, globalOptions.Namespace, globalOptions.Address) 342 if err != nil { 343 s.SetError(err) 344 return 345 } 346 defer func() { 347 cancel() 348 client.Close() 349 }() 350 container, err := client.LoadContainer(ctx, id) 351 if err != nil { 352 s.SetError(err) 353 return 354 } 355 356 go func() { 357 previousStats := new(statsutil.ContainerStats) 358 firstSet := true 359 for { 360 //task is in the for loop to avoid nil task just after Container creation 361 task, err := container.Task(ctx, nil) 362 if err != nil { 363 u <- err 364 continue 365 } 366 367 //labels is in the for loop to avoid nil labels just after Container creation 368 clabels, err := container.Labels(ctx) 369 if err != nil { 370 u <- err 371 continue 372 } 373 374 metric, err := task.Metrics(ctx) 375 if err != nil { 376 u <- err 377 continue 378 } 379 anydata, err := typeurl.UnmarshalAny(metric.Data) 380 if err != nil { 381 u <- err 382 continue 383 } 384 385 netNS, err := containerinspector.InspectNetNS(ctx, int(task.Pid())) 386 if err != nil { 387 u <- err 388 continue 389 } 390 391 // when (firstSet == true), we only set container stats without rendering stat entry 392 statsEntry, err := setContainerStatsAndRenderStatsEntry(previousStats, firstSet, anydata, int(task.Pid()), netNS.Interfaces) 393 if err != nil { 394 u <- err 395 continue 396 } 397 statsEntry.Name = clabels[labels.Name] 398 statsEntry.ID = container.ID() 399 400 if firstSet { 401 firstSet = false 402 } else { 403 s.SetStatistics(statsEntry) 404 } 405 u <- nil 406 //sleep to create distant CPU readings 407 time.Sleep(500 * time.Millisecond) 408 } 409 }() 410 for { 411 select { 412 case <-time.After(6 * time.Second): 413 // zero out the values if we have not received an update within 414 // the specified duration. 415 s.SetErrorAndReset(errors.New("timeout waiting for stats")) 416 // if this is the first stat you get, release WaitGroup 417 if getFirst { 418 getFirst = false 419 waitFirst.Done() 420 } 421 case err := <-u: 422 if err != nil { 423 if !errdefs.IsNotFound(err) { 424 s.SetError(err) 425 continue 426 } 427 } 428 // if this is the first stat you get, release WaitGroup 429 if getFirst { 430 getFirst = false 431 waitFirst.Done() 432 } 433 } 434 } 435 }