mirror of
https://github.com/jesseduffield/lazydocker.git
synced 2026-07-25 08:31:03 +00:00
configurable graphs
This commit is contained in:
parent
5bf1a40d2f
commit
28b488b740
7 changed files with 282 additions and 122 deletions
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
"github.com/go-errors/errors"
|
"github.com/go-errors/errors"
|
||||||
|
"github.com/jesseduffield/lazydocker/pkg/config"
|
||||||
"github.com/jesseduffield/lazydocker/pkg/utils"
|
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"golang.org/x/xerrors"
|
"golang.org/x/xerrors"
|
||||||
|
|
@ -28,10 +29,21 @@ type Container struct {
|
||||||
DisplayString string
|
DisplayString string
|
||||||
Client *client.Client
|
Client *client.Client
|
||||||
OSCommand *OSCommand
|
OSCommand *OSCommand
|
||||||
|
Config *config.AppConfig
|
||||||
Log *logrus.Entry
|
Log *logrus.Entry
|
||||||
Stats ContainerCliStat
|
CLIStats ContainerCliStat // for realtime we use the CLI, for long-term we use the client
|
||||||
StatHistory []ContainerCliStat
|
StatHistory []RecordedStats
|
||||||
Details Details
|
Details Details
|
||||||
|
MonitoringStats bool
|
||||||
|
}
|
||||||
|
type RecordedStats struct {
|
||||||
|
ClientStats ContainerStats
|
||||||
|
DerivedStats DerivedStats
|
||||||
|
}
|
||||||
|
|
||||||
|
type DerivedStats struct {
|
||||||
|
CPUPercentage float64
|
||||||
|
MemoryPercentage float64
|
||||||
}
|
}
|
||||||
|
|
||||||
type Details struct {
|
type Details struct {
|
||||||
|
|
@ -246,7 +258,7 @@ func (c *Container) GetDisplayStrings(isFocused bool) []string {
|
||||||
|
|
||||||
// GetDisplayCPUPerc colors the cpu percentage based on how extreme it is
|
// GetDisplayCPUPerc colors the cpu percentage based on how extreme it is
|
||||||
func (c *Container) GetDisplayCPUPerc() string {
|
func (c *Container) GetDisplayCPUPerc() string {
|
||||||
stats := c.GetStats()
|
stats := c.CLIStats
|
||||||
|
|
||||||
if stats.CPUPerc == "" {
|
if stats.CPUPerc == "" {
|
||||||
return ""
|
return ""
|
||||||
|
|
@ -341,11 +353,3 @@ func (c *Container) Attach() (*exec.Cmd, error) {
|
||||||
func (c *Container) Top() (types.ContainerProcessList, error) {
|
func (c *Container) Top() (types.ContainerProcessList, error) {
|
||||||
return c.Client.ContainerTop(context.Background(), c.ID, []string{})
|
return c.Client.ContainerTop(context.Background(), c.ID, []string{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetStats gets the container's most recent stats
|
|
||||||
func (c *Container) GetStats() ContainerCliStat {
|
|
||||||
if len(c.StatHistory) == 0 {
|
|
||||||
return ContainerCliStat{}
|
|
||||||
}
|
|
||||||
return c.StatHistory[len(c.StatHistory)-1]
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,16 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/carlosms/asciigraph"
|
"github.com/carlosms/asciigraph"
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
|
"github.com/jesseduffield/lazydocker/pkg/config"
|
||||||
"github.com/jesseduffield/lazydocker/pkg/utils"
|
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||||
|
"github.com/mcuadros/go-lookup"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ContainerStats autogenerated at https://mholt.github.io/json-to-go/
|
// ContainerStats autogenerated at https://mholt.github.io/json-to-go/
|
||||||
|
|
@ -143,6 +148,7 @@ func (s *ContainerStats) CalculateContainerCPUPercentage() float64 {
|
||||||
|
|
||||||
// CalculateContainerMemoryUsage calculates the memory usage of the container as a percent of total available memory
|
// CalculateContainerMemoryUsage calculates the memory usage of the container as a percent of total available memory
|
||||||
func (s *ContainerStats) CalculateContainerMemoryUsage() float64 {
|
func (s *ContainerStats) CalculateContainerMemoryUsage() float64 {
|
||||||
|
|
||||||
value := float64(s.MemoryStats.Usage*100) / float64(s.MemoryStats.Limit)
|
value := float64(s.MemoryStats.Usage*100) / float64(s.MemoryStats.Limit)
|
||||||
if math.IsNaN(value) {
|
if math.IsNaN(value) {
|
||||||
return 0
|
return 0
|
||||||
|
|
@ -151,44 +157,34 @@ func (s *ContainerStats) CalculateContainerMemoryUsage() float64 {
|
||||||
}
|
}
|
||||||
|
|
||||||
// RenderStats returns a string containing the rendered stats of the container
|
// RenderStats returns a string containing the rendered stats of the container
|
||||||
func (s *ContainerStats) RenderStats(viewWidth int, cpuUsageHistory []float64, memoryUsageHistory []float64) (string, error) {
|
func (c *Container) RenderStats(viewWidth int) (string, error) {
|
||||||
memoryGraph := asciigraph.Plot(
|
history := c.StatHistory
|
||||||
memoryUsageHistory,
|
if len(history) == 0 {
|
||||||
asciigraph.Height(10),
|
return "", nil
|
||||||
asciigraph.Width(viewWidth-10),
|
}
|
||||||
asciigraph.Min(0),
|
currentStats := history[len(history)-1]
|
||||||
asciigraph.Max(100),
|
|
||||||
asciigraph.Caption(
|
|
||||||
fmt.Sprintf(
|
|
||||||
"%.2f%% Memory (%s/%s)",
|
|
||||||
memoryUsageHistory[len(memoryUsageHistory)-1],
|
|
||||||
utils.FormatBinaryBytes(s.MemoryStats.Usage),
|
|
||||||
utils.FormatBinaryBytes(int(s.MemoryStats.Limit)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
cpuGraph := asciigraph.Plot(
|
graphSpecs := c.Config.UserConfig.Stats.Graphs
|
||||||
cpuUsageHistory,
|
graphs := make([]string, len(graphSpecs))
|
||||||
asciigraph.Height(10),
|
for i, spec := range graphSpecs {
|
||||||
asciigraph.Width(viewWidth-10),
|
graph, err := c.PlotGraph(spec, viewWidth-10)
|
||||||
asciigraph.Min(0),
|
if err != nil {
|
||||||
asciigraph.Max(100),
|
return "", err
|
||||||
asciigraph.Caption(fmt.Sprintf("%.2f%% CPU", cpuUsageHistory[len(cpuUsageHistory)-1])),
|
}
|
||||||
)
|
graphs[i] = utils.ColoredString(graph, utils.GetColorAttribute(spec.Color))
|
||||||
|
}
|
||||||
|
|
||||||
pidsCount := fmt.Sprintf("PIDs: %d", s.PidsStats.Current)
|
pidsCount := fmt.Sprintf("PIDs: %d", currentStats.ClientStats.PidsStats.Current)
|
||||||
dataReceived := fmt.Sprintf("Traffic received: %s", utils.FormatDecimalBytes(s.Networks.Eth0.RxBytes))
|
dataReceived := fmt.Sprintf("Traffic received: %s", utils.FormatDecimalBytes(currentStats.ClientStats.Networks.Eth0.RxBytes))
|
||||||
dataSent := fmt.Sprintf("Traffic sent: %s", utils.FormatDecimalBytes(s.Networks.Eth0.TxBytes))
|
dataSent := fmt.Sprintf("Traffic sent: %s", utils.FormatDecimalBytes(currentStats.ClientStats.Networks.Eth0.TxBytes))
|
||||||
|
|
||||||
originalJSON, err := json.MarshalIndent(s, "", " ")
|
originalJSON, err := json.MarshalIndent(currentStats, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
contents := fmt.Sprintf("\n\n%s\n\n%s\n\n%s\n\n%s\n%s\n\n%s",
|
contents := fmt.Sprintf("\n\n%s\n\n%s\n\n%s\n%s\n\n%s",
|
||||||
utils.ColoredString(cpuGraph, color.FgCyan),
|
utils.ColoredString(strings.Join(graphs, "\n\n"), color.FgGreen),
|
||||||
utils.ColoredString(memoryGraph, color.FgGreen),
|
|
||||||
pidsCount,
|
pidsCount,
|
||||||
dataReceived,
|
dataReceived,
|
||||||
dataSent,
|
dataSent,
|
||||||
|
|
@ -197,3 +193,68 @@ func (s *ContainerStats) RenderStats(viewWidth int, cpuUsageHistory []float64, m
|
||||||
|
|
||||||
return contents, nil
|
return contents, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
data := make([]float64, len(c.StatHistory))
|
||||||
|
for i, stats := range c.StatHistory {
|
||||||
|
value, err := lookup.LookupString(stats, spec.StatPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
floatValue, err := getFloat(value.Interface())
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
data[i] = floatValue
|
||||||
|
}
|
||||||
|
|
||||||
|
return asciigraph.Plot(
|
||||||
|
data,
|
||||||
|
asciigraph.Height(spec.Height),
|
||||||
|
asciigraph.Width(width),
|
||||||
|
asciigraph.Min(spec.Min),
|
||||||
|
asciigraph.Max(spec.Max),
|
||||||
|
asciigraph.Caption(spec.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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/acarl005/stripansi"
|
"github.com/acarl005/stripansi"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
|
|
@ -53,9 +54,13 @@ func NewDockerCommand(log *logrus.Entry, osCommand *OSCommand, tr *i18n.Localize
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// MonitorContainerStats monitors a stream of container stats and updates the containers as each new stats object is received
|
|
||||||
func (c *DockerCommand) MonitorContainerStats() {
|
func (c *DockerCommand) MonitorContainerStats() {
|
||||||
|
go c.MonitorCLIContainerStats()
|
||||||
|
go c.MonitorClientContainerStats()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MonitorCLIContainerStats monitors a stream of container stats and updates the containers as each new stats object is received
|
||||||
|
func (c *DockerCommand) MonitorCLIContainerStats() {
|
||||||
command := `docker stats --all --no-trunc --format '{{json .}}'`
|
command := `docker stats --all --no-trunc --format '{{json .}}'`
|
||||||
cmd := c.OSCommand.RunCustomCommand(command)
|
cmd := c.OSCommand.RunCustomCommand(command)
|
||||||
|
|
||||||
|
|
@ -80,7 +85,7 @@ func (c *DockerCommand) MonitorContainerStats() {
|
||||||
c.ContainerMutex.Lock()
|
c.ContainerMutex.Lock()
|
||||||
for _, container := range c.Containers {
|
for _, container := range c.Containers {
|
||||||
if container.ID == stats.ID {
|
if container.ID == stats.ID {
|
||||||
container.StatHistory = append(container.StatHistory, stats)
|
container.CLIStats = stats
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
c.ContainerMutex.Unlock()
|
c.ContainerMutex.Unlock()
|
||||||
|
|
@ -91,6 +96,52 @@ func (c *DockerCommand) MonitorContainerStats() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *DockerCommand) MonitorClientContainerStats() {
|
||||||
|
// periodically loop through running containers and see if we need to create a monitor goroutine for any
|
||||||
|
// every second we check if we need to spawn a new goroutine
|
||||||
|
for range time.Tick(time.Second) {
|
||||||
|
for _, container := range c.Containers {
|
||||||
|
if !container.MonitoringStats {
|
||||||
|
go c.createClientStatMonitor(container)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *DockerCommand) createClientStatMonitor(container *Container) {
|
||||||
|
container.MonitoringStats = true
|
||||||
|
stream, err := c.Client.ContainerStats(context.Background(), container.ID, true)
|
||||||
|
if err != nil {
|
||||||
|
c.ErrorChan <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer stream.Body.Close()
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(stream.Body)
|
||||||
|
for scanner.Scan() {
|
||||||
|
data := scanner.Bytes()
|
||||||
|
var stats ContainerStats
|
||||||
|
json.Unmarshal(data, &stats)
|
||||||
|
|
||||||
|
recordedStats := RecordedStats{
|
||||||
|
ClientStats: stats,
|
||||||
|
DerivedStats: DerivedStats{
|
||||||
|
CPUPercentage: stats.CalculateContainerCPUPercentage(),
|
||||||
|
MemoryPercentage: stats.CalculateContainerMemoryUsage(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
c.ContainerMutex.Lock()
|
||||||
|
// TODO: for now we never truncate the recorded stats, and we should
|
||||||
|
container.StatHistory = append(container.StatHistory, recordedStats)
|
||||||
|
c.ContainerMutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
container.MonitoringStats = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// GetContainersAndServices returns a slice of docker containers
|
// GetContainersAndServices returns a slice of docker containers
|
||||||
func (c *DockerCommand) GetContainersAndServices() error {
|
func (c *DockerCommand) GetContainersAndServices() error {
|
||||||
c.ServiceMutex.Lock()
|
c.ServiceMutex.Lock()
|
||||||
|
|
@ -147,7 +198,7 @@ func (c *DockerCommand) GetContainers() ([]*Container, error) {
|
||||||
c.ContainerMutex.Lock()
|
c.ContainerMutex.Lock()
|
||||||
defer c.ContainerMutex.Unlock()
|
defer c.ContainerMutex.Unlock()
|
||||||
|
|
||||||
currentContainers := c.Containers
|
existingContainers := c.Containers
|
||||||
|
|
||||||
containers, err := c.Client.ContainerList(context.Background(), types.ContainerListOptions{All: true})
|
containers, err := c.Client.ContainerList(context.Background(), types.ContainerListOptions{All: true})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -157,26 +208,35 @@ func (c *DockerCommand) GetContainers() ([]*Container, error) {
|
||||||
ownContainers := make([]*Container, len(containers))
|
ownContainers := make([]*Container, len(containers))
|
||||||
|
|
||||||
for i, container := range containers {
|
for i, container := range containers {
|
||||||
ownContainers[i] = &Container{
|
var newContainer *Container
|
||||||
ID: container.ID,
|
|
||||||
Name: strings.TrimLeft(container.Names[0], "/"),
|
|
||||||
ServiceName: container.Labels["com.docker.compose.service"],
|
|
||||||
ServiceID: container.Labels["com.docker.compose.config-hash"],
|
|
||||||
ProjectName: container.Labels["com.docker.compose.project"],
|
|
||||||
ContainerNumber: container.Labels["com.docker.compose.container"],
|
|
||||||
Container: container,
|
|
||||||
Client: c.Client,
|
|
||||||
OSCommand: c.OSCommand,
|
|
||||||
Log: c.Log,
|
|
||||||
}
|
|
||||||
|
|
||||||
// bring across old stats information
|
// check if we already data stored against the container
|
||||||
for _, currentContainer := range currentContainers {
|
for _, existingContainer := range existingContainers {
|
||||||
if currentContainer.ID == container.ID {
|
if existingContainer.ID == container.ID {
|
||||||
ownContainers[i].StatHistory = currentContainer.StatHistory // might need to use pointers here in case we're deep copying everything
|
newContainer = existingContainer
|
||||||
ownContainers[i].Details = currentContainer.Details
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// initialise the container if it's completely new
|
||||||
|
if newContainer == nil {
|
||||||
|
newContainer = &Container{
|
||||||
|
ID: container.ID,
|
||||||
|
Client: c.Client,
|
||||||
|
OSCommand: c.OSCommand,
|
||||||
|
Log: c.Log,
|
||||||
|
Config: c.Config,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newContainer.Container = container
|
||||||
|
newContainer.Name = strings.TrimLeft(container.Names[0], "/")
|
||||||
|
newContainer.ServiceName = container.Labels["com.docker.compose.service"]
|
||||||
|
newContainer.ServiceID = container.Labels["com.docker.compose.config-hash"]
|
||||||
|
newContainer.ProjectName = container.Labels["com.docker.compose.project"]
|
||||||
|
newContainer.ContainerNumber = container.Labels["com.docker.compose.container"]
|
||||||
|
|
||||||
|
ownContainers[i] = newContainer
|
||||||
}
|
}
|
||||||
|
|
||||||
return ownContainers, nil
|
return ownContainers, nil
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,7 @@ type UserConfig struct {
|
||||||
CommandTemplates CommandTemplatesConfig `yaml:"commandTemplates,omitempty"`
|
CommandTemplates CommandTemplatesConfig `yaml:"commandTemplates,omitempty"`
|
||||||
OS OSConfig `yaml:"oS,omitempty"`
|
OS OSConfig `yaml:"oS,omitempty"`
|
||||||
Update UpdateConfig `yaml:"update,omitempty"`
|
Update UpdateConfig `yaml:"update,omitempty"`
|
||||||
|
Stats StatsConfig `yaml:"stats,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ThemeConfig struct {
|
type ThemeConfig struct {
|
||||||
|
|
@ -114,6 +115,20 @@ type UpdateConfig struct {
|
||||||
Method string `yaml:"method,omitempty"`
|
Method string `yaml:"method,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GraphConfig specifies how to make a graph of recorded container stats
|
||||||
|
type GraphConfig struct {
|
||||||
|
Min float64 `yaml:"min,omitempty"`
|
||||||
|
Max float64 `yaml:"max,omitempty"`
|
||||||
|
Height int `yaml:"height,omitempty"`
|
||||||
|
Caption string `yaml:"caption,omitempty"`
|
||||||
|
StatPath string `yaml:"statPath,omitempty"`
|
||||||
|
Color string `yaml:"color,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type StatsConfig struct {
|
||||||
|
Graphs []GraphConfig
|
||||||
|
}
|
||||||
|
|
||||||
// GetDefaultConfig returns the application default configuration
|
// GetDefaultConfig returns the application default configuration
|
||||||
func GetDefaultConfig() UserConfig {
|
func GetDefaultConfig() UserConfig {
|
||||||
return UserConfig{
|
return UserConfig{
|
||||||
|
|
@ -139,6 +154,26 @@ func GetDefaultConfig() UserConfig {
|
||||||
Update: UpdateConfig{
|
Update: UpdateConfig{
|
||||||
Method: "never",
|
Method: "never",
|
||||||
},
|
},
|
||||||
|
Stats: StatsConfig{
|
||||||
|
Graphs: []GraphConfig{
|
||||||
|
{
|
||||||
|
Min: 0,
|
||||||
|
Max: 100,
|
||||||
|
Height: 10,
|
||||||
|
Caption: "CPU (%)",
|
||||||
|
StatPath: "DerivedStats.CPUPercentage",
|
||||||
|
Color: "blue",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Min: 0,
|
||||||
|
Max: 100,
|
||||||
|
Height: 10,
|
||||||
|
Caption: "Memory (%)",
|
||||||
|
StatPath: "DerivedStats.MemoryPercentage",
|
||||||
|
Color: "green",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,10 @@ package gui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"context"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
|
|
@ -123,46 +123,22 @@ func (gui *Gui) renderContainerStats(mainView *gocui.View, container *commands.C
|
||||||
mainView.Autoscroll = false
|
mainView.Autoscroll = false
|
||||||
mainView.Title = "Stats"
|
mainView.Title = "Stats"
|
||||||
|
|
||||||
stream, err := gui.DockerCommand.Client.ContainerStats(context.Background(), container.ID, true)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return gui.T.NewTask(func(stop chan struct{}) {
|
return gui.T.NewTask(func(stop chan struct{}) {
|
||||||
defer stream.Body.Close()
|
tickChan := time.NewTicker(time.Second)
|
||||||
|
for {
|
||||||
cpuUsageHistory := []float64{}
|
|
||||||
memoryUsageHistory := []float64{}
|
|
||||||
scanner := bufio.NewScanner(stream.Body)
|
|
||||||
for scanner.Scan() {
|
|
||||||
data := scanner.Bytes()
|
|
||||||
var stats commands.ContainerStats
|
|
||||||
json.Unmarshal(data, &stats)
|
|
||||||
|
|
||||||
cpuUsageHistory = append(cpuUsageHistory, stats.CalculateContainerCPUPercentage())
|
|
||||||
if len(cpuUsageHistory) >= 20 {
|
|
||||||
cpuUsageHistory = cpuUsageHistory[1:]
|
|
||||||
}
|
|
||||||
|
|
||||||
memoryUsageHistory = append(memoryUsageHistory, stats.CalculateContainerMemoryUsage())
|
|
||||||
if len(memoryUsageHistory) >= 20 {
|
|
||||||
memoryUsageHistory = memoryUsageHistory[1:]
|
|
||||||
}
|
|
||||||
|
|
||||||
width, _ := mainView.Size()
|
|
||||||
|
|
||||||
contents, err := stats.RenderStats(width, cpuUsageHistory, memoryUsageHistory)
|
|
||||||
if err != nil {
|
|
||||||
gui.createErrorPanel(gui.g, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-stop:
|
case <-stop:
|
||||||
return
|
return
|
||||||
default:
|
case <-tickChan.C:
|
||||||
}
|
width, _ := mainView.Size()
|
||||||
|
|
||||||
gui.reRenderString(gui.g, "main", contents)
|
contents, err := container.RenderStats(width)
|
||||||
|
if err != nil {
|
||||||
|
gui.createErrorPanel(gui.g, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
gui.reRenderString(gui.g, "main", contents)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,36 +2,14 @@ package gui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/jesseduffield/gocui"
|
"github.com/jesseduffield/gocui"
|
||||||
|
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetAttribute gets the gocui color attribute from the string
|
|
||||||
func (gui *Gui) GetAttribute(key string) gocui.Attribute {
|
|
||||||
colorMap := map[string]gocui.Attribute{
|
|
||||||
"default": gocui.ColorDefault,
|
|
||||||
"black": gocui.ColorBlack,
|
|
||||||
"red": gocui.ColorRed,
|
|
||||||
"green": gocui.ColorGreen,
|
|
||||||
"yellow": gocui.ColorYellow,
|
|
||||||
"blue": gocui.ColorBlue,
|
|
||||||
"magenta": gocui.ColorMagenta,
|
|
||||||
"cyan": gocui.ColorCyan,
|
|
||||||
"white": gocui.ColorWhite,
|
|
||||||
"bold": gocui.AttrBold,
|
|
||||||
"reverse": gocui.AttrReverse,
|
|
||||||
"underline": gocui.AttrUnderline,
|
|
||||||
}
|
|
||||||
value, present := colorMap[key]
|
|
||||||
if present {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
return gocui.ColorWhite
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetColor bitwise OR's a list of attributes obtained via the given keys
|
// GetColor bitwise OR's a list of attributes obtained via the given keys
|
||||||
func (gui *Gui) GetColor(keys []string) gocui.Attribute {
|
func (gui *Gui) GetColor(keys []string) gocui.Attribute {
|
||||||
var attribute gocui.Attribute
|
var attribute gocui.Attribute
|
||||||
for _, key := range keys {
|
for _, key := range keys {
|
||||||
attribute = attribute | gui.GetAttribute(key)
|
attribute = attribute | utils.GetGocuiAttribute(key)
|
||||||
}
|
}
|
||||||
return attribute
|
return attribute
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-errors/errors"
|
"github.com/go-errors/errors"
|
||||||
|
"github.com/jesseduffield/gocui"
|
||||||
|
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
)
|
)
|
||||||
|
|
@ -296,3 +297,48 @@ func ApplyTemplate(str string, object interface{}) string {
|
||||||
template.Must(template.New("").Parse(str)).Execute(&buf, object)
|
template.Must(template.New("").Parse(str)).Execute(&buf, object)
|
||||||
return buf.String()
|
return buf.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetGocuiAttribute gets the gocui color attribute from the string
|
||||||
|
func GetGocuiAttribute(key string) gocui.Attribute {
|
||||||
|
colorMap := map[string]gocui.Attribute{
|
||||||
|
"default": gocui.ColorDefault,
|
||||||
|
"black": gocui.ColorBlack,
|
||||||
|
"red": gocui.ColorRed,
|
||||||
|
"green": gocui.ColorGreen,
|
||||||
|
"yellow": gocui.ColorYellow,
|
||||||
|
"blue": gocui.ColorBlue,
|
||||||
|
"magenta": gocui.ColorMagenta,
|
||||||
|
"cyan": gocui.ColorCyan,
|
||||||
|
"white": gocui.ColorWhite,
|
||||||
|
"bold": gocui.AttrBold,
|
||||||
|
"reverse": gocui.AttrReverse,
|
||||||
|
"underline": gocui.AttrUnderline,
|
||||||
|
}
|
||||||
|
value, present := colorMap[key]
|
||||||
|
if present {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return gocui.ColorWhite
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetColorAttribute gets the color attribute from the string
|
||||||
|
func GetColorAttribute(key string) color.Attribute {
|
||||||
|
colorMap := map[string]color.Attribute{
|
||||||
|
"default": color.FgWhite,
|
||||||
|
"black": color.FgBlack,
|
||||||
|
"red": color.FgRed,
|
||||||
|
"green": color.FgGreen,
|
||||||
|
"yellow": color.FgYellow,
|
||||||
|
"blue": color.FgBlue,
|
||||||
|
"magenta": color.FgMagenta,
|
||||||
|
"cyan": color.FgCyan,
|
||||||
|
"white": color.FgWhite,
|
||||||
|
"bold": color.Bold,
|
||||||
|
"underline": color.Underline,
|
||||||
|
}
|
||||||
|
value, present := colorMap[key]
|
||||||
|
if present {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return color.FgWhite
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue