mirror of
https://github.com/jesseduffield/lazydocker.git
synced 2026-07-21 23:01:03 +00:00
move container stats stuff into presentation layer
This commit is contained in:
parent
a87b698761
commit
02a03ed18d
5 changed files with 154 additions and 150 deletions
|
|
@ -13,7 +13,6 @@ import (
|
|||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/jesseduffield/lazydocker/pkg/config"
|
||||
"github.com/jesseduffield/lazydocker/pkg/i18n"
|
||||
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
|
@ -33,7 +32,6 @@ type Container struct {
|
|||
Container dockerTypes.Container
|
||||
Client *client.Client
|
||||
OSCommand *OSCommand
|
||||
Config *config.AppConfig
|
||||
Log *logrus.Entry
|
||||
StatHistory []*RecordedStats
|
||||
Details dockerTypes.ContainerJSON
|
||||
|
|
|
|||
|
|
@ -1,19 +1,8 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/asciigraph"
|
||||
"github.com/jesseduffield/lazydocker/pkg/config"
|
||||
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||
"github.com/mcuadros/go-lookup"
|
||||
)
|
||||
|
||||
// RecordedStats contains both the container stats we've received from docker, and our own derived stats from those container stats. When configuring a graph, you're basically specifying the path of a value in this struct
|
||||
|
|
@ -166,59 +155,22 @@ func (s *ContainerStats) CalculateContainerMemoryUsage() float64 {
|
|||
return value
|
||||
}
|
||||
|
||||
// RenderStats returns a string containing the rendered stats of the container
|
||||
func (c *Container) RenderStats(viewWidth int) (string, error) {
|
||||
stats, ok := c.GetLastStats()
|
||||
if !ok {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
graphSpecs := c.Config.UserConfig.Stats.Graphs
|
||||
graphs := make([]string, len(graphSpecs))
|
||||
for i, spec := range graphSpecs {
|
||||
graph, err := c.PlotGraph(spec, viewWidth-10)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
graphs[i] = utils.ColoredString(graph, utils.GetColorAttribute(spec.Color))
|
||||
}
|
||||
|
||||
pidsCount := fmt.Sprintf("PIDs: %d", stats.ClientStats.PidsStats.Current)
|
||||
dataReceived := fmt.Sprintf("Traffic received: %s", utils.FormatDecimalBytes(stats.ClientStats.Networks.Eth0.RxBytes))
|
||||
dataSent := fmt.Sprintf("Traffic sent: %s", utils.FormatDecimalBytes(stats.ClientStats.Networks.Eth0.TxBytes))
|
||||
|
||||
originalJSON, err := json.MarshalIndent(stats, "", " ")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
contents := fmt.Sprintf("\n\n%s\n\n%s\n\n%s\n%s\n\n%s",
|
||||
utils.ColoredString(strings.Join(graphs, "\n\n"), color.FgGreen),
|
||||
pidsCount,
|
||||
dataReceived,
|
||||
dataSent,
|
||||
string(originalJSON),
|
||||
)
|
||||
|
||||
return contents, nil
|
||||
}
|
||||
|
||||
func (c *Container) appendStats(stats *RecordedStats) {
|
||||
func (c *Container) appendStats(stats *RecordedStats, maxDuration time.Duration) {
|
||||
c.StatsMutex.Lock()
|
||||
defer c.StatsMutex.Unlock()
|
||||
|
||||
c.StatHistory = append(c.StatHistory, stats)
|
||||
c.eraseOldHistory()
|
||||
c.eraseOldHistory(maxDuration)
|
||||
}
|
||||
|
||||
// eraseOldHistory removes any history before the user-specified max duration
|
||||
func (c *Container) eraseOldHistory() {
|
||||
if c.Config.UserConfig.Stats.MaxDuration == 0 {
|
||||
func (c *Container) eraseOldHistory(maxDuration time.Duration) {
|
||||
if maxDuration == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
for i, stat := range c.StatHistory {
|
||||
if time.Since(stat.RecordedAt) < c.Config.UserConfig.Stats.MaxDuration {
|
||||
if time.Since(stat.RecordedAt) < maxDuration {
|
||||
c.StatHistory = c.StatHistory[i:]
|
||||
return
|
||||
}
|
||||
|
|
@ -234,95 +186,3 @@ func (c *Container) GetLastStats() (*RecordedStats, bool) {
|
|||
}
|
||||
return history[len(history)-1], true
|
||||
}
|
||||
|
||||
// PlotGraph returns the plotted graph based on the graph spec and the stat history
|
||||
func (c *Container) PlotGraph(spec config.GraphConfig, width int) (string, error) {
|
||||
c.StatsMutex.Lock()
|
||||
defer c.StatsMutex.Unlock()
|
||||
|
||||
data := make([]float64, len(c.StatHistory))
|
||||
|
||||
max := spec.Max
|
||||
min := spec.Min
|
||||
for i, stats := range c.StatHistory {
|
||||
value, err := lookup.LookupString(stats, spec.StatPath)
|
||||
if err != nil {
|
||||
return "Could not find key: " + spec.StatPath, nil
|
||||
}
|
||||
floatValue, err := getFloat(value.Interface())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if spec.MinType == "" {
|
||||
if i == 0 {
|
||||
min = floatValue
|
||||
} else if floatValue < min {
|
||||
min = floatValue
|
||||
}
|
||||
}
|
||||
|
||||
if spec.MaxType == "" {
|
||||
if i == 0 {
|
||||
max = floatValue
|
||||
} else if floatValue > max {
|
||||
max = floatValue
|
||||
}
|
||||
}
|
||||
|
||||
data[i] = floatValue
|
||||
}
|
||||
|
||||
height := 10
|
||||
if spec.Height > 0 {
|
||||
height = spec.Height
|
||||
}
|
||||
|
||||
return asciigraph.Plot(
|
||||
data,
|
||||
asciigraph.Height(height),
|
||||
asciigraph.Width(width),
|
||||
asciigraph.Min(min),
|
||||
asciigraph.Max(max),
|
||||
asciigraph.Caption(fmt.Sprintf("%s: %0.2f (%v)", spec.Caption, data[len(data)-1], time.Since(c.StatHistory[0].RecordedAt).Round(time.Second))),
|
||||
), nil
|
||||
}
|
||||
|
||||
// from Dave C's answer at https://stackoverflow.com/questions/20767724/converting-unknown-interface-to-float64-in-golang
|
||||
func getFloat(unk interface{}) (float64, error) {
|
||||
floatType := reflect.TypeOf(float64(0))
|
||||
stringType := reflect.TypeOf("")
|
||||
|
||||
switch i := unk.(type) {
|
||||
case float64:
|
||||
return i, nil
|
||||
case float32:
|
||||
return float64(i), nil
|
||||
case int64:
|
||||
return float64(i), nil
|
||||
case int32:
|
||||
return float64(i), nil
|
||||
case int:
|
||||
return float64(i), nil
|
||||
case uint64:
|
||||
return float64(i), nil
|
||||
case uint32:
|
||||
return float64(i), nil
|
||||
case uint:
|
||||
return float64(i), nil
|
||||
case string:
|
||||
return strconv.ParseFloat(i, 64)
|
||||
default:
|
||||
v := reflect.ValueOf(unk)
|
||||
v = reflect.Indirect(v)
|
||||
if v.Type().ConvertibleTo(floatType) {
|
||||
fv := v.Convert(floatType)
|
||||
return fv.Float(), nil
|
||||
} else if v.Type().ConvertibleTo(stringType) {
|
||||
sv := v.Convert(stringType)
|
||||
s := sv.String()
|
||||
return strconv.ParseFloat(s, 64)
|
||||
} else {
|
||||
return math.NaN(), fmt.Errorf("Can't convert %v to float64", v.Type())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -140,7 +140,7 @@ func (c *DockerCommand) CreateClientStatMonitor(container *Container) {
|
|||
RecordedAt: time.Now(),
|
||||
}
|
||||
|
||||
container.appendStats(recordedStats)
|
||||
container.appendStats(recordedStats, c.Config.UserConfig.Stats.MaxDuration)
|
||||
}
|
||||
|
||||
container.MonitoringStats = false
|
||||
|
|
@ -214,7 +214,6 @@ func (c *DockerCommand) GetContainers(existingContainers []*Container) ([]*Conta
|
|||
Client: c.Client,
|
||||
OSCommand: c.OSCommand,
|
||||
Log: c.Log,
|
||||
Config: c.Config,
|
||||
DockerCommand: c,
|
||||
Tr: c.Tr,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -222,7 +222,7 @@ func (gui *Gui) renderContainerStats(container *commands.Container) error {
|
|||
return gui.T.NewTickerTask(time.Second, func(stop chan struct{}) { gui.clearMainView() }, func(stop, notifyStopped chan struct{}) {
|
||||
width, _ := mainView.Size()
|
||||
|
||||
contents, err := container.RenderStats(width)
|
||||
contents, err := presentation.RenderStats(gui.Config.UserConfig, container, width)
|
||||
if err != nil {
|
||||
_ = gui.createErrorPanel(err.Error())
|
||||
}
|
||||
|
|
|
|||
147
pkg/gui/presentation/container_stats.go
Normal file
147
pkg/gui/presentation/container_stats.go
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
package presentation
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/asciigraph"
|
||||
"github.com/jesseduffield/lazydocker/pkg/commands"
|
||||
"github.com/jesseduffield/lazydocker/pkg/config"
|
||||
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||
"github.com/mcuadros/go-lookup"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
func RenderStats(userConfig *config.UserConfig, container *commands.Container, viewWidth int) (string, error) {
|
||||
stats, ok := container.GetLastStats()
|
||||
if !ok {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
graphSpecs := userConfig.Stats.Graphs
|
||||
graphs := make([]string, len(graphSpecs))
|
||||
for i, spec := range graphSpecs {
|
||||
graph, err := plotGraph(container, spec, viewWidth-10)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
graphs[i] = utils.ColoredString(graph, utils.GetColorAttribute(spec.Color))
|
||||
}
|
||||
|
||||
pidsCount := fmt.Sprintf("PIDs: %d", stats.ClientStats.PidsStats.Current)
|
||||
dataReceived := fmt.Sprintf("Traffic received: %s", utils.FormatDecimalBytes(stats.ClientStats.Networks.Eth0.RxBytes))
|
||||
dataSent := fmt.Sprintf("Traffic sent: %s", utils.FormatDecimalBytes(stats.ClientStats.Networks.Eth0.TxBytes))
|
||||
|
||||
originalJSON, err := json.MarshalIndent(stats, "", " ")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
contents := fmt.Sprintf("\n\n%s\n\n%s\n\n%s\n%s\n\n%s",
|
||||
utils.ColoredString(strings.Join(graphs, "\n\n"), color.FgGreen),
|
||||
pidsCount,
|
||||
dataReceived,
|
||||
dataSent,
|
||||
string(originalJSON),
|
||||
)
|
||||
|
||||
return contents, nil
|
||||
}
|
||||
|
||||
// plotGraph returns the plotted graph based on the graph spec and the stat history
|
||||
func plotGraph(container *commands.Container, spec config.GraphConfig, width int) (string, error) {
|
||||
container.StatsMutex.Lock()
|
||||
defer container.StatsMutex.Unlock()
|
||||
|
||||
data := make([]float64, len(container.StatHistory))
|
||||
|
||||
for i, stats := range container.StatHistory {
|
||||
value, err := lookup.LookupString(stats, spec.StatPath)
|
||||
if err != nil {
|
||||
return "Could not find key: " + spec.StatPath, nil
|
||||
}
|
||||
floatValue, err := getFloat(value.Interface())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
data[i] = floatValue
|
||||
}
|
||||
|
||||
max := spec.Max
|
||||
if spec.MaxType == "" {
|
||||
max = lo.Max(data)
|
||||
}
|
||||
|
||||
min := spec.Min
|
||||
if spec.MinType == "" {
|
||||
min = lo.Min(data)
|
||||
}
|
||||
|
||||
height := 10
|
||||
if spec.Height > 0 {
|
||||
height = spec.Height
|
||||
}
|
||||
|
||||
caption := fmt.Sprintf(
|
||||
"%s: %0.2f (%v)",
|
||||
spec.Caption,
|
||||
data[len(data)-1],
|
||||
time.Since(container.StatHistory[0].RecordedAt).Round(time.Second),
|
||||
)
|
||||
|
||||
return asciigraph.Plot(
|
||||
data,
|
||||
asciigraph.Height(height),
|
||||
asciigraph.Width(width),
|
||||
asciigraph.Min(min),
|
||||
asciigraph.Max(max),
|
||||
asciigraph.Caption(caption),
|
||||
), nil
|
||||
}
|
||||
|
||||
// from Dave C's answer at https://stackoverflow.com/questions/20767724/converting-unknown-interface-to-float64-in-golang
|
||||
func getFloat(unk interface{}) (float64, error) {
|
||||
floatType := reflect.TypeOf(float64(0))
|
||||
stringType := reflect.TypeOf("")
|
||||
|
||||
switch i := unk.(type) {
|
||||
case float64:
|
||||
return i, nil
|
||||
case float32:
|
||||
return float64(i), nil
|
||||
case int64:
|
||||
return float64(i), nil
|
||||
case int32:
|
||||
return float64(i), nil
|
||||
case int:
|
||||
return float64(i), nil
|
||||
case uint64:
|
||||
return float64(i), nil
|
||||
case uint32:
|
||||
return float64(i), nil
|
||||
case uint:
|
||||
return float64(i), nil
|
||||
case string:
|
||||
return strconv.ParseFloat(i, 64)
|
||||
default:
|
||||
v := reflect.ValueOf(unk)
|
||||
v = reflect.Indirect(v)
|
||||
if v.Type().ConvertibleTo(floatType) {
|
||||
fv := v.Convert(floatType)
|
||||
return fv.Float(), nil
|
||||
} else if v.Type().ConvertibleTo(stringType) {
|
||||
sv := v.Convert(stringType)
|
||||
s := sv.String()
|
||||
return strconv.ParseFloat(s, 64)
|
||||
} else {
|
||||
return math.NaN(), fmt.Errorf("Can't convert %v to float64", v.Type())
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue