From 139ea8707cdc7017bca1dd562cfa90b6f7b9af2c Mon Sep 17 00:00:00 2001 From: "Joel D. Elkins" Date: Thu, 22 Feb 2024 21:56:34 -0600 Subject: [PATCH] ls enhancement: show running stats Mainly for zabbix, but why not. Format of both json and text output of ls has changed. --- cmd/ls.go | 58 ++++++++++++++++++++++++++--- internal/pkg/container/container.go | 30 +++++++++++++++ 2 files changed, 82 insertions(+), 6 deletions(-) diff --git a/cmd/ls.go b/cmd/ls.go index 4a609dc..7821c15 100644 --- a/cmd/ls.go +++ b/cmd/ls.go @@ -48,6 +48,20 @@ type lsContainerObj struct { Running bool `json:"running"` } +type lsContainerStatsObj struct { + CPU float64 `json:"cpu_usage_pct"` + CPUNano uint64 `json:"cpu_nano"` + MemUsage uint64 `json:"mem_usage_bytes"` + MemLimit uint64 `json:"mem_limit_bytes"` + MemPerc float64 `json:"mem_usage_pct"` + NetInput uint64 `json:"net_input_bytes"` + NetOutput uint64 `json:"net_output_bytes"` + BlockInput uint64 `json:"block_input_bytes"` + BlockOutput uint64 `json:"block_output_bytes"` + UpTime uint64 `json:"uptime_sec"` + lsContainerObj +} + // lsCmd represents the ls command var lsCmd = &cobra.Command{ Use: "ls", @@ -90,20 +104,41 @@ ccl ls squid`, } if lsJsonFormat { - out := make([]lsContainerObj, 0) + out := make([]interface{}, 0) for _, c := range conts { run, cre := false, false if conn != nil { run, cre = c.IsRunning(), c.IsCreated() } - out = append(out, lsContainerObj{ + baseObj := lsContainerObj{ Category: c.Category, StartGroup: c.StartGroup, Name: c.Name, Image: c.Image, Running: run, Created: cre, - }) + } + if run { + if stats := c.GetStats(); stats != nil { + out = append(out, lsContainerStatsObj{ + CPUNano: stats.CPUNano, + CPU: stats.CPU, + MemUsage: stats.MemUsage, + MemLimit: stats.MemLimit, + MemPerc: stats.MemPerc, + NetInput: stats.NetInput, + NetOutput: stats.NetOutput, + BlockInput: stats.BlockInput, + BlockOutput: stats.BlockOutput, + UpTime: uint64(c.RunningTime().Seconds()), + lsContainerObj: baseObj, + }) + } else { + out = append(out, baseObj) + } + } else { + out = append(out, baseObj) + } } val, err := json.Marshal(out) if err != nil { @@ -117,7 +152,8 @@ ccl ls squid`, defer tw.Flush() if conn != nil { - titlemsg := "CATEGORY\tGROUP\tNAME\tIMAGE\tCREATED\tRUNNING" + titlemsg := "CATEGORY\tGROUP\tNAME\tIMAGE\tCREATED\t RUNNING\tCPU%\tMEM%" + block := "%s\t%5d\t%s\t%s\t%s\t%s\t%.1f\t%.1f\n" fmt.Fprintf(tw, "%s\n", titlemsg) for _, c := range conts { data := []interface{}{c.Category, c.StartGroup, c.Name, c.Image} @@ -127,11 +163,21 @@ ccl ls squid`, data = append(data, "") } if c.IsRunning() { - data = append(data, " ✓") + raw := int64(c.RunningTime().Seconds()) + seconds := raw % 60 + minutes := (raw / 60) % 60 + hours := (raw / 60) / 60 + disp := fmt.Sprintf("%3d:%02d:%02d", hours, minutes, seconds) + data = append(data, disp) } else { data = append(data, "") } - fmt.Fprintf(tw, "%s\t%5d\t%s\t%s\t%s\t%s\n", data...) + if stats := c.GetStats(); c.IsRunning() && stats != nil { + data = append(data, stats.CPU, stats.MemPerc) + } else { + data = append(data, 0.0, 0.0) + } + fmt.Fprintf(tw, block, data...) } } else { titlemsg := "CATEGORY\tGROUP\tNAME\tIMAGE" diff --git a/internal/pkg/container/container.go b/internal/pkg/container/container.go index 78eb53e..1daec61 100644 --- a/internal/pkg/container/container.go +++ b/internal/pkg/container/container.go @@ -28,8 +28,10 @@ import ( "context" "fmt" "net" + "os" "os/exec" "regexp" + "time" cmd "gitea.elkins.co/Networking/ccl/internal/pkg/command" "gitea.elkins.co/Networking/ccl/internal/pkg/network" @@ -390,6 +392,14 @@ func (c *Container) IsCreated() bool { return true } +func (c *Container) RunningTime() time.Duration { + cdata := c.getCData() + if cdata == nil || cdata.State == nil { + return 0 + } + return time.Since(cdata.State.StartedAt) +} + // UpdateCommands will pull the image (to force updates) and then recreate the // container. It will be stopped first. func (c *Container) UpdateCommands() cmd.Set { @@ -503,6 +513,26 @@ func (c *Container) watchCData() { } } +func (c *Container) GetStats() *define.ContainerStats { + no := false + reportChan, err := containers.Stats(c.conn, []string{c.getCData().ID}, &containers.StatsOptions{Stream: &no}) + if err != nil { + fmt.Fprintf(os.Stderr, "containers.stats returned error (%s): %s\n", c.Name, err) + return nil + } + select { + case report := <-reportChan: + if report.Error != nil { + fmt.Fprintf(os.Stderr, "containers.stats returned error in the channel (%s): %s\n", c.Name, report.Error) + break + } + return &report.Stats[0] + case <-time.After(250 * time.Millisecond): + break + } + return nil +} + // Pid will return the host process id of the main container process (pid // 1 inside the container) func (c *Container) Pid() int {