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 }