podman/cmd/kpod/stats.go

227 lines
5.9 KiB
Go

package main
import (
"fmt"
"reflect"
"strings"
"time"
tm "github.com/buger/goterm"
"github.com/docker/go-units"
"github.com/pkg/errors"
"github.com/projectatomic/libpod/cmd/kpod/formats"
"github.com/projectatomic/libpod/libpod"
"github.com/urfave/cli"
)
type statsOutputParams struct {
Container string `json:"name"`
ID string `json:"id"`
CPUPerc string `json:"cpu_percent"`
MemUsage string `json:"mem_usage"`
MemPerc string `json:"mem_percent"`
NetIO string `json:"netio"`
BlockIO string `json:"blocki"`
PIDS uint64 `json:"pids"`
}
var (
statsFlags = []cli.Flag{
cli.BoolFlag{
Name: "all, a",
Usage: "show all containers. Only running containers are shown by default. The default is false",
},
cli.BoolFlag{
Name: "no-stream",
Usage: "disable streaming stats and only pull the first result, default setting is false",
},
cli.StringFlag{
Name: "format",
Usage: "pretty-print container statistics using a Go template",
},
cli.BoolFlag{
Name: "no-reset",
Usage: "disable resetting the screen between intervals",
},
}
statsDescription = "display a live stream of one or more containers' resource usage statistics"
statsCommand = cli.Command{
Name: "stats",
Usage: "Display percentage of CPU, memory, network I/O, block I/O and PIDs for one or more containers",
Description: statsDescription,
Flags: statsFlags,
Action: statsCmd,
ArgsUsage: "",
}
)
func statsCmd(c *cli.Context) error {
if err := validateFlags(c, statsFlags); err != nil {
return err
}
runtime, err := getRuntime(c)
if err != nil {
return errors.Wrapf(err, "could not get runtime")
}
defer runtime.Shutdown(false)
times := -1
if c.Bool("no-stream") {
times = 1
}
var format string
var ctrs []*libpod.Container
var containerFunc func() ([]*libpod.Container, error)
all := c.Bool("all")
if c.IsSet("format") {
format = c.String("format")
} else {
format = genStatsFormat()
}
if len(c.Args()) > 0 {
containerFunc = func() ([]*libpod.Container, error) { return runtime.GetContainersByList(c.Args()) }
} else if all {
containerFunc = runtime.GetAllContainers
} else {
containerFunc = runtime.GetRunningContainers
}
ctrs, err = containerFunc()
if err != nil {
return errors.Wrapf(err, "unable to get list of containers")
}
containerStats := map[string]*libpod.ContainerStats{}
for _, ctr := range ctrs {
initialStats, err := ctr.GetContainerStats(&libpod.ContainerStats{})
if err != nil {
return err
}
containerStats[ctr.ID()] = initialStats
}
step := 1
if times == -1 {
times = 1
step = 0
}
for i := 0; i < times; i += step {
reportStats := []*libpod.ContainerStats{}
for _, ctr := range ctrs {
id := ctr.ID()
if _, ok := containerStats[ctr.ID()]; !ok {
initialStats, err := ctr.GetContainerStats(&libpod.ContainerStats{})
if err != nil {
return err
}
containerStats[id] = initialStats
}
stats, err := ctr.GetContainerStats(containerStats[id])
if err != nil {
return err
}
// replace the previous measurement with the current one
containerStats[id] = stats
reportStats = append(reportStats, stats)
}
ctrs, err = containerFunc()
if err != nil {
return err
}
if strings.ToLower(format) != formats.JSONString && !c.Bool("no-reset") {
tm.Clear()
tm.MoveCursor(1, 1)
tm.Flush()
}
outputStats(reportStats, format)
time.Sleep(time.Second)
}
return nil
}
func outputStats(stats []*libpod.ContainerStats, format string) error {
var out formats.Writer
var outputStats []statsOutputParams
for _, s := range stats {
outputStats = append(outputStats, getStatsOutputParams(s))
}
if len(outputStats) == 0 {
return nil
}
if strings.ToLower(format) == formats.JSONString {
out = formats.JSONStructArray{Output: statsToGeneric(outputStats, []statsOutputParams{})}
} else {
out = formats.StdoutTemplateArray{Output: statsToGeneric(outputStats, []statsOutputParams{}), Template: format, Fields: outputStats[0].headerMap()}
}
return formats.Writer(out).Out()
}
func genStatsFormat() (format string) {
return "table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}\t{{.NetIO}}\t{{.BlockIO}}\t{{.PIDS}}"
}
// imagesToGeneric creates an empty array of interfaces for output
func statsToGeneric(templParams []statsOutputParams, JSONParams []statsOutputParams) (genericParams []interface{}) {
if len(templParams) > 0 {
for _, v := range templParams {
genericParams = append(genericParams, interface{}(v))
}
return
}
for _, v := range JSONParams {
genericParams = append(genericParams, interface{}(v))
}
return
}
// generate the header based on the template provided
func (i *statsOutputParams) headerMap() map[string]string {
v := reflect.Indirect(reflect.ValueOf(i))
values := make(map[string]string)
for i := 0; i < v.NumField(); i++ {
key := v.Type().Field(i).Name
value := key
switch value {
case "CPUPerc":
value = "CPU%"
case "MemUsage":
value = "MemUsage/Limit"
case "MemPerc":
value = "Mem%"
}
values[key] = strings.ToUpper(splitCamelCase(value))
}
return values
}
func combineHumanValues(a, b uint64) string {
return fmt.Sprintf("%s / %s", units.HumanSize(float64(a)), units.HumanSize(float64(b)))
}
func floatToPercentString(f float64) string {
strippedFloat, err := libpod.RemoveScientificNotationFromFloat(f)
if err != nil {
// If things go bazinga, return a safe value
return "0.00 %"
}
return fmt.Sprintf("%.2f", strippedFloat) + "%"
}
func getStatsOutputParams(stats *libpod.ContainerStats) statsOutputParams {
return statsOutputParams{
Container: stats.ContainerID[:12],
ID: stats.ContainerID,
CPUPerc: floatToPercentString(stats.CPU),
MemUsage: combineHumanValues(stats.MemUsage, stats.MemLimit),
MemPerc: floatToPercentString(stats.MemPerc),
NetIO: combineHumanValues(stats.NetInput, stats.NetOutput),
BlockIO: combineHumanValues(stats.BlockInput, stats.BlockOutput),
PIDS: stats.PIDs,
}
}