From 02a03ed18dc939ca946865acf4589454b3ae8c47 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Mon, 31 Oct 2022 11:59:40 -0700 Subject: [PATCH] move container stats stuff into presentation layer --- pkg/commands/container.go | 2 - pkg/commands/container_stats.go | 150 +----------------------- pkg/commands/docker.go | 3 +- pkg/gui/containers_panel.go | 2 +- pkg/gui/presentation/container_stats.go | 147 +++++++++++++++++++++++ 5 files changed, 154 insertions(+), 150 deletions(-) create mode 100644 pkg/gui/presentation/container_stats.go diff --git a/pkg/commands/container.go b/pkg/commands/container.go index 033f8ce2..f46d7ffc 100644 --- a/pkg/commands/container.go +++ b/pkg/commands/container.go @@ -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 diff --git a/pkg/commands/container_stats.go b/pkg/commands/container_stats.go index 3579743b..419782dd 100644 --- a/pkg/commands/container_stats.go +++ b/pkg/commands/container_stats.go @@ -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()) - } - } -} diff --git a/pkg/commands/docker.go b/pkg/commands/docker.go index 75a916fc..df1f1973 100644 --- a/pkg/commands/docker.go +++ b/pkg/commands/docker.go @@ -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, } diff --git a/pkg/gui/containers_panel.go b/pkg/gui/containers_panel.go index 3781f772..7bcf5b1e 100644 --- a/pkg/gui/containers_panel.go +++ b/pkg/gui/containers_panel.go @@ -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()) } diff --git a/pkg/gui/presentation/container_stats.go b/pkg/gui/presentation/container_stats.go new file mode 100644 index 00000000..82767881 --- /dev/null +++ b/pkg/gui/presentation/container_stats.go @@ -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()) + } + } +}