Merge pull request #6080 from baude/v2stats

v2 podman stats
This commit is contained in:
OpenShift Merge Robot
2020-05-05 17:29:56 +02:00
committed by GitHub
20 changed files with 396 additions and 63 deletions

View File

@ -206,7 +206,7 @@ func ps(cmd *cobra.Command, args []string) error {
return err
}
if err := tmpl.Execute(w, responses); err != nil {
return nil
return err
}
if err := w.Flush(); err != nil {
return err

View File

@ -0,0 +1,244 @@
package containers
import (
"fmt"
"os"
"strings"
"text/tabwriter"
"text/template"
tm "github.com/buger/goterm"
"github.com/containers/libpod/cmd/podman/registry"
"github.com/containers/libpod/libpod/define"
"github.com/containers/libpod/pkg/cgroups"
"github.com/containers/libpod/pkg/domain/entities"
"github.com/containers/libpod/pkg/rootless"
"github.com/containers/libpod/utils"
"github.com/docker/go-units"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
var (
statsDescription = "Display percentage of CPU, memory, network I/O, block I/O and PIDs for one or more containers."
statsCommand = &cobra.Command{
Use: "stats [flags] [CONTAINER...]",
Short: "Display a live stream of container resource usage statistics",
Long: statsDescription,
RunE: stats,
Args: checkStatOptions,
Example: `podman stats --all --no-stream
podman stats ctrID
podman stats --no-stream --format "table {{.ID}} {{.Name}} {{.MemUsage}}" ctrID`,
}
containerStatsCommand = &cobra.Command{
Use: statsCommand.Use,
Short: statsCommand.Short,
Long: statsCommand.Long,
RunE: statsCommand.RunE,
Args: checkStatOptions,
Example: `podman container stats --all --no-stream
podman container stats ctrID
podman container stats --no-stream --format "table {{.ID}} {{.Name}} {{.MemUsage}}" ctrID`,
}
)
var (
statsOptions entities.ContainerStatsOptions
defaultStatsRow = "{{.ID}}\t{{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}\t{{.NetIO}}\t{{.BlockIO}}\t{{.PIDS}}\n"
defaultStatsHeader = "ID\tNAME\tCPU %\tMEM USAGE / LIMIT\tMEM %\tNET IO\tBLOCK IO\tPIDS\n"
)
func statFlags(flags *pflag.FlagSet) {
flags.BoolVarP(&statsOptions.All, "all", "a", false, "Show all containers. Only running containers are shown by default. The default is false")
flags.StringVar(&statsOptions.Format, "format", "", "Pretty-print container statistics to JSON or using a Go template")
flags.BoolVarP(&statsOptions.Latest, "latest", "l", false, "Act on the latest container Podman is aware of")
flags.BoolVar(&statsOptions.NoReset, "no-reset", false, "Disable resetting the screen between intervals")
flags.BoolVar(&statsOptions.NoStream, "no-stream", false, "Disable streaming stats and only pull the first result, default setting is false")
if registry.IsRemote() {
_ = flags.MarkHidden("latest")
}
}
func init() {
registry.Commands = append(registry.Commands, registry.CliCommand{
Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode},
Command: statsCommand,
})
flags := statsCommand.Flags()
statFlags(flags)
registry.Commands = append(registry.Commands, registry.CliCommand{
Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode},
Command: containerStatsCommand,
Parent: containerCmd,
})
containerStatsFlags := containerStatsCommand.Flags()
statFlags(containerStatsFlags)
}
// stats is different in that it will assume running containers if
// no input is given, so we need to validate differently
func checkStatOptions(cmd *cobra.Command, args []string) error {
opts := 0
if statsOptions.All {
opts += 1
}
if statsOptions.Latest {
opts += 1
}
if len(args) > 0 {
opts += 1
}
if opts > 1 {
return errors.Errorf("--all, --latest and containers cannot be used together")
}
return nil
}
func stats(cmd *cobra.Command, args []string) error {
if rootless.IsRootless() {
unified, err := cgroups.IsCgroup2UnifiedMode()
if err != nil {
return err
}
if !unified {
return errors.New("stats is not supported in rootless mode without cgroups v2")
}
}
statsOptions.StatChan = make(chan []*define.ContainerStats, 1)
go func() {
for reports := range statsOptions.StatChan {
if err := outputStats(reports); err != nil {
logrus.Error(err)
}
}
}()
return registry.ContainerEngine().ContainerStats(registry.Context(), args, statsOptions)
}
func outputStats(reports []*define.ContainerStats) error {
if len(statsOptions.Format) < 1 && !statsOptions.NoReset {
tm.Clear()
tm.MoveCursor(1, 1)
tm.Flush()
}
var stats []*containerStats
for _, r := range reports {
stats = append(stats, &containerStats{r})
}
if statsOptions.Format == "json" {
return outputJSON(stats)
}
format := defaultStatsRow
if len(statsOptions.Format) > 0 {
format = statsOptions.Format
if !strings.HasSuffix(format, "\n") {
format += "\n"
}
}
format = "{{range . }}" + format + "{{end}}"
if len(statsOptions.Format) < 1 {
format = defaultStatsHeader + format
}
tmpl, err := template.New("stats").Parse(format)
if err != nil {
return err
}
w := tabwriter.NewWriter(os.Stdout, 8, 2, 2, ' ', 0)
if err := tmpl.Execute(w, stats); err != nil {
return err
}
if err := w.Flush(); err != nil {
return err
}
return nil
}
type containerStats struct {
*define.ContainerStats
}
func (s *containerStats) ID() string {
return s.ContainerID[0:12]
}
func (s *containerStats) CPUPerc() string {
return floatToPercentString(s.CPU)
}
func (s *containerStats) MemPerc() string {
return floatToPercentString(s.ContainerStats.MemPerc)
}
func (s *containerStats) NetIO() string {
return combineHumanValues(s.NetInput, s.NetOutput)
}
func (s *containerStats) BlockIO() string {
return combineHumanValues(s.BlockInput, s.BlockOutput)
}
func (s *containerStats) PIDS() string {
if s.PIDs == 0 {
// If things go bazinga, return a safe value
return "--"
}
return fmt.Sprintf("%d", s.PIDs)
}
func (s *containerStats) MemUsage() string {
return combineHumanValues(s.ContainerStats.MemUsage, s.ContainerStats.MemLimit)
}
func floatToPercentString(f float64) string {
strippedFloat, err := utils.RemoveScientificNotationFromFloat(f)
if err != nil || strippedFloat == 0 {
// If things go bazinga, return a safe value
return "--"
}
return fmt.Sprintf("%.2f", strippedFloat) + "%"
}
func combineHumanValues(a, b uint64) string {
if a == 0 && b == 0 {
return "-- / --"
}
return fmt.Sprintf("%s / %s", units.HumanSize(float64(a)), units.HumanSize(float64(b)))
}
func outputJSON(stats []*containerStats) error {
type jstat struct {
Id string `json:"id"`
Name string `json:"name"`
CpuPercent string `json:"cpu_percent"`
MemUsage string `json:"mem_usage"`
MemPerc string `json:"mem_percent"`
NetIO string `json:"net_io"`
BlockIO string `json:"block_io"`
Pids string `json:"pids"`
}
var jstats []jstat
for _, j := range stats {
jstats = append(jstats, jstat{
Id: j.ID(),
Name: j.Name,
CpuPercent: j.CPUPerc(),
MemUsage: j.MemPerc(),
MemPerc: j.MemUsage(),
NetIO: j.NetIO(),
BlockIO: j.BlockIO(),
Pids: j.PIDS(),
})
}
b, err := json.MarshalIndent(jstats, "", " ")
if err != nil {
return err
}
fmt.Println(string(b))
return nil
}

View File

@ -112,3 +112,22 @@ func (s ContainerExecStatus) String() string {
return "bad state"
}
}
// ContainerStats contains the statistics information for a running container
type ContainerStats struct {
ContainerID string
Name string
PerCPU []uint64
CPU float64
CPUNano uint64
CPUSystemNano uint64
SystemNano uint64
MemUsage uint64
MemLimit uint64
MemPerc float64
NetInput uint64
NetOutput uint64
BlockInput uint64
BlockOutput uint64
PIDs uint64
}

View File

@ -247,14 +247,14 @@ func (p *Pod) InfraContainerID() (string, error) {
// PodContainerStats is an organization struct for pods and their containers
type PodContainerStats struct {
Pod *Pod
ContainerStats map[string]*ContainerStats
ContainerStats map[string]*define.ContainerStats
}
// GetPodStats returns the stats for each of its containers
func (p *Pod) GetPodStats(previousContainerStats map[string]*ContainerStats) (map[string]*ContainerStats, error) {
func (p *Pod) GetPodStats(previousContainerStats map[string]*define.ContainerStats) (map[string]*define.ContainerStats, error) {
var (
ok bool
prevStat *ContainerStats
prevStat *define.ContainerStats
)
p.lock.Lock()
defer p.lock.Unlock()
@ -266,10 +266,10 @@ func (p *Pod) GetPodStats(previousContainerStats map[string]*ContainerStats) (ma
if err != nil {
return nil, err
}
newContainerStats := make(map[string]*ContainerStats)
newContainerStats := make(map[string]*define.ContainerStats)
for _, c := range containers {
if prevStat, ok = previousContainerStats[c.ID()]; !ok {
prevStat = &ContainerStats{}
prevStat = &define.ContainerStats{}
}
newStats, err := c.GetContainerStats(prevStat)
// If the container wasn't running, don't include it

View File

@ -13,8 +13,8 @@ import (
)
// GetContainerStats gets the running stats for a given container
func (c *Container) GetContainerStats(previousStats *ContainerStats) (*ContainerStats, error) {
stats := new(ContainerStats)
func (c *Container) GetContainerStats(previousStats *define.ContainerStats) (*define.ContainerStats, error) {
stats := new(define.ContainerStats)
stats.ContainerID = c.ID()
stats.Name = c.Name()

View File

@ -1,20 +0,0 @@
package libpod
// ContainerStats contains the statistics information for a running container
type ContainerStats struct {
ContainerID string
Name string
PerCPU []uint64
CPU float64
CPUNano uint64
CPUSystemNano uint64
SystemNano uint64
MemUsage uint64
MemLimit uint64
MemPerc float64
NetInput uint64
NetOutput uint64
BlockInput uint64
BlockOutput uint64
PIDs uint64
}

View File

@ -5,6 +5,6 @@ package libpod
import "github.com/containers/libpod/libpod/define"
// GetContainerStats gets the running stats for a given container
func (c *Container) GetContainerStats(previousStats *ContainerStats) (*ContainerStats, error) {
func (c *Container) GetContainerStats(previousStats *define.ContainerStats) (*define.ContainerStats, error) {
return nil, define.ErrOSNotSupported
}

View File

@ -9,12 +9,10 @@ import (
"os/exec"
"path/filepath"
"sort"
"strconv"
"strings"
"time"
"github.com/containers/common/pkg/config"
"github.com/containers/libpod/libpod/define"
"github.com/containers/libpod/utils"
"github.com/fsnotify/fsnotify"
@ -36,24 +34,6 @@ func FuncTimer(funcName string) {
fmt.Printf("%s executed in %d ms\n", funcName, elapsed)
}
// RemoveScientificNotationFromFloat returns a float without any
// scientific notation if the number has any.
// golang does not handle conversion of float64s that have scientific
// notation in them and otherwise stinks. please replace this if you have
// a better implementation.
func RemoveScientificNotationFromFloat(x float64) (float64, error) {
bigNum := strconv.FormatFloat(x, 'g', -1, 64)
breakPoint := strings.IndexAny(bigNum, "Ee")
if breakPoint > 0 {
bigNum = bigNum[:breakPoint]
}
result, err := strconv.ParseFloat(bigNum, 64)
if err != nil {
return x, errors.Wrapf(err, "unable to remove scientific number from calculations")
}
return result, nil
}
// MountExists returns true if dest exists in the list of mounts
func MountExists(specMounts []spec.Mount, dest string) bool {
for _, m := range specMounts {

View File

@ -3,6 +3,7 @@ package libpod
import (
"testing"
"github.com/containers/libpod/utils"
"github.com/stretchr/testify/assert"
)
@ -10,7 +11,7 @@ func TestRemoveScientificNotationFromFloat(t *testing.T) {
numbers := []float64{0.0, .5, 1.99999932, 1.04e+10}
results := []float64{0.0, .5, 1.99999932, 1.04}
for i, x := range numbers {
result, err := RemoveScientificNotationFromFloat(x)
result, err := utils.RemoveScientificNotationFromFloat(x)
assert.NoError(t, err)
assert.Equal(t, result, results[i])
}

View File

@ -50,7 +50,7 @@ func StatsContainer(w http.ResponseWriter, r *http.Request) {
return
}
stats, err := ctnr.GetContainerStats(&libpod.ContainerStats{})
stats, err := ctnr.GetContainerStats(&define.ContainerStats{})
if err != nil {
utils.InternalServerError(w, errors.Wrapf(err, "Failed to obtain Container %s stats", name))
return

View File

@ -367,3 +367,14 @@ type ContainerCpOptions struct {
// ContainerCpReport describes the output from a cp operation
type ContainerCpReport struct {
}
// ContainerStatsOptions describes input options for getting
// stats on containers
type ContainerStatsOptions struct {
All bool
Format string
Latest bool
NoReset bool
NoStream bool
StatChan chan []*define.ContainerStats
}

View File

@ -35,6 +35,7 @@ type ContainerEngine interface {
ContainerRm(ctx context.Context, namesOrIds []string, options RmOptions) ([]*RmReport, error)
ContainerRun(ctx context.Context, opts ContainerRunOptions) (*ContainerRunReport, error)
ContainerStart(ctx context.Context, namesOrIds []string, options ContainerStartOptions) ([]*ContainerStartReport, error)
ContainerStats(ctx context.Context, namesOrIds []string, options ContainerStatsOptions) error
ContainerStop(ctx context.Context, namesOrIds []string, options StopOptions) ([]*StopReport, error)
ContainerTop(ctx context.Context, options TopOptions) (*StringSliceReport, error)
ContainerUnmount(ctx context.Context, nameOrIds []string, options ContainerUnmountOptions) ([]*ContainerUnmountReport, error)

View File

@ -8,8 +8,7 @@ import (
"strconv"
"strings"
"sync"
lpfilters "github.com/containers/libpod/libpod/filters"
"time"
"github.com/containers/buildah"
"github.com/containers/common/pkg/config"
@ -17,8 +16,10 @@ import (
"github.com/containers/libpod/libpod"
"github.com/containers/libpod/libpod/define"
"github.com/containers/libpod/libpod/events"
lpfilters "github.com/containers/libpod/libpod/filters"
"github.com/containers/libpod/libpod/image"
"github.com/containers/libpod/libpod/logs"
"github.com/containers/libpod/pkg/cgroups"
"github.com/containers/libpod/pkg/checkpoint"
"github.com/containers/libpod/pkg/domain/entities"
"github.com/containers/libpod/pkg/domain/infra/abi/terminal"
@ -1003,3 +1004,76 @@ func (ic *ContainerEngine) Shutdown(_ context.Context) {
_ = ic.Libpod.Shutdown(false)
})
}
func (ic *ContainerEngine) ContainerStats(ctx context.Context, namesOrIds []string, options entities.ContainerStatsOptions) error {
containerFunc := ic.Libpod.GetRunningContainers
switch {
case len(namesOrIds) > 0:
containerFunc = func() ([]*libpod.Container, error) { return ic.Libpod.GetContainersByList(namesOrIds) }
case options.Latest:
containerFunc = func() ([]*libpod.Container, error) {
lastCtr, err := ic.Libpod.GetLatestContainer()
if err != nil {
return nil, err
}
return []*libpod.Container{lastCtr}, nil
}
case options.All:
containerFunc = ic.Libpod.GetAllContainers
}
ctrs, err := containerFunc()
if err != nil {
return errors.Wrapf(err, "unable to get list of containers")
}
containerStats := map[string]*define.ContainerStats{}
for _, ctr := range ctrs {
initialStats, err := ctr.GetContainerStats(&define.ContainerStats{})
if err != nil {
// when doing "all", don't worry about containers that are not running
cause := errors.Cause(err)
if options.All && (cause == define.ErrCtrRemoved || cause == define.ErrNoSuchCtr || cause == define.ErrCtrStateInvalid) {
continue
}
if cause == cgroups.ErrCgroupV1Rootless {
err = cause
}
return err
}
containerStats[ctr.ID()] = initialStats
}
for {
reportStats := []*define.ContainerStats{}
for _, ctr := range ctrs {
id := ctr.ID()
if _, ok := containerStats[ctr.ID()]; !ok {
initialStats, err := ctr.GetContainerStats(&define.ContainerStats{})
if errors.Cause(err) == define.ErrCtrRemoved || errors.Cause(err) == define.ErrNoSuchCtr || errors.Cause(err) == define.ErrCtrStateInvalid {
// skip dealing with a container that is gone
continue
}
if err != nil {
return err
}
containerStats[id] = initialStats
}
stats, err := ctr.GetContainerStats(containerStats[id])
if err != nil && errors.Cause(err) != define.ErrNoSuchCtr {
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
}
options.StatChan <- reportStats
if options.NoStream {
break
}
time.Sleep(time.Second)
}
return nil
}

View File

@ -8,6 +8,7 @@ import (
"github.com/containers/libpod/pkg/cgroups"
"github.com/containers/libpod/pkg/domain/entities"
"github.com/containers/libpod/pkg/rootless"
"github.com/containers/libpod/utils"
"github.com/docker/go-units"
"github.com/pkg/errors"
)
@ -68,7 +69,7 @@ func combineHumanValues(a, b uint64) string {
}
func floatToPercentString(f float64) string {
strippedFloat, err := libpod.RemoveScientificNotationFromFloat(f)
strippedFloat, err := utils.RemoveScientificNotationFromFloat(f)
if err != nil || strippedFloat == 0 {
// If things go bazinga, return a safe value
return "--"

View File

@ -387,3 +387,7 @@ func (ic *ContainerEngine) ContainerCp(ctx context.Context, source, dest string,
// Shutdown Libpod engine
func (ic *ContainerEngine) Shutdown(_ context.Context) {
}
func (ic *ContainerEngine) ContainerStats(ctx context.Context, namesOrIds []string, options entities.ContainerStatsOptions) error {
return errors.New("not implemented")
}

View File

@ -331,7 +331,7 @@ func (i *VarlinkAPI) GetContainerStats(call iopodman.VarlinkCall, name string) e
if err != nil {
return call.ReplyContainerNotFound(name, err.Error())
}
containerStats, err := ctr.GetContainerStats(&libpod.ContainerStats{})
containerStats, err := ctr.GetContainerStats(&define.ContainerStats{})
if err != nil {
if errors.Cause(err) == define.ErrCtrStateInvalid {
return call.ReplyNoContainerRunning()

View File

@ -8,12 +8,12 @@ import (
"strconv"
"syscall"
"github.com/containers/libpod/libpod"
"github.com/containers/libpod/libpod/define"
iopodman "github.com/containers/libpod/pkg/varlink"
"github.com/cri-o/ocicni/pkg/ocicni"
"github.com/docker/go-connections/nat"
"github.com/pkg/errors"
"github.com/containers/libpod/libpod"
iopodman "github.com/containers/libpod/pkg/varlink"
)
// CreatePod ...
@ -263,7 +263,7 @@ func (i *VarlinkAPI) GetPodStats(call iopodman.VarlinkCall, name string) error {
if err != nil {
return call.ReplyPodNotFound(name, err.Error())
}
prevStats := make(map[string]*libpod.ContainerStats)
prevStats := make(map[string]*define.ContainerStats)
podStats, err := pod.GetPodStats(prevStats)
if err != nil {
return call.ReplyErrorOccurred(err.Error())

View File

@ -3,14 +3,14 @@
package varlinkapi
import (
"github.com/containers/libpod/libpod"
"github.com/containers/libpod/libpod/define"
iopodman "github.com/containers/libpod/pkg/varlink"
)
// ContainerStatsToLibpodContainerStats converts the varlink containerstats to a libpod
// container stats
func ContainerStatsToLibpodContainerStats(stats iopodman.ContainerStats) libpod.ContainerStats {
cstats := libpod.ContainerStats{
func ContainerStatsToLibpodContainerStats(stats iopodman.ContainerStats) define.ContainerStats {
cstats := define.ContainerStats{
ContainerID: stats.Id,
Name: stats.Name,
CPU: stats.Cpu,

View File

@ -21,7 +21,6 @@ var _ = Describe("Podman stats", func() {
)
BeforeEach(func() {
Skip(v2fail)
cgroupsv2, err := cgroups.IsCgroup2UnifiedMode()
Expect(err).To(BeNil())

View File

@ -6,6 +6,7 @@ import (
"io"
"os"
"os/exec"
"strconv"
"strings"
"github.com/containers/storage/pkg/archive"
@ -125,3 +126,21 @@ func Tar(source string) (io.ReadCloser, error) {
logrus.Debugf("creating tarball of %s", source)
return archive.Tar(source, archive.Uncompressed)
}
// RemoveScientificNotationFromFloat returns a float without any
// scientific notation if the number has any.
// golang does not handle conversion of float64s that have scientific
// notation in them and otherwise stinks. please replace this if you have
// a better implementation.
func RemoveScientificNotationFromFloat(x float64) (float64, error) {
bigNum := strconv.FormatFloat(x, 'g', -1, 64)
breakPoint := strings.IndexAny(bigNum, "Ee")
if breakPoint > 0 {
bigNum = bigNum[:breakPoint]
}
result, err := strconv.ParseFloat(bigNum, 64)
if err != nil {
return x, errors.Wrapf(err, "unable to remove scientific number from calculations")
}
return result, nil
}