diff --git a/.golangci.yml b/.golangci.yml index 1680c94..643e2c8 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -8,7 +8,7 @@ linters-settings: - default - prefix(github.com/devemio/docker-color-output) varnamelen: - ignore-names: [ i, in, j, m, sb, tt, v ] + ignore-names: [ fn, i, in, j, m, sb, tt, v ] depguard: rules: main: diff --git a/README.md b/README.md index 8abf55c..b2a85c1 100644 --- a/README.md +++ b/README.md @@ -116,3 +116,15 @@ docker compose ps | docker-color-output ``` ![docker compose ps](https://user-images.githubusercontent.com/5787193/93630916-7267dd00-f9f3-11ea-9521-e69152fa86f1.png) + +#### 💡 docker stats + +```bash +ds # alias +``` + +```bash +docker stats [--no-stream] | docker-color-output +``` + +![docker stats](https://github.com/devemio/docker-color-output/assets/5787193/a3134ae9-707b-4ad7-a5ea-765150d535e8) diff --git a/bash/aliases.sh b/bash/aliases.sh index 297a48b..fa912ec 100644 --- a/bash/aliases.sh +++ b/bash/aliases.sh @@ -11,3 +11,7 @@ dps() { dcps() { docker compose ps "$@" | docker-color-output } + +ds() { + docker stats "$@" | docker-color-output +} diff --git a/cmd/cli/main.go b/cmd/cli/main.go index 4fffbd4..a9ea040 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -28,19 +28,16 @@ func run() error { color.SetPalette(color.Palette(cfg.Colors)) - in, err := stdin.Get() - if err != nil { - return err //nolint:wrapcheck - } - - rows, err := app.Run(in) - if err != nil { - return err //nolint:wrapcheck - } - - for _, row := range rows { - stdout.Println(row) - } - - return nil + return stdin.Get(func(rows []string) error { //nolint:wrapcheck + rows, err = app.Run(rows) + if err != nil { + return fmt.Errorf("app: %w", err) + } + + for _, row := range rows { + stdout.Println(row) + } + + return nil + }) } diff --git a/internal/app/app.go b/internal/app/app.go index 942b6a0..417b33b 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -2,6 +2,7 @@ package app import ( "errors" + "fmt" "strings" "github.com/devemio/docker-color-output/internal/cmd" @@ -29,7 +30,7 @@ func Run(in []string) ([]string, error) { command, err := cmd.Parse(header) if err != nil { - return nil, err //nolint:wrapcheck + return nil, fmt.Errorf("cmd: parse: %w", err) } res := make([]string, len(in)) diff --git a/internal/app/app_test.go b/internal/app/app_test.go index 8d56f12..aa48143 100644 --- a/internal/app/app_test.go +++ b/internal/app/app_test.go @@ -44,6 +44,7 @@ func TestRun(t *testing.T) { "docker_images": {in: "docker_images.out", want: "docker_images.out"}, "docker_compose_ps": {in: "docker_compose_ps.out", want: "docker_compose_ps.out"}, "docker_compose_ps_1": {in: "docker_compose_ps_1.out", want: "docker_compose_ps_1.out"}, + "docker_stats": {in: "docker_stats.out", want: "docker_stats.out"}, } for name, tt := range tests { diff --git a/internal/app/const.go b/internal/app/const.go index 366162f..1882b3f 100644 --- a/internal/app/const.go +++ b/internal/app/const.go @@ -1,6 +1,6 @@ package app const ( - Ver = "2.4.0" + Ver = "2.5.0" Name = "docker-color-output" ) diff --git a/internal/app/usage.go b/internal/app/usage.go index d841b27..f5e2e60 100644 --- a/internal/app/usage.go +++ b/internal/app/usage.go @@ -3,19 +3,18 @@ package app import ( - "fmt" - + "github.com/devemio/docker-color-output/internal/stdout" "github.com/devemio/docker-color-output/pkg/color" ) const indent = " " -//nolint:forbidigo func Usage(err error) { - fmt.Println(color.LightRed("💡 Error: " + err.Error())) - fmt.Println("💥 Version: " + color.Green(Ver)) - fmt.Println("👌 Usage:") - fmt.Println(indent + color.Green("docker compose ps") + " [-a] | " + color.Brown(Name)) - fmt.Println(indent + color.Green("docker images") + " [--format] | " + color.Brown(Name)) - fmt.Println(indent + color.Green("docker ps") + " [-a] [--format] | " + color.Brown(Name)) + stdout.Println(color.LightRed("💡 Error: " + err.Error())) + stdout.Println("💥 Version: " + color.Green(Ver)) + stdout.Println("👌 Usage:") + stdout.Println(indent + color.Green("docker compose ps") + " [-a] | " + color.Brown(Name)) + stdout.Println(indent + color.Green("docker images") + " [--format] | " + color.Brown(Name)) + stdout.Println(indent + color.Green("docker ps") + " [-a] [--format] | " + color.Brown(Name)) + stdout.Println(indent + color.Green("docker stats") + " [--no-stream] | " + color.Brown(Name)) } diff --git a/internal/cmd/docker_stats.go b/internal/cmd/docker_stats.go new file mode 100644 index 0000000..a4332bc --- /dev/null +++ b/internal/cmd/docker_stats.go @@ -0,0 +1,154 @@ +package cmd + +import ( + "strings" + + "github.com/devemio/docker-color-output/internal/layout" + "github.com/devemio/docker-color-output/pkg/color" + "github.com/devemio/docker-color-output/pkg/util/number" +) + +const ( + DockerStatsContainerID = "CONTAINER ID" + DockerStatsName = "NAME" + DockerStatsCPUPercent = "CPU %" + DockerStatsMemUsage = "MEM USAGE / LIMIT" + DockerStatsMemPercent = "MEM %" + DockerStatsNetIO = "NET I/O" + DockerStatsBlockIO = "BLOCK I/O" + DockerStatsPIDs = "PIDS" +) + +const ( + cpuPercentThresholdMedium = 50 + cpuPercentThresholdHigh = 90 + + memPercentThresholdMedium = 50 + memPercentThresholdHigh = 90 + + netIOThresholdHigh = 10 + + blockIOThresholdHigh = 10 + + pidsThresholdHigh = 100 +) + +type DockerStats struct{} + +func (c *DockerStats) Columns() []string { + return []string{ + DockerStatsContainerID, // + DockerStatsName, // + DockerStatsCPUPercent, // + DockerStatsMemUsage, // + DockerStatsMemPercent, // + DockerStatsNetIO, // + DockerStatsBlockIO, // + DockerStatsPIDs, // + } +} + +func (c *DockerStats) Format(rows layout.Row, col layout.Column) string { + v := string(rows[col]) + + switch col { + case DockerStatsContainerID: + return c.ContainerID(v) + case DockerStatsName: + return c.Name(v) + case DockerStatsCPUPercent: + return c.CPUPercent(v) + case DockerStatsMemUsage: + return c.MemUsage(v) + case DockerStatsMemPercent: + return c.MemPercent(v) + case DockerStatsNetIO: + return c.NetIO(v) + case DockerStatsBlockIO: + return c.BlockIO(v) + case DockerStatsPIDs: + return c.PIDs(v) + default: + return v + } +} + +func (*DockerStats) ContainerID(v string) string { + return color.DarkGray(v) +} + +func (c *DockerStats) Name(v string) string { + return color.White(v) +} + +func (c *DockerStats) CPUPercent(v string) string { + percent := number.ParseFloat(v) + + switch { + case percent >= cpuPercentThresholdHigh: + return color.Red(v) + case percent >= cpuPercentThresholdMedium: + return color.Brown(v) + default: + return v + } +} + +func (c *DockerStats) MemUsage(v string) string { + parts := strings.Split(v, "/") + + return parts[0] + color.DarkGray("/"+parts[1]) +} + +func (c *DockerStats) MemPercent(v string) string { + percent := number.ParseFloat(v) + + switch { + case percent >= memPercentThresholdHigh: + return color.Red(v) + case percent >= memPercentThresholdMedium: + return color.Brown(v) + default: + return v + } +} + +func (*DockerStats) NetIO(v string) string { + parts := strings.Split(v, "/") + + for i := range parts { + if strings.Contains(parts[i], "GB") { + if number.ParseFloat(parts[i]) >= netIOThresholdHigh { + parts[i] = color.Red(parts[i]) + } else { + parts[i] = color.Brown(parts[i]) + } + } + } + + return parts[0] + color.DarkGray("/") + parts[1] +} + +func (*DockerStats) BlockIO(v string) string { + parts := strings.Split(v, "/") + + for i := range parts { + if strings.Contains(parts[i], "GB") { + if number.ParseFloat(parts[i]) >= blockIOThresholdHigh { + parts[i] = color.Red(parts[i]) + } else { + parts[i] = color.Brown(parts[i]) + } + } + } + + return parts[0] + color.DarkGray("/") + parts[1] +} + +func (*DockerStats) PIDs(v string) string { + if number.ParseFloat(v) >= pidsThresholdHigh { + return color.Red(v) + } + + return color.LightCyan(v) +} diff --git a/internal/cmd/parse.go b/internal/cmd/parse.go index fa04912..56a6703 100644 --- a/internal/cmd/parse.go +++ b/internal/cmd/parse.go @@ -30,5 +30,10 @@ func Parse(header layout.Header) (Command, error) { //nolint:ireturn return composePs, nil } + stats := &DockerStats{} + if util.Intersect(columns, stats.Columns()) { + return stats, nil + } + return nil, ErrInvalidFirstLine } diff --git a/internal/config/config.go b/internal/config/config.go index 461b293..9cf6299 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -40,7 +40,7 @@ func Get() (Config, error) { if *cfgPath != "" { data, err := os.ReadFile(*cfgPath) if err != nil { - return Config{}, fmt.Errorf("read file: %w", err) + return Config{}, fmt.Errorf("read: %w", err) } if err = json.Unmarshal(data, &cfg); err != nil { diff --git a/internal/stdin/stdin.go b/internal/stdin/stdin.go index 343944c..f9a0dfa 100644 --- a/internal/stdin/stdin.go +++ b/internal/stdin/stdin.go @@ -2,32 +2,58 @@ package stdin import ( "bufio" + "bytes" "errors" + "fmt" "os" + + "github.com/devemio/docker-color-output/internal/stdout" ) -var ErrNoStdin = errors.New("no stdin") +var ErrEmpty = errors.New("empty") + +const rowsCap = 20 + +// clsBytes contains bytes to clear +// the screen for nix systems. +var clsBytes = []byte("\033[2J\033[H") //nolint:gochecknoglobals -func Get() ([]string, error) { +func Get(fn func(rows []string) error) error { fi, err := os.Stdin.Stat() if err != nil { - return nil, err //nolint:wrapcheck + return fmt.Errorf("stdin: %w", err) } if fi.Mode()&os.ModeNamedPipe == 0 && fi.Size() <= 0 { - return nil, ErrNoStdin + return fmt.Errorf("stdin: %w", ErrEmpty) } - var res []string + rows := make([]string, 0, rowsCap) + + scanner := bufio.NewScanner(os.Stdin) + for scanner.Scan() { + row, found := scanner.Bytes(), false //nolint:wastedassign + + if row, found = bytes.CutPrefix(row, clsBytes); found && len(rows) > 0 { + stdout.Print(string(clsBytes)) + + if err = fn(rows); err != nil { + return err + } + + rows = rows[:0] + } + + rows = append(rows, string(row)) + } - s := bufio.NewScanner(os.Stdin) - for s.Scan() { - res = append(res, s.Text()) + if err = scanner.Err(); err != nil { + return fmt.Errorf("stdin: scan: %w", err) } - if err = s.Err(); err != nil { - return nil, err //nolint:wrapcheck + if err = fn(rows); err != nil { + return err } - return res, nil + return nil } diff --git a/internal/stdout/stdout.go b/internal/stdout/stdout.go index 9fcadeb..69c5550 100644 --- a/internal/stdout/stdout.go +++ b/internal/stdout/stdout.go @@ -4,6 +4,10 @@ import ( "fmt" ) +func Print(in string) { + fmt.Print(in) //nolint:forbidigo +} + func Println(in string) { fmt.Println(in) //nolint:forbidigo } diff --git a/test/data/in/docker_stats.out b/test/data/in/docker_stats.out new file mode 100644 index 0000000..d943291 --- /dev/null +++ b/test/data/in/docker_stats.out @@ -0,0 +1,2 @@ +CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS +e39ba801c0e1 nginx 90.00% 13.53MiB / 7.657GiB 50.17% 10.8MB / 42.1GB 0B / 12.3kB 17 diff --git a/test/data/out/docker_stats.out b/test/data/out/docker_stats.out new file mode 100644 index 0000000..e18bd3b --- /dev/null +++ b/test/data/out/docker_stats.out @@ -0,0 +1,2 @@ +CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS +e39ba801c0e1 nginx 90.00% 13.53MiB / 7.657GiB 50.17% 10.8MB / 42.1GB 0B / 12.3kB 17