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  }