Additional stats for podman info

In support of podman machine and its counterpart desktop, we have added
new stats to podman info.

For storage, we have added GraphRootAllocated and GraphRootUsed in
bytes.

For CPUs, we have added user, system, and idle percents based on
/proc/stat.

Fixes: #13876

Signed-off-by: Brent Baude <bbaude@redhat.com>
This commit is contained in:
Brent Baude
2022-04-28 12:31:17 -05:00
parent 3ac5cec086
commit 0bb4849377
4 changed files with 150 additions and 17 deletions

View File

@ -39,6 +39,10 @@ host:
package: conmon-2.0.29-2.fc34.x86_64 package: conmon-2.0.29-2.fc34.x86_64
path: /usr/bin/conmon path: /usr/bin/conmon
version: 'conmon version 2.0.29, commit: ' version: 'conmon version 2.0.29, commit: '
cpu_utilization:
idle_percent: 96.84
system_percent: 0.71
user_percent: 2.45
cpus: 8 cpus: 8
distribution: distribution:
distribution: fedora distribution: fedora
@ -124,6 +128,8 @@ store:
graphDriverName: overlay graphDriverName: overlay
graphOptions: {} graphOptions: {}
graphRoot: /home/dwalsh/.local/share/containers/storage graphRoot: /home/dwalsh/.local/share/containers/storage
graphRootAllocated: 510389125120
graphRootUsed: 129170714624
graphStatus: graphStatus:
Backing Filesystem: extfs Backing Filesystem: extfs
Native Overlay Diff: "true" Native Overlay Diff: "true"

View File

@ -1,6 +1,8 @@
package define package define
import "github.com/containers/storage/pkg/idtools" import (
"github.com/containers/storage/pkg/idtools"
)
// Info is the overall struct that describes the host system // Info is the overall struct that describes the host system
// running libpod/podman // running libpod/podman
@ -31,6 +33,7 @@ type HostInfo struct {
CgroupControllers []string `json:"cgroupControllers"` CgroupControllers []string `json:"cgroupControllers"`
Conmon *ConmonInfo `json:"conmon"` Conmon *ConmonInfo `json:"conmon"`
CPUs int `json:"cpus"` CPUs int `json:"cpus"`
CPUUtilization *CPUUsage `json:"cpuUtilization"`
Distribution DistributionInfo `json:"distribution"` Distribution DistributionInfo `json:"distribution"`
EventLogger string `json:"eventLogger"` EventLogger string `json:"eventLogger"`
Hostname string `json:"hostname"` Hostname string `json:"hostname"`
@ -108,11 +111,15 @@ type StoreInfo struct {
GraphDriverName string `json:"graphDriverName"` GraphDriverName string `json:"graphDriverName"`
GraphOptions map[string]interface{} `json:"graphOptions"` GraphOptions map[string]interface{} `json:"graphOptions"`
GraphRoot string `json:"graphRoot"` GraphRoot string `json:"graphRoot"`
GraphStatus map[string]string `json:"graphStatus"` // GraphRootAllocated is how much space the graphroot has in bytes
ImageCopyTmpDir string `json:"imageCopyTmpDir"` GraphRootAllocated uint64 `json:"graphRootAllocated"`
ImageStore ImageStore `json:"imageStore"` // GraphRootUsed is how much of graphroot is used in bytes
RunRoot string `json:"runRoot"` GraphRootUsed uint64 `json:"graphRootUsed"`
VolumePath string `json:"volumePath"` GraphStatus map[string]string `json:"graphStatus"`
ImageCopyTmpDir string `json:"imageCopyTmpDir"`
ImageStore ImageStore `json:"imageStore"`
RunRoot string `json:"runRoot"`
VolumePath string `json:"volumePath"`
} }
// ImageStore describes the image store. Right now only the number // ImageStore describes the image store. Right now only the number
@ -137,3 +144,9 @@ type Plugins struct {
// FIXME what should we do with Authorization, docker seems to return nothing by default // FIXME what should we do with Authorization, docker seems to return nothing by default
// Authorization []string `json:"authorization"` // Authorization []string `json:"authorization"`
} }
type CPUUsage struct {
UserPercent float64 `json:"userPercent"`
SystemPercent float64 `json:"systemPercent"`
IdlePercent float64 `json:"idlePercent"`
}

View File

@ -5,11 +5,13 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"math"
"os" "os"
"os/exec" "os/exec"
"runtime" "runtime"
"strconv" "strconv"
"strings" "strings"
"syscall"
"time" "time"
"github.com/containers/buildah" "github.com/containers/buildah"
@ -115,7 +117,10 @@ func (r *Runtime) hostInfo() (*define.HostInfo, error) {
if err != nil { if err != nil {
return nil, errors.Wrapf(err, "error getting available cgroup controllers") return nil, errors.Wrapf(err, "error getting available cgroup controllers")
} }
cpuUtil, err := getCPUUtilization()
if err != nil {
return nil, err
}
info := define.HostInfo{ info := define.HostInfo{
Arch: runtime.GOARCH, Arch: runtime.GOARCH,
BuildahVersion: buildah.Version, BuildahVersion: buildah.Version,
@ -123,6 +128,7 @@ func (r *Runtime) hostInfo() (*define.HostInfo, error) {
CgroupControllers: availableControllers, CgroupControllers: availableControllers,
Linkmode: linkmode.Linkmode(), Linkmode: linkmode.Linkmode(),
CPUs: runtime.NumCPU(), CPUs: runtime.NumCPU(),
CPUUtilization: cpuUtil,
Distribution: hostDistributionInfo, Distribution: hostDistributionInfo,
LogDriver: r.config.Containers.LogDriver, LogDriver: r.config.Containers.LogDriver,
EventLogger: r.eventer.String(), EventLogger: r.eventer.String(),
@ -285,17 +291,25 @@ func (r *Runtime) storeInfo() (*define.StoreInfo, error) {
} }
imageInfo := define.ImageStore{Number: len(images)} imageInfo := define.ImageStore{Number: len(images)}
info := define.StoreInfo{ var grStats syscall.Statfs_t
ImageStore: imageInfo, if err := syscall.Statfs(r.store.GraphRoot(), &grStats); err != nil {
ImageCopyTmpDir: os.Getenv("TMPDIR"), return nil, errors.Wrapf(err, "unable to collect graph root usasge for %q", r.store.GraphRoot())
ContainerStore: conInfo,
GraphRoot: r.store.GraphRoot(),
RunRoot: r.store.RunRoot(),
GraphDriverName: r.store.GraphDriverName(),
GraphOptions: nil,
VolumePath: r.config.Engine.VolumePath,
ConfigFile: configFile,
} }
allocated := uint64(grStats.Bsize) * grStats.Blocks
info := define.StoreInfo{
ImageStore: imageInfo,
ImageCopyTmpDir: os.Getenv("TMPDIR"),
ContainerStore: conInfo,
GraphRoot: r.store.GraphRoot(),
GraphRootAllocated: allocated,
GraphRootUsed: allocated - (uint64(grStats.Bsize) * grStats.Bfree),
RunRoot: r.store.RunRoot(),
GraphDriverName: r.store.GraphDriverName(),
GraphOptions: nil,
VolumePath: r.config.Engine.VolumePath,
ConfigFile: configFile,
}
graphOptions := map[string]interface{}{} graphOptions := map[string]interface{}{}
for _, o := range r.store.GraphOptions() { for _, o := range r.store.GraphOptions() {
split := strings.SplitN(o, "=", 2) split := strings.SplitN(o, "=", 2)
@ -382,3 +396,44 @@ func (r *Runtime) GetHostDistributionInfo() define.DistributionInfo {
} }
return dist return dist
} }
// getCPUUtilization Returns a CPUUsage object that summarizes CPU
// usage for userspace, system, and idle time.
func getCPUUtilization() (*define.CPUUsage, error) {
f, err := os.Open("/proc/stat")
if err != nil {
return nil, err
}
defer f.Close()
scanner := bufio.NewScanner(f)
// Read firt line of /proc/stat
for scanner.Scan() {
break
}
// column 1 is user, column 3 is system, column 4 is idle
stats := strings.Split(scanner.Text(), " ")
return statToPercent(stats)
}
func statToPercent(stats []string) (*define.CPUUsage, error) {
// There is always an extra space between cpu and the first metric
userTotal, err := strconv.ParseFloat(stats[2], 64)
if err != nil {
return nil, errors.Wrapf(err, "unable to parse user value %q", stats[1])
}
systemTotal, err := strconv.ParseFloat(stats[4], 64)
if err != nil {
return nil, errors.Wrapf(err, "unable to parse system value %q", stats[3])
}
idleTotal, err := strconv.ParseFloat(stats[5], 64)
if err != nil {
return nil, errors.Wrapf(err, "unable to parse idle value %q", stats[4])
}
total := userTotal + systemTotal + idleTotal
s := define.CPUUsage{
UserPercent: math.Round((userTotal/total*100)*100) / 100,
SystemPercent: math.Round((systemTotal/total*100)*100) / 100,
IdlePercent: math.Round((idleTotal/total*100)*100) / 100,
}
return &s, nil
}

59
libpod/info_test.go Normal file
View File

@ -0,0 +1,59 @@
package libpod
import (
"fmt"
"testing"
"github.com/containers/podman/v4/libpod/define"
"github.com/stretchr/testify/assert"
)
func Test_statToPercent(t *testing.T) {
type args struct {
in0 []string
}
tests := []struct {
name string
args args
want *define.CPUUsage
wantErr assert.ErrorAssertionFunc
}{
{
name: "GoodParse",
args: args{in0: []string{"cpu", " ", "33628064", "27537", "9696996", "1314806705", "588142", "4775073", "2789228", "0", "598711", "0"}},
want: &define.CPUUsage{
UserPercent: 2.48,
SystemPercent: 0.71,
IdlePercent: 96.81,
},
wantErr: assert.NoError,
},
{
name: "BadUserValue",
args: args{in0: []string{"cpu", " ", "k", "27537", "9696996", "1314806705", "588142", "4775073", "2789228", "0", "598711", "0"}},
want: nil,
wantErr: assert.Error,
},
{
name: "BadSystemValue",
args: args{in0: []string{"cpu", " ", "33628064", "27537", "k", "1314806705", "588142", "4775073", "2789228", "0", "598711", "0"}},
want: nil,
wantErr: assert.Error,
},
{
name: "BadIdleValue",
args: args{in0: []string{"cpu", " ", "33628064", "27537", "9696996", "k", "588142", "4775073", "2789228", "0", "598711", "0"}},
want: nil,
wantErr: assert.Error,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := statToPercent(tt.args.in0)
if !tt.wantErr(t, err, fmt.Sprintf("statToPercent(%v)", tt.args.in0)) {
return
}
assert.Equalf(t, tt.want, got, "statToPercent(%v)", tt.args.in0)
})
}
}