189 lines
4.3 KiB
Go
189 lines
4.3 KiB
Go
// SPDX-License-Identifier: Apache-2.0
|
|
// Copyright Authors of K9s
|
|
|
|
package render
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/derailed/k9s/internal/client"
|
|
"github.com/derailed/k9s/internal/model1"
|
|
"github.com/derailed/tcell/v2"
|
|
"github.com/derailed/tview"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
)
|
|
|
|
var (
|
|
totalRx = regexp.MustCompile(`Total:\s+([0-9.]+)\ssecs`)
|
|
reqRx = regexp.MustCompile(`Requests/sec:\s+([0-9.]+)`)
|
|
okRx = regexp.MustCompile(`\[2\d{2}\]\s+(\d+)\s+responses`)
|
|
errRx = regexp.MustCompile(`\[[45]\d{2}\]\s+(\d+)\s+responses`)
|
|
toastRx = regexp.MustCompile(`Error distribution`)
|
|
)
|
|
|
|
// Benchmark renders a benchmarks to screen.
|
|
type Benchmark struct {
|
|
Base
|
|
}
|
|
|
|
// ColorerFunc colors a resource row.
|
|
func (Benchmark) ColorerFunc() model1.ColorerFunc {
|
|
return func(ns string, h model1.Header, re *model1.RowEvent) tcell.Color {
|
|
if !model1.IsValid(ns, h, re.Row) {
|
|
return model1.ErrColor
|
|
}
|
|
|
|
return tcell.ColorPaleGreen
|
|
}
|
|
}
|
|
|
|
// Header returns a header row.
|
|
func (Benchmark) Header(string) model1.Header {
|
|
return model1.Header{
|
|
model1.HeaderColumn{Name: "NAMESPACE"},
|
|
model1.HeaderColumn{Name: "NAME"},
|
|
model1.HeaderColumn{Name: "STATUS"},
|
|
model1.HeaderColumn{Name: "TIME"},
|
|
model1.HeaderColumn{Name: "REQ/S", Attrs: model1.Attrs{Align: tview.AlignRight}},
|
|
model1.HeaderColumn{Name: "2XX", Attrs: model1.Attrs{Align: tview.AlignRight}},
|
|
model1.HeaderColumn{Name: "4XX/5XX", Attrs: model1.Attrs{Align: tview.AlignRight}},
|
|
model1.HeaderColumn{Name: "REPORT"},
|
|
model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}},
|
|
model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}},
|
|
}
|
|
}
|
|
|
|
// Render renders a K8s resource to screen.
|
|
func (b Benchmark) Render(o any, ns string, r *model1.Row) error {
|
|
bench, ok := o.(BenchInfo)
|
|
if !ok {
|
|
return fmt.Errorf("no benchmarks available %T", o)
|
|
}
|
|
|
|
data, err := b.readFile(bench.Path)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to load bench file %s", bench.Path)
|
|
}
|
|
|
|
r.ID = bench.Path
|
|
r.Fields = make(model1.Fields, len(b.Header(ns)))
|
|
if err := b.initRow(r.Fields, bench.File); err != nil {
|
|
return err
|
|
}
|
|
b.augmentRow(r.Fields, data)
|
|
r.Fields[8] = AsStatus(b.diagnose(ns, r.Fields))
|
|
|
|
return nil
|
|
}
|
|
|
|
// Happy returns true if resource is happy, false otherwise.
|
|
func (Benchmark) diagnose(ns string, ff model1.Fields) error {
|
|
statusCol := 3
|
|
if !client.IsAllNamespaces(ns) {
|
|
statusCol--
|
|
}
|
|
|
|
if len(ff) < statusCol {
|
|
return nil
|
|
}
|
|
if ff[statusCol] != "pass" {
|
|
return errors.New("failed benchmark")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Helpers...
|
|
|
|
func (Benchmark) readFile(file string) (string, error) {
|
|
data, err := os.ReadFile(file)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return string(data), nil
|
|
}
|
|
|
|
func (Benchmark) initRow(row model1.Fields, f os.FileInfo) error {
|
|
tokens := strings.Split(f.Name(), "_")
|
|
if len(tokens) < 2 {
|
|
return fmt.Errorf("invalid file name %s", f.Name())
|
|
}
|
|
row[0] = tokens[0]
|
|
row[1] = tokens[1]
|
|
row[7] = f.Name()
|
|
row[9] = ToAge(metav1.Time{Time: f.ModTime()})
|
|
|
|
return nil
|
|
}
|
|
|
|
func (b Benchmark) augmentRow(fields model1.Fields, data string) {
|
|
if data == "" {
|
|
return
|
|
}
|
|
|
|
col := 2
|
|
fields[col] = "pass"
|
|
mf := toastRx.FindAllStringSubmatch(data, 1)
|
|
if len(mf) > 0 {
|
|
fields[col] = "fail"
|
|
}
|
|
col++
|
|
|
|
mt := totalRx.FindAllStringSubmatch(data, 1)
|
|
if len(mt) > 0 {
|
|
fields[col] = mt[0][1]
|
|
}
|
|
col++
|
|
|
|
mr := reqRx.FindAllStringSubmatch(data, 1)
|
|
if len(mr) > 0 {
|
|
fields[col] = mr[0][1]
|
|
}
|
|
col++
|
|
|
|
ms := okRx.FindAllStringSubmatch(data, -1)
|
|
fields[col] = b.countReq(ms)
|
|
col++
|
|
|
|
me := errRx.FindAllStringSubmatch(data, -1)
|
|
fields[col] = b.countReq(me)
|
|
}
|
|
|
|
func (Benchmark) countReq(rr [][]string) string {
|
|
if len(rr) == 0 {
|
|
return "0"
|
|
}
|
|
|
|
var sum int
|
|
for _, m := range rr {
|
|
if m, err := strconv.Atoi(m[1]); err == nil {
|
|
sum += m
|
|
}
|
|
}
|
|
return AsThousands(int64(sum))
|
|
}
|
|
|
|
// BenchInfo represents benchmark run info.
|
|
type BenchInfo struct {
|
|
File os.FileInfo
|
|
Path string
|
|
}
|
|
|
|
// GetObjectKind returns a schema object.
|
|
func (BenchInfo) GetObjectKind() schema.ObjectKind {
|
|
return nil
|
|
}
|
|
|
|
// DeepCopyObject returns a container copy.
|
|
func (b BenchInfo) DeepCopyObject() runtime.Object {
|
|
return b
|
|
}
|