github.com/containers/libpod@v1.9.4-0.20220419124438-4284fd425507/cmd/podman/stats.go (about)

     1  package main
     2  
     3  import (
     4  	"fmt"
     5  	"reflect"
     6  	"strings"
     7  	"time"
     8  
     9  	tm "github.com/buger/goterm"
    10  	"github.com/containers/buildah/pkg/formats"
    11  	"github.com/containers/libpod/cmd/podman/cliconfig"
    12  	"github.com/containers/libpod/cmd/podman/libpodruntime"
    13  	"github.com/containers/libpod/libpod"
    14  	"github.com/containers/libpod/libpod/define"
    15  	"github.com/containers/libpod/pkg/cgroups"
    16  	"github.com/containers/libpod/pkg/rootless"
    17  	"github.com/docker/go-units"
    18  	"github.com/pkg/errors"
    19  	"github.com/spf13/cobra"
    20  )
    21  
    22  type statsOutputParams struct {
    23  	ID       string `json:"id"`
    24  	Name     string `json:"name"`
    25  	CPUPerc  string `json:"cpu_percent"`
    26  	MemUsage string `json:"mem_usage"`
    27  	MemPerc  string `json:"mem_percent"`
    28  	NetIO    string `json:"netio"`
    29  	BlockIO  string `json:"blocki"`
    30  	PIDS     string `json:"pids"`
    31  }
    32  
    33  var (
    34  	statsCommand cliconfig.StatsValues
    35  
    36  	statsDescription = "Display percentage of CPU, memory, network I/O, block I/O and PIDs for one or more containers."
    37  	_statsCommand    = &cobra.Command{
    38  		Use:   "stats [flags] [CONTAINER...]",
    39  		Short: "Display a live stream of container resource usage statistics",
    40  		Long:  statsDescription,
    41  		RunE: func(cmd *cobra.Command, args []string) error {
    42  			statsCommand.InputArgs = args
    43  			statsCommand.GlobalFlags = MainGlobalOpts
    44  			statsCommand.Remote = remoteclient
    45  			return statsCmd(&statsCommand)
    46  		},
    47  		Example: `podman stats --all --no-stream
    48    podman stats ctrID
    49    podman stats --no-stream --format "table {{.ID}} {{.Name}} {{.MemUsage}}" ctrID`,
    50  	}
    51  )
    52  
    53  func init() {
    54  	statsCommand.Command = _statsCommand
    55  	statsCommand.SetHelpTemplate(HelpTemplate())
    56  	statsCommand.SetUsageTemplate(UsageTemplate())
    57  	flags := statsCommand.Flags()
    58  	flags.BoolVarP(&statsCommand.All, "all", "a", false, "Show all containers. Only running containers are shown by default. The default is false")
    59  	flags.StringVar(&statsCommand.Format, "format", "", "Pretty-print container statistics to JSON or using a Go template")
    60  	flags.BoolVarP(&statsCommand.Latest, "latest", "l", false, "Act on the latest container podman is aware of")
    61  	flags.BoolVar(&statsCommand.NoReset, "no-reset", false, "Disable resetting the screen between intervals")
    62  	flags.BoolVar(&statsCommand.NoStream, "no-stream", false, "Disable streaming stats and only pull the first result, default setting is false")
    63  	markFlagHiddenForRemoteClient("latest", flags)
    64  }
    65  
    66  func statsCmd(c *cliconfig.StatsValues) error {
    67  	if rootless.IsRootless() {
    68  		unified, err := cgroups.IsCgroup2UnifiedMode()
    69  		if err != nil {
    70  			return err
    71  		}
    72  		if !unified {
    73  			return errors.New("stats is not supported in rootless mode without cgroups v2")
    74  		}
    75  	}
    76  
    77  	all := c.All
    78  	latest := c.Latest
    79  	ctr := 0
    80  	if all {
    81  		ctr += 1
    82  	}
    83  	if latest {
    84  		ctr += 1
    85  	}
    86  	if len(c.InputArgs) > 0 {
    87  		ctr += 1
    88  	}
    89  
    90  	if ctr > 1 {
    91  		return errors.Errorf("--all, --latest and containers cannot be used together")
    92  	}
    93  
    94  	runtime, err := libpodruntime.GetRuntime(getContext(), &c.PodmanCommand)
    95  	if err != nil {
    96  		return errors.Wrapf(err, "could not get runtime")
    97  	}
    98  	defer runtime.DeferredShutdown(false)
    99  
   100  	times := -1
   101  	if c.NoStream {
   102  		times = 1
   103  	}
   104  
   105  	var ctrs []*libpod.Container
   106  
   107  	containerFunc := runtime.GetRunningContainers
   108  	switch {
   109  	case len(c.InputArgs) > 0:
   110  		containerFunc = func() ([]*libpod.Container, error) { return runtime.GetContainersByList(c.InputArgs) }
   111  	case latest:
   112  		containerFunc = func() ([]*libpod.Container, error) {
   113  			lastCtr, err := runtime.GetLatestContainer()
   114  			if err != nil {
   115  				return nil, err
   116  			}
   117  			return []*libpod.Container{lastCtr}, nil
   118  		}
   119  	case all:
   120  		containerFunc = runtime.GetAllContainers
   121  	}
   122  
   123  	ctrs, err = containerFunc()
   124  	if err != nil {
   125  		return errors.Wrapf(err, "unable to get list of containers")
   126  	}
   127  
   128  	containerStats := map[string]*libpod.ContainerStats{}
   129  	for _, ctr := range ctrs {
   130  		initialStats, err := ctr.GetContainerStats(&libpod.ContainerStats{})
   131  		if err != nil {
   132  			// when doing "all", don't worry about containers that are not running
   133  			cause := errors.Cause(err)
   134  			if c.All && (cause == define.ErrCtrRemoved || cause == define.ErrNoSuchCtr || cause == define.ErrCtrStateInvalid) {
   135  				continue
   136  			}
   137  			if cause == cgroups.ErrCgroupV1Rootless {
   138  				err = cause
   139  			}
   140  			return err
   141  		}
   142  		containerStats[ctr.ID()] = initialStats
   143  	}
   144  
   145  	format := genStatsFormat(c.Format)
   146  
   147  	step := 1
   148  	if times == -1 {
   149  		times = 1
   150  		step = 0
   151  	}
   152  	for i := 0; i < times; i += step {
   153  		reportStats := []*libpod.ContainerStats{}
   154  		for _, ctr := range ctrs {
   155  			id := ctr.ID()
   156  			if _, ok := containerStats[ctr.ID()]; !ok {
   157  				initialStats, err := ctr.GetContainerStats(&libpod.ContainerStats{})
   158  				if errors.Cause(err) == define.ErrCtrRemoved || errors.Cause(err) == define.ErrNoSuchCtr || errors.Cause(err) == define.ErrCtrStateInvalid {
   159  					// skip dealing with a container that is gone
   160  					continue
   161  				}
   162  				if err != nil {
   163  					return err
   164  				}
   165  				containerStats[id] = initialStats
   166  			}
   167  			stats, err := ctr.GetContainerStats(containerStats[id])
   168  			if err != nil && errors.Cause(err) != define.ErrNoSuchCtr {
   169  				return err
   170  			}
   171  			// replace the previous measurement with the current one
   172  			containerStats[id] = stats
   173  			reportStats = append(reportStats, stats)
   174  		}
   175  		ctrs, err = containerFunc()
   176  		if err != nil {
   177  			return err
   178  		}
   179  		if strings.ToLower(format) != formats.JSONString && !c.NoReset {
   180  			tm.Clear()
   181  			tm.MoveCursor(1, 1)
   182  			tm.Flush()
   183  		}
   184  		if err := outputStats(reportStats, format); err != nil {
   185  			return err
   186  		}
   187  		time.Sleep(time.Second)
   188  	}
   189  	return nil
   190  }
   191  
   192  func outputStats(stats []*libpod.ContainerStats, format string) error {
   193  	var out formats.Writer
   194  	var outputStats []statsOutputParams
   195  	for _, s := range stats {
   196  		outputStats = append(outputStats, getStatsOutputParams(s))
   197  	}
   198  	if strings.ToLower(format) == formats.JSONString {
   199  		out = formats.JSONStructArray{Output: statsToGeneric(outputStats, []statsOutputParams{})}
   200  	} else {
   201  		var mapOfHeaders map[string]string
   202  		if len(outputStats) == 0 {
   203  			params := getStatsOutputParamsEmpty()
   204  			mapOfHeaders = params.headerMap()
   205  		} else {
   206  			mapOfHeaders = outputStats[0].headerMap()
   207  		}
   208  		out = formats.StdoutTemplateArray{Output: statsToGeneric(outputStats, []statsOutputParams{}), Template: format, Fields: mapOfHeaders}
   209  	}
   210  	return out.Out()
   211  }
   212  
   213  func genStatsFormat(format string) string {
   214  	if format != "" {
   215  		// "\t" from the command line is not being recognized as a tab
   216  		// replacing the string "\t" to a tab character if the user passes in "\t"
   217  		return strings.Replace(format, `\t`, "\t", -1)
   218  	}
   219  	return "table {{.ID}}\t{{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}\t{{.NetIO}}\t{{.BlockIO}}\t{{.PIDS}}"
   220  }
   221  
   222  // imagesToGeneric creates an empty array of interfaces for output
   223  func statsToGeneric(templParams []statsOutputParams, jsonParams []statsOutputParams) (genericParams []interface{}) {
   224  	if len(templParams) > 0 {
   225  		for _, v := range templParams {
   226  			genericParams = append(genericParams, interface{}(v))
   227  		}
   228  		return
   229  	}
   230  	for _, v := range jsonParams {
   231  		genericParams = append(genericParams, interface{}(v))
   232  	}
   233  	return
   234  }
   235  
   236  // generate the header based on the template provided
   237  func (i *statsOutputParams) headerMap() map[string]string {
   238  	v := reflect.Indirect(reflect.ValueOf(i))
   239  	values := make(map[string]string)
   240  
   241  	for i := 0; i < v.NumField(); i++ {
   242  		key := v.Type().Field(i).Name
   243  		value := key
   244  		switch value {
   245  		case "CPUPerc":
   246  			value = "CPU%"
   247  		case "MemUsage":
   248  			value = "MemUsage/Limit"
   249  		case "MemPerc":
   250  			value = "Mem%"
   251  		}
   252  		values[key] = strings.ToUpper(splitCamelCase(value))
   253  	}
   254  	return values
   255  }
   256  
   257  func combineHumanValues(a, b uint64) string {
   258  	if a == 0 && b == 0 {
   259  		return "-- / --"
   260  	}
   261  	return fmt.Sprintf("%s / %s", units.HumanSize(float64(a)), units.HumanSize(float64(b)))
   262  }
   263  
   264  func floatToPercentString(f float64) string {
   265  	strippedFloat, err := libpod.RemoveScientificNotationFromFloat(f)
   266  	if err != nil || strippedFloat == 0 {
   267  		// If things go bazinga, return a safe value
   268  		return "--"
   269  	}
   270  	return fmt.Sprintf("%.2f", strippedFloat) + "%"
   271  }
   272  
   273  func pidsToString(pid uint64) string {
   274  	if pid == 0 {
   275  		// If things go bazinga, return a safe value
   276  		return "--"
   277  	}
   278  	return fmt.Sprintf("%d", pid)
   279  }
   280  
   281  func getStatsOutputParams(stats *libpod.ContainerStats) statsOutputParams {
   282  	return statsOutputParams{
   283  		Name:     stats.Name,
   284  		ID:       shortID(stats.ContainerID),
   285  		CPUPerc:  floatToPercentString(stats.CPU),
   286  		MemUsage: combineHumanValues(stats.MemUsage, stats.MemLimit),
   287  		MemPerc:  floatToPercentString(stats.MemPerc),
   288  		NetIO:    combineHumanValues(stats.NetInput, stats.NetOutput),
   289  		BlockIO:  combineHumanValues(stats.BlockInput, stats.BlockOutput),
   290  		PIDS:     pidsToString(stats.PIDs),
   291  	}
   292  }
   293  
   294  func getStatsOutputParamsEmpty() statsOutputParams {
   295  	return statsOutputParams{
   296  		Name:     "",
   297  		ID:       "",
   298  		CPUPerc:  "",
   299  		MemUsage: "",
   300  		MemPerc:  "",
   301  		NetIO:    "",
   302  		BlockIO:  "",
   303  		PIDS:     "",
   304  	}
   305  }