diff --git a/internal/client/metrics.go b/internal/client/metrics.go index 7f02787e..c88ccb43 100644 --- a/internal/client/metrics.go +++ b/internal/client/metrics.go @@ -48,29 +48,6 @@ func NewMetricsServer(c Connection) *MetricsServer { } } -// NodesMetrics retrieves metrics for a given set of nodes. -func (m *MetricsServer) NodesMetrics(nodes *v1.NodeList, metrics *mv1beta1.NodeMetricsList, mmx NodesMetrics) { - if nodes == nil || metrics == nil { - return - } - - for _, no := range nodes.Items { - mmx[no.Name] = NodeMetrics{ - AvailCPU: no.Status.Allocatable.Cpu().MilliValue(), - AvailMEM: ToMB(no.Status.Allocatable.Memory().Value()), - TotalCPU: no.Status.Capacity.Cpu().MilliValue(), - TotalMEM: ToMB(no.Status.Capacity.Memory().Value()), - } - } - for _, c := range metrics.Items { - if mx, ok := mmx[c.Name]; ok { - mx.CurrentCPU = c.Usage.Cpu().MilliValue() - mx.CurrentMEM = ToMB(c.Usage.Memory().Value()) - mmx[c.Name] = mx - } - } -} - // ClusterLoad retrieves all cluster nodes metrics. func (m *MetricsServer) ClusterLoad(nos *v1.NodeList, nmx *mv1beta1.NodeMetricsList, mx *ClusterMetrics) error { if nos == nil || nmx == nil { @@ -79,26 +56,32 @@ func (m *MetricsServer) ClusterLoad(nos *v1.NodeList, nmx *mv1beta1.NodeMetricsL nodeMetrics := make(NodesMetrics, len(nos.Items)) for _, no := range nos.Items { nodeMetrics[no.Name] = NodeMetrics{ - AvailCPU: no.Status.Allocatable.Cpu().MilliValue(), - AvailMEM: ToMB(no.Status.Allocatable.Memory().Value()), + AllocatableCPU: no.Status.Allocatable.Cpu().MilliValue(), + AllocatableMEM: no.Status.Allocatable.Memory().Value(), + AllocatableEphemeral: no.Status.Allocatable.StorageEphemeral().Value(), } } for _, mx := range nmx.Items { - if m, ok := nodeMetrics[mx.Name]; ok { - m.CurrentCPU = mx.Usage.Cpu().MilliValue() - m.CurrentMEM = ToMB(mx.Usage.Memory().Value()) - nodeMetrics[mx.Name] = m + if node, ok := nodeMetrics[mx.Name]; ok { + node.CurrentCPU = mx.Usage.Cpu().MilliValue() + node.CurrentMEM = mx.Usage.Memory().Value() + node.CurrentEphemeral = mx.Usage.StorageEphemeral().Value() + nodeMetrics[mx.Name] = node } } - var cpu, tcpu, mem, tmem float64 + var ccpu, cmem, ceph, tcpu, tmem, teph int64 for _, mx := range nodeMetrics { - cpu += float64(mx.CurrentCPU) - tcpu += float64(mx.AvailCPU) - mem += mx.CurrentMEM - tmem += mx.AvailMEM + ccpu += mx.CurrentCPU + cmem += mx.CurrentMEM + ceph += mx.CurrentEphemeral + tcpu += mx.AllocatableCPU + tmem += mx.AllocatableMEM + teph += mx.AllocatableEphemeral } - mx.PercCPU, mx.PercMEM = toPerc(cpu, tcpu), toPerc(mem, tmem) + mx.PercCPU, mx.PercMEM, mx.PercEphemeral = ToPercentage(ccpu, tcpu), + ToPercentage(cmem, tmem), + ToPercentage(ceph, teph) return nil } @@ -118,6 +101,33 @@ func (m *MetricsServer) checkAccess(ns, gvr, msg string) error { return nil } +// NodesMetrics retrieves metrics for a given set of nodes. +func (m *MetricsServer) NodesMetrics(nodes *v1.NodeList, metrics *mv1beta1.NodeMetricsList, mmx NodesMetrics) { + if nodes == nil || metrics == nil { + return + } + + for _, no := range nodes.Items { + mmx[no.Name] = NodeMetrics{ + AllocatableCPU: no.Status.Allocatable.Cpu().MilliValue(), + AllocatableMEM: ToMB(no.Status.Allocatable.Memory().Value()), + AllocatableEphemeral: ToMB(no.Status.Allocatable.StorageEphemeral().Value()), + TotalCPU: no.Status.Capacity.Cpu().MilliValue(), + TotalMEM: ToMB(no.Status.Capacity.Memory().Value()), + TotalEphemeral: ToMB(no.Status.Capacity.StorageEphemeral().Value()), + } + } + for _, c := range metrics.Items { + if mx, ok := mmx[c.Name]; ok { + mx.CurrentCPU = c.Usage.Cpu().MilliValue() + mx.CurrentMEM = ToMB(c.Usage.Memory().Value()) + mx.AvailableCPU = mx.AllocatableCPU - mx.CurrentCPU + mx.AvailableMEM = mx.AllocatableMEM - mx.CurrentMEM + mmx[c.Name] = mx + } + } +} + // FetchNodesMetrics return all metrics for nodes. func (m *MetricsServer) FetchNodesMetrics() (*mv1beta1.NodeMetricsList, error) { const msg = "user is not authorized to list node metrics" @@ -237,19 +247,20 @@ func (m *MetricsServer) PodsMetrics(pods *mv1beta1.PodMetricsList, mmx PodsMetri } } -// 0--------------------------------------------------------------------------- +// ---------------------------------------------------------------------------- // Helpers... const megaByte = 1024 * 1024 // ToMB converts bytes to megabytes. -func ToMB(v int64) float64 { - return float64(v) / megaByte +func ToMB(v int64) int64 { + return v / megaByte } -func toPerc(v1, v2 float64) float64 { +// ToPercentageentage computes percentage. +func ToPercentage(v1, v2 int64) int { if v2 == 0 { return 0 } - return math.Round((v1 / v2) * 100) + return int(math.Floor((float64(v1) / float64(v2)) * 100)) } diff --git a/internal/client/metrics_test.go b/internal/client/metrics_test.go index 4244f7e8..32377cee 100644 --- a/internal/client/metrics_test.go +++ b/internal/client/metrics_test.go @@ -12,6 +12,38 @@ import ( v1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" ) +func TestToPercentage(t *testing.T) { + uu := []struct { + v1, v2 int64 + e int + }{ + {0, 0, 0}, + {100, 200, 50}, + {200, 100, 200}, + {224, 4000, 5}, + } + + for _, u := range uu { + assert.Equal(t, u.e, client.ToPercentage(u.v1, u.v2)) + } +} + +func TestToMB(t *testing.T) { + const mb = 1024 * 1024 + uu := []struct { + v int64 + e int64 + }{ + {0, 0}, + {2 * mb, 2}, + {10 * mb, 10}, + } + + for _, u := range uu { + assert.Equal(t, u.e, client.ToMB(u.v)) + } +} + func TestPodsMetrics(t *testing.T) { uu := map[string]struct { metrics *mv1beta1.PodMetricsList @@ -32,8 +64,8 @@ func TestPodsMetrics(t *testing.T) { eSize: 2, e: client.PodsMetrics{ "default/p1": client.PodMetrics{ - CurrentCPU: int64(3000), - CurrentMEM: float64(12288), + CurrentCPU: 3000, + CurrentMEM: 12288, }, }, }, @@ -107,8 +139,8 @@ func TestNodesMetrics(t *testing.T) { "ok": { nodes: &v1.NodeList{ Items: []v1.Node{ - makeNode("n1", "32", "128Gi", "50m", "2Mi"), - makeNode("n2", "8", "4Gi", "50m", "10Mi"), + makeNode("n1", "32", "128Gi", "32", "128Gi"), + makeNode("n2", "8", "4Gi", "8", "4Gi"), }, }, metrics: &v1beta1.NodeMetricsList{ @@ -120,13 +152,15 @@ func TestNodesMetrics(t *testing.T) { eSize: 2, e: client.NodesMetrics{ "n1": client.NodeMetrics{ - TotalCPU: int64(32000), - TotalMEM: float64(131072), - AvailCPU: int64(50), - AvailMEM: float64(2), + TotalCPU: 32000, + TotalMEM: 131072, + AllocatableCPU: 32000, + AllocatableMEM: 131072, + AvailableCPU: 22000, + AvailableMEM: 122880, CurrentMetrics: client.CurrentMetrics{ - CurrentCPU: int64(10000), - CurrentMEM: float64(8192), + CurrentCPU: 10000, + CurrentMEM: 8192, }, }, }, diff --git a/internal/client/types.go b/internal/client/types.go index 76467127..6a115db4 100644 --- a/internal/client/types.go +++ b/internal/client/types.go @@ -105,8 +105,7 @@ type Connection interface { // CurrentMetrics tracks current cpu/mem. type CurrentMetrics struct { - CurrentCPU int64 - CurrentMEM float64 + CurrentCPU, CurrentMEM, CurrentEphemeral int64 } // PodMetrics represent an aggregation of all pod containers metrics. @@ -115,16 +114,15 @@ type PodMetrics CurrentMetrics // NodeMetrics describes raw node metrics. type NodeMetrics struct { CurrentMetrics - AvailCPU int64 - AvailMEM float64 - TotalCPU int64 - TotalMEM float64 + + AllocatableCPU, AllocatableMEM, AllocatableEphemeral int64 + AvailableCPU, AvailableMEM, AvailableEphemeral int64 + TotalCPU, TotalMEM, TotalEphemeral int64 } // ClusterMetrics summarizes total node metrics as percentages. type ClusterMetrics struct { - PercCPU float64 - PercMEM float64 + PercCPU, PercMEM, PercEphemeral int } // NodesMetrics tracks usage metrics per nodes. diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 0f940013..cd34777f 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -297,6 +297,10 @@ var expectedConfig = `k9s: - kube-system view: active: ctx + thresholds: + cpu: 80 + memory: 80 + disk: 80 ` var resetConfig = `k9s: @@ -316,4 +320,8 @@ var resetConfig = `k9s: - default view: active: po + thresholds: + cpu: 80 + memory: 80 + disk: 80 ` diff --git a/internal/config/k9s.go b/internal/config/k9s.go index ae439edf..6f416920 100644 --- a/internal/config/k9s.go +++ b/internal/config/k9s.go @@ -20,6 +20,7 @@ type K9s struct { CurrentCluster string `yaml:"currentCluster"` FullScreenLogs bool `yaml:"fullScreenLogs"` Clusters map[string]*Cluster `yaml:"clusters,omitempty"` + Thresholds *Threshold `yaml:"thresholds"` manualRefreshRate int manualHeadless *bool manualReadOnly *bool @@ -34,6 +35,7 @@ func NewK9s() *K9s { LogBufferSize: defaultLogBufferSize, LogRequestSize: defaultLogRequestSize, Clusters: make(map[string]*Cluster), + Thresholds: newThreshold(), } } @@ -133,12 +135,16 @@ func (k *K9s) checkClusters(ks KubeSettings) { // Validate the current configuration. func (k *K9s) Validate(c client.Connection, ks KubeSettings) { k.validateDefaults() - if k.Clusters == nil { k.Clusters = map[string]*Cluster{} } k.checkClusters(ks) + if k.Thresholds == nil { + k.Thresholds = newThreshold() + } + k.Thresholds.Validate(c, ks) + if ctx, err := ks.CurrentContextName(); err == nil && len(k.CurrentContext) == 0 { k.CurrentContext = ctx k.CurrentCluster = "" diff --git a/internal/config/styles.go b/internal/config/styles.go index 8504600a..263961dd 100644 --- a/internal/config/styles.go +++ b/internal/config/styles.go @@ -213,6 +213,10 @@ func newCharts() Charts { ChartBgColor: "default", DefaultDialColors: Colors{Color("palegreen"), Color("orangered")}, DefaultChartColors: Colors{Color("palegreen"), Color("orangered")}, + ResourceColors: map[string]Colors{ + "cpu": Colors{Color("dodgerblue"), Color("darkslateblue")}, + "mem": Colors{Color("yellow"), Color("goldenrod")}, + }, } } func newViews() Views { diff --git a/internal/config/threshold.go b/internal/config/threshold.go new file mode 100644 index 00000000..445603ab --- /dev/null +++ b/internal/config/threshold.go @@ -0,0 +1,54 @@ +package config + +import ( + "github.com/derailed/k9s/internal/client" +) + +const ( + defaultCPU = 80 + defaultMEM = 80 + defaultDisk = 70 +) + +// Threshold tracks threshold to alert user when excided. +type Threshold struct { + CPU int `yaml:"cpu"` + Memory int `yaml:"memory"` + Disk int `yaml:"disk"` +} + +func newThreshold() *Threshold { + return &Threshold{ + CPU: defaultCPU, + Memory: defaultMEM, + Disk: defaultMEM, + } +} + +// Validate a namespace is setup correctly +func (t *Threshold) Validate(c client.Connection, ks KubeSettings) { + if t.CPU == 0 || t.CPU > 100 { + t.CPU = defaultCPU + } + if t.Memory == 0 || t.Memory > 100 { + t.Memory = defaultMEM + } + if t.Disk == 0 || t.Disk > 100 { + t.Disk = defaultDisk + } +} + +// ExceedsCPUPerc returns true if current metrics exceeds threshold or false otherwise. +func (t *Threshold) ExceedsCPUPerc(p int) bool { + return p >= t.CPU +} + +// ExceedsMemoryPerc returns true if current metrics exceeds threshold or false otherwise. +func (t *Threshold) ExceedsMemoryPerc(p int) bool { + return p >= t.Memory +} + +// ExceedsDiskPerc returns true if current metrics exceeds threshold or false otherwise. +func (t *Threshold) ExceedsDiskPerc(p int) bool { + return p >= t.Disk +} diff --git a/internal/health/check.go b/internal/health/check.go index b3713bfd..14a56c12 100644 --- a/internal/health/check.go +++ b/internal/health/check.go @@ -24,7 +24,7 @@ func NewCheck(gvr string) *Check { } // Set sets a health metric. -func (c *Check) Set(l Level, v int) { +func (c *Check) Set(l Level, v int64) { c.Counts[l] = v } @@ -34,12 +34,12 @@ func (c *Check) Inc(l Level) { } // Total stores a metric total. -func (c *Check) Total(n int) { +func (c *Check) Total(n int64) { c.Counts[Corpus] = n } // Tally retrieves a given health metric. -func (c *Check) Tally(l Level) int { +func (c *Check) Tally(l Level) int64 { return c.Counts[l] } diff --git a/internal/health/check_test.go b/internal/health/check_test.go index f0a40b82..0cdd2703 100644 --- a/internal/health/check_test.go +++ b/internal/health/check_test.go @@ -13,14 +13,14 @@ func TestCheck(t *testing.T) { c := health.NewCheck("test") n := 0 for i := 0; i < 10; i++ { - c.Inc(health.OK) + c.Inc(health.S1) cc = append(cc, c) n++ } - c.Total(n) + c.Total(int64(n)) assert.Equal(t, 10, len(cc)) - assert.Equal(t, 10, c.Tally(health.Corpus)) - assert.Equal(t, 10, c.Tally(health.OK)) - assert.Equal(t, 0, c.Tally(health.Toast)) + assert.Equal(t, int64(10), c.Tally(health.Corpus)) + assert.Equal(t, int64(10), c.Tally(health.S1)) + assert.Equal(t, int64(0), c.Tally(health.S2)) } diff --git a/internal/health/types.go b/internal/health/types.go index 63571ea8..4bb5de21 100644 --- a/internal/health/types.go +++ b/internal/health/types.go @@ -10,14 +10,14 @@ const ( // Corpus tracks total health. Corpus - // OK tracks healhy. - OK + // S1 tracks series 1. + S1 - // Warn tracks health warnings. - Warn + // S2 tracks series 2. + S2 - // Toast tracks unhealties. - Toast + // S3 tracks series 3. + S3 ) // Message represents a health message. @@ -32,7 +32,7 @@ type Message struct { type Messages []Message // Counts tracks health counts by category. -type Counts map[Level]int +type Counts map[Level]int64 // Vital tracks a resource vitals. type Vital struct { diff --git a/internal/model/cluster_info.go b/internal/model/cluster_info.go index 7f4a2da2..01198014 100644 --- a/internal/model/cluster_info.go +++ b/internal/model/cluster_info.go @@ -3,7 +3,6 @@ package model import ( "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/dao" - "github.com/derailed/k9s/internal/render" ) // ClusterInfoListener registers a listener for model changes. @@ -20,31 +19,35 @@ const NA = "n/a" // ClusterMeta represents cluster meta data. type ClusterMeta struct { - Context, Cluster string - User string - K9sVer, K8sVer string - Cpu, Mem float64 + Context, Cluster string + User string + K9sVer, K8sVer string + Cpu, Mem, Ephemeral int } // NewClusterMeta returns a new instance. func NewClusterMeta() ClusterMeta { return ClusterMeta{ - Context: NA, - Cluster: NA, - User: NA, - K9sVer: NA, - K8sVer: NA, - Cpu: 0, - Mem: 0, + Context: NA, + Cluster: NA, + User: NA, + K9sVer: NA, + K8sVer: NA, + Cpu: 0, + Mem: 0, + Ephemeral: 0, } } // Deltas diffs cluster meta return true if different, false otherwise. func (c ClusterMeta) Deltas(n ClusterMeta) bool { - if render.AsPerc(c.Cpu) != render.AsPerc(n.Cpu) { + if c.Cpu != n.Cpu { return true } - if render.AsPerc(c.Mem) != render.AsPerc(n.Mem) { + if c.Mem != n.Mem { + return true + } + if c.Ephemeral != n.Ephemeral { return true } @@ -89,7 +92,7 @@ func (c *ClusterInfo) Refresh() { var mx client.ClusterMetrics if err := c.cluster.Metrics(&mx); err == nil { - data.Cpu, data.Mem = mx.PercCPU, mx.PercMEM + data.Cpu, data.Mem, data.Ephemeral = mx.PercCPU, mx.PercMEM, mx.PercEphemeral } if c.data.Deltas(data) { diff --git a/internal/model/pulse_health.go b/internal/model/pulse_health.go index 82a98604..d7ddf87a 100644 --- a/internal/model/pulse_health.go +++ b/internal/model/pulse_health.go @@ -3,7 +3,6 @@ package model import ( "context" "fmt" - "math" "time" "github.com/derailed/k9s/internal/client" @@ -65,21 +64,38 @@ func (h *PulseHealth) List(ctx context.Context, ns string) ([]runtime.Object, er func (h *PulseHealth) checkMetrics() (health.Checks, error) { dial := client.DialMetrics(h.factory.Client()) + + nn, err := dao.FetchNodes(h.factory, "") + if err != nil { + return nil, err + } + nmx, err := dial.FetchNodesMetrics() if err != nil { log.Error().Err(err).Msgf("Fetching metrics") return nil, err } - var cpu, mem float64 - for _, mx := range nmx.Items { - cpu += float64(mx.Usage.Cpu().MilliValue()) - mem += client.ToMB(mx.Usage.Memory().Value()) + mx := make(client.NodesMetrics, len(nn.Items)) + dial.NodesMetrics(nn, nmx, mx) + + var ccpu, cmem, acpu, amem, tcpu, tmem int64 + for _, m := range mx { + ccpu += m.CurrentCPU + cmem += m.CurrentMEM + acpu += m.AllocatableCPU + amem += m.AllocatableMEM + tcpu += m.TotalCPU + tmem += m.TotalMEM } c1 := health.NewCheck("cpu") - c1.Set(health.OK, int(math.Round(cpu))) + c1.Set(health.S1, ccpu) + c1.Set(health.S2, acpu) + c1.Set(health.S3, tcpu) c2 := health.NewCheck("mem") - c2.Set(health.OK, int(math.Round(mem))) + c2.Set(health.S1, cmem) + c2.Set(health.S2, amem) + c2.Set(health.S3, tmem) return health.Checks{c1, c2}, nil } @@ -100,16 +116,16 @@ func (h *PulseHealth) check(ctx context.Context, ns, gvr string) (*health.Check, } c := health.NewCheck(gvr) - c.Total(len(oo)) + c.Total(int64(len(oo))) rr, re := make(render.Rows, len(oo)), meta.Renderer for i, o := range oo { if err := re.Render(o, ns, &rr[i]); err != nil { return nil, err } if !render.Happy(ns, re.Header(ns), rr[i]) { - c.Inc(health.Toast) + c.Inc(health.S2) } else { - c.Inc(health.OK) + c.Inc(health.S1) } } diff --git a/internal/render/benchmark.go b/internal/render/benchmark.go index c3e6feba..6c884714 100644 --- a/internal/render/benchmark.go +++ b/internal/render/benchmark.go @@ -12,8 +12,6 @@ import ( "github.com/derailed/k9s/internal/client" "github.com/derailed/tview" "github.com/gdamore/tcell" - "golang.org/x/text/language" - "golang.org/x/text/message" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" ) @@ -163,13 +161,7 @@ func (Benchmark) countReq(rr [][]string) string { sum += m } } - return asNum(sum) -} - -// AsNumb prints a number with thousand separator. -func asNum(n int) string { - p := message.NewPrinter(language.English) - return p.Sprintf("%d", n) + return AsThousands(int64(sum)) } // BenchInfo represents benchmark run info. diff --git a/internal/render/container.go b/internal/render/container.go index 0f720e2a..e4b68907 100644 --- a/internal/render/container.go +++ b/internal/render/container.go @@ -6,6 +6,7 @@ import ( "strconv" "strings" + "github.com/derailed/k9s/internal/client" "github.com/derailed/tview" "github.com/gdamore/tcell" v1 "k8s.io/api/core/v1" @@ -142,7 +143,7 @@ func gatherMetrics(co *v1.Container, mx *mv1beta1.ContainerMetrics) (c, p, l met } cpu := mx.Usage.Cpu().MilliValue() - mem := ToMB(mx.Usage.Memory().Value()) + mem := client.ToMB(mx.Usage.Memory().Value()) c = metric{ cpu: ToMillicore(cpu), mem: ToMi(mem), @@ -150,18 +151,18 @@ func gatherMetrics(co *v1.Container, mx *mv1beta1.ContainerMetrics) (c, p, l met rcpu, rmem := containerResources(*co) if rcpu != nil { - p.cpu = AsPerc(toPerc(float64(cpu), float64(rcpu.MilliValue()))) + p.cpu = IntToStr(client.ToPercentage(cpu, rcpu.MilliValue())) } if rmem != nil { - p.mem = AsPerc(toPerc(mem, ToMB(rmem.Value()))) + p.mem = IntToStr(client.ToPercentage(mem, client.ToMB(rmem.Value()))) } lcpu, lmem := containerLimits(*co) if lcpu != nil { - l.cpu = AsPerc(toPerc(float64(cpu), float64(lcpu.MilliValue()))) + l.cpu = IntToStr(client.ToPercentage(cpu, lcpu.MilliValue())) } if lmem != nil { - l.mem = AsPerc(toPerc(mem, ToMB(lmem.Value()))) + l.mem = IntToStr(client.ToPercentage(mem, client.ToMB(lmem.Value()))) } return diff --git a/internal/render/dp.go b/internal/render/dp.go index ee98cc8d..6260def5 100644 --- a/internal/render/dp.go +++ b/internal/render/dp.go @@ -24,10 +24,9 @@ func (Deployment) Header(ns string) Header { return Header{ HeaderColumn{Name: "NAMESPACE"}, HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "READY"}, + HeaderColumn{Name: "READY", Align: tview.AlignRight}, HeaderColumn{Name: "UP-TO-DATE", Align: tview.AlignRight}, HeaderColumn{Name: "AVAILABLE", Align: tview.AlignRight}, - HeaderColumn{Name: "READY", Align: tview.AlignRight}, HeaderColumn{Name: "LABELS", Wide: true}, HeaderColumn{Name: "VALID", Wide: true}, HeaderColumn{Name: "AGE", Time: true, Decorator: AgeDecorator}, @@ -54,7 +53,6 @@ func (d Deployment) Render(o interface{}, ns string, r *Row) error { strconv.Itoa(int(dp.Status.AvailableReplicas)) + "/" + strconv.Itoa(int(dp.Status.Replicas)), strconv.Itoa(int(dp.Status.UpdatedReplicas)), strconv.Itoa(int(dp.Status.AvailableReplicas)), - strconv.Itoa(int(dp.Status.ReadyReplicas)), mapToStr(dp.Labels), asStatus(d.diagnose(dp.Status.Replicas, dp.Status.AvailableReplicas)), toAge(dp.ObjectMeta.CreationTimestamp), diff --git a/internal/render/helpers.go b/internal/render/helpers.go index 8e81cde3..2b693b72 100644 --- a/internal/render/helpers.go +++ b/internal/render/helpers.go @@ -9,10 +9,18 @@ import ( "github.com/derailed/tview" runewidth "github.com/mattn/go-runewidth" "github.com/rs/zerolog/log" + "golang.org/x/text/language" + "golang.org/x/text/message" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/duration" ) +// AsThousands prints a number with thousand separator. +func AsThousands(n int64) string { + p := message.NewPrinter(language.English) + return p.Sprintf("%d", n) +} + // Happy returns true if resoure is happy, false otherwise func Happy(ns string, h Header, r Row) bool { if len(r.Fields) == 0 { @@ -25,12 +33,12 @@ func Happy(ns string, h Header, r Row) bool { return strings.TrimSpace(r.Fields[validCol]) == "" } -const megaByte = 1024 * 1024 +// const megaByte = 1024 * 1024 -// ToMB converts bytes to megabytes. -func ToMB(v int64) float64 { - return float64(v) / megaByte -} +// // ToMB converts bytes to megabytes. +// func ToMB(v int64) float64 { +// return float64(v) / megaByte +// } func asStatus(err error) string { if err == nil { @@ -112,22 +120,14 @@ func join(a []string, sep string) string { return buff.String() } -// ToPerc prints a number as percentage. -func ToPerc(f float64) string { - return AsPerc(f) + "%" +// PrintPerc prints a number as percentage. +func PrintPerc(p int) string { + return strconv.Itoa(p) + "%" } -// AsPerc prints a number as a percentage. -func AsPerc(f float64) string { - return strconv.Itoa(int(f)) -} - -// ToPerc computes the ratio of two numbers as a percentage. -func toPerc(v1, v2 float64) float64 { - if v2 == 0 { - return 0 - } - return (v1 / v2) * 100 +// IntToStr converts an int to a string. +func IntToStr(p int) string { + return strconv.Itoa(int(p)) } func missing(s string) string { @@ -233,7 +233,7 @@ func ToMillicore(v int64) string { } // ToMi shows mem reading for human. -func ToMi(v float64) string { +func ToMi(v int64) string { return strconv.Itoa(int(v)) } diff --git a/internal/render/helpers_test.go b/internal/render/helpers_test.go index 51b93339..fb911c65 100644 --- a/internal/render/helpers_test.go +++ b/internal/render/helpers_test.go @@ -9,35 +9,6 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -func TestToMB(t *testing.T) { - uu := []struct { - v int64 - e float64 - }{ - {0, 0}, - {2 * megaByte, 2}, - {10 * megaByte, 10}, - } - - for _, u := range uu { - assert.Equal(t, u.e, ToMB(u.v)) - } -} - -func TestToPerc(t *testing.T) { - uu := []struct { - v1, v2, e float64 - }{ - {0, 0, 0}, - {100, 200, 50}, - {200, 100, 200}, - } - - for _, u := range uu { - assert.Equal(t, u.e, toPerc(u.v1, u.v2)) - } -} - func TestToAge(t *testing.T) { uu := map[string]struct { t time.Time @@ -343,7 +314,7 @@ func TestToMillicore(t *testing.T) { func TestToMi(t *testing.T) { uu := []struct { - v float64 + v int64 e string }{ {0, "0"}, @@ -356,27 +327,25 @@ func TestToMi(t *testing.T) { } } -func TestAsPerc(t *testing.T) { +func TestIntToStr(t *testing.T) { uu := []struct { - v float64 + v int e string }{ {0, "0"}, - {10.5, "10"}, {10, "10"}, - {0.05, "0"}, } for _, u := range uu { - assert.Equal(t, u.e, AsPerc(u.v)) + assert.Equal(t, u.e, IntToStr(u.v)) } } -func BenchmarkAsPerc(b *testing.B) { - v := 10.5 +func BenchmarkIntToStr(b *testing.B) { + v := 10 b.ResetTimer() b.ReportAllocs() for n := 0; n < b.N; n++ { - AsPerc(v) + IntToStr(v) } } diff --git a/internal/render/hpa.go b/internal/render/hpa.go index 1317f3f2..0d80ba17 100644 --- a/internal/render/hpa.go +++ b/internal/render/hpa.go @@ -259,12 +259,12 @@ func resourceMetricsV2b2(i int, spec autoscalingv2beta2.MetricSpec, statuses []a } if len(statuses) > i && statuses[i].Resource != nil && statuses[i].Resource.Current.AverageUtilization != nil { - current = AsPerc(float64(*statuses[i].Resource.Current.AverageUtilization)) + current = IntToStr(int(*statuses[i].Resource.Current.AverageUtilization)) } target := "" if spec.Resource.Target.AverageUtilization != nil { - target = AsPerc(float64(*spec.Resource.Target.AverageUtilization)) + target = IntToStr(int(*spec.Resource.Target.AverageUtilization)) } return current + "/" + target @@ -293,12 +293,12 @@ func resourceMetricsV2b1(i int, spec autoscalingv2beta1.MetricSpec, statuses []a } if len(statuses) > i && statuses[i].Resource != nil && statuses[i].Resource.CurrentAverageUtilization != nil { - current = AsPerc(float64(*statuses[i].Resource.CurrentAverageUtilization)) + current = IntToStr(int(*statuses[i].Resource.CurrentAverageUtilization)) } target := "" if spec.Resource.TargetAverageUtilization != nil { - target = AsPerc(float64(*spec.Resource.TargetAverageUtilization)) + target = IntToStr(int(*spec.Resource.TargetAverageUtilization)) } return current + "/" + target diff --git a/internal/render/node.go b/internal/render/node.go index f2efb5ad..77c9b397 100644 --- a/internal/render/node.go +++ b/internal/render/node.go @@ -141,23 +141,21 @@ func gatherNodeMX(no *v1.Node, mx *mv1beta1.NodeMetrics) (c metric, a metric, p return } - cpu := mx.Usage.Cpu().MilliValue() - mem := ToMB(mx.Usage.Memory().Value()) + cpu, mem := mx.Usage.Cpu().MilliValue(), client.ToMB(mx.Usage.Memory().Value()) c = metric{ cpu: ToMillicore(cpu), mem: ToMi(mem), } - acpu := no.Status.Allocatable.Cpu().MilliValue() - amem := ToMB(no.Status.Allocatable.Memory().Value()) + acpu, amem := no.Status.Allocatable.Cpu().MilliValue(), client.ToMB(no.Status.Allocatable.Memory().Value()) a = metric{ cpu: ToMillicore(acpu), mem: ToMi(amem), } p = metric{ - cpu: AsPerc(toPerc(float64(cpu), float64(acpu))), - mem: AsPerc(toPerc(mem, amem)), + cpu: IntToStr(client.ToPercentage(cpu, acpu)), + mem: IntToStr(client.ToPercentage(mem, amem)), } return diff --git a/internal/render/pod.go b/internal/render/pod.go index ae1b0ccc..809f5d5e 100644 --- a/internal/render/pod.go +++ b/internal/render/pod.go @@ -156,16 +156,16 @@ func (*Pod) gatherPodMX(pod *v1.Pod, mx *mv1beta1.PodMetrics) (c, p metric) { cpu, mem := currentRes(mx) c = metric{ cpu: ToMillicore(cpu.MilliValue()), - mem: ToMi(ToMB(mem.Value())), + mem: ToMi(client.ToMB(mem.Value())), } rc, rm := requestedRes(pod.Spec.Containers) lc, lm := resourceLimits(pod.Spec.Containers) p = metric{ - cpu: AsPerc(toPerc(float64(cpu.MilliValue()), float64(rc.MilliValue()))), - mem: AsPerc(toPerc(ToMB(mem.Value()), ToMB(rm.Value()))), - cpuLim: AsPerc(toPerc(float64(cpu.MilliValue()), float64(lc.MilliValue()))), - memLim: AsPerc(toPerc(ToMB(mem.Value()), ToMB(lm.Value()))), + cpu: IntToStr(client.ToPercentage(cpu.MilliValue(), rc.MilliValue())), + mem: IntToStr(client.ToPercentage(client.ToMB(mem.Value()), client.ToMB(rm.Value()))), + cpuLim: IntToStr(client.ToPercentage(cpu.MilliValue(), lc.MilliValue())), + memLim: IntToStr(client.ToPercentage(client.ToMB(mem.Value()), client.ToMB(lm.Value()))), } return diff --git a/internal/render/portforward.go b/internal/render/portforward.go index 16dd1641..c05185cc 100644 --- a/internal/render/portforward.go +++ b/internal/render/portforward.go @@ -70,8 +70,8 @@ func (f PortForward) Render(o interface{}, gvr string, r *Row) error { pf.Container(), strings.Join(pf.Ports(), ","), UrlFor(pf.Config.Host, pf.Config.Path, ports[0]), - asNum(pf.Config.C), - asNum(pf.Config.N), + AsThousands(int64(pf.Config.C)), + AsThousands(int64(pf.Config.N)), "", pf.Age(), } diff --git a/internal/tchart/component.go b/internal/tchart/component.go index 15e85a3e..89219f5c 100644 --- a/internal/tchart/component.go +++ b/internal/tchart/component.go @@ -98,7 +98,7 @@ func (c *Component) GetSeriesColorNames() []string { c.mx.RLock() defer c.mx.RUnlock() - var nn []string + nn := make([]string, 0, len(c.seriesColors)) for _, color := range c.seriesColors { for name, co := range tcell.ColorNames { if co == color { diff --git a/internal/tchart/dot_matrix.go b/internal/tchart/dot_matrix.go index e12c4675..a2533456 100644 --- a/internal/tchart/dot_matrix.go +++ b/internal/tchart/dot_matrix.go @@ -1,8 +1,6 @@ package tchart import ( - "fmt" - "github.com/derailed/tview" ) @@ -24,12 +22,6 @@ const ( lv = '\u257b' ) -// Segment represents a dial segment. -type Segment []int - -// Segments represents a collection of segments. -type Segments []Segment - // Matrix represents a number dial. type Matrix [][]rune @@ -42,94 +34,16 @@ type DotMatrix struct { } // NewDotMatrix returns a new matrix. -func NewDotMatrix(row, col int) DotMatrix { +func NewDotMatrix() DotMatrix { return DotMatrix{ - row: row, - col: col, + row: 3, + col: 3, } } // Print prints the matrix. func (d DotMatrix) Print(n int) Matrix { - if d.row == d.col { - return To3x3Char(n) - } - m := make(Matrix, d.row) - segs := asSegments(n) - for row := 0; row < d.row; row++ { - for col := 0; col < d.col; col++ { - m[row] = append(m[row], segs.CharFor(row, col)) - } - } - return m -} - -func asSegments(n int) Segment { - switch n { - case 0: - return Segment{1, 1, 1, 0, 1, 1, 1} - case 1: - return Segment{0, 0, 1, 0, 0, 1, 0} - case 2: - return Segment{1, 0, 1, 1, 1, 0, 1} - case 3: - return Segment{1, 0, 1, 1, 0, 1, 1} - case 4: - return Segment{0, 1, 0, 1, 0, 1, 0} - case 5: - return Segment{1, 1, 0, 1, 0, 1, 1} - case 6: - return Segment{0, 1, 0, 1, 1, 1, 1} - case 7: - return Segment{1, 0, 1, 0, 0, 1, 0} - case 8: - return Segment{1, 1, 1, 1, 1, 1, 1} - case 9: - return Segment{1, 1, 1, 1, 0, 1, 0} - - default: - panic(fmt.Sprintf("NYI %d", n)) - } -} - -// CharFor returns a char based on row/col. -func (s Segment) CharFor(row, col int) rune { - c := dots[0] - segs := ToSegments(row, col) - if segs == nil { - return c - } - for _, seg := range segs { - if s[seg] == 1 { - c = charForSeg(seg, row, col) - } - } - return c -} - -func charForSeg(seg, row, col int) rune { - switch seg { - case 0, 3, 6: - return dots[2] - } - if row == 0 && (col == 0 || col == 2) { - return dots[2] - } - - return dots[3] -} - -var segs = map[int][][]int{ - 0: {{1, 0}, {0}, {2, 0}}, - 1: {{1}, nil, {2}}, - 2: {{1, 3}, {3}, {2, 3}}, - 3: {{4}, nil, {5}}, - 4: {{4, 6}, {6}, {5, 6}}, -} - -// ToSegments return path segments. -func ToSegments(row, col int) []int { - return segs[row][col] + return To3x3Char(n) } // To3x3Char returns 3x3 number matrix diff --git a/internal/tchart/dot_matrix_test.go b/internal/tchart/dot_matrix_test.go index 65a9e722..ad9cf7df 100644 --- a/internal/tchart/dot_matrix_test.go +++ b/internal/tchart/dot_matrix_test.go @@ -1,7 +1,6 @@ package tchart_test import ( - "fmt" "strconv" "testing" @@ -9,130 +8,12 @@ import ( "github.com/stretchr/testify/assert" ) -func TestSegmentFor(t *testing.T) { - uu := map[string]struct { - r, c int - e []int - }{ - "0x0": {r: 0, c: 0, e: []int{1, 0}}, - "0x1": {r: 0, c: 1, e: []int{0}}, - "0x2": {r: 0, c: 2, e: []int{2, 0}}, - "1x0": {r: 1, c: 0, e: []int{1}}, - "1x1": {r: 1, c: 1, e: nil}, - "1x2": {r: 1, c: 2, e: []int{2}}, - "2x0": {r: 2, c: 0, e: []int{1, 3}}, - "2x1": {r: 2, c: 1, e: []int{3}}, - "2x2": {r: 2, c: 2, e: []int{2, 3}}, - "3x0": {r: 3, c: 0, e: []int{4}}, - "3x1": {r: 3, c: 1, e: nil}, - "3x2": {r: 3, c: 2, e: []int{5}}, - "4x0": {r: 4, c: 0, e: []int{4, 6}}, - "4x1": {r: 4, c: 1, e: []int{6}}, - "4x2": {r: 4, c: 2, e: []int{5, 6}}, - } - - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, tchart.ToSegments(u.r, u.c)) - }) - } -} - -func TestDial5x3(t *testing.T) { - d := tchart.NewDotMatrix(5, 3) - for n := 0; n <= 9; n++ { - i := n - t.Run(strconv.Itoa(n), func(t *testing.T) { - assert.Equal(t, numbers3x5[i], d.Print(i)) - }) - } -} - func TestDial3x3(t *testing.T) { - d := tchart.NewDotMatrix(3, 3) + d := tchart.NewDotMatrix() for n := 0; n <= 2; n++ { i := n t.Run(strconv.Itoa(n), func(t *testing.T) { - fmt.Println(tchart.To3x3Char(i)) assert.Equal(t, tchart.To3x3Char(i), d.Print(i)) }) } } - -// Helpers... - -const hChar, vChar = '▤', '▥' - -var numbers3x5 = []tchart.Matrix{ - [][]rune{ - {hChar, hChar, hChar}, - {vChar, ' ', vChar}, - {vChar, ' ', vChar}, - {vChar, ' ', vChar}, - {hChar, hChar, hChar}, - }, - [][]rune{ - {' ', ' ', hChar}, - {' ', ' ', vChar}, - {' ', ' ', vChar}, - {' ', ' ', vChar}, - {' ', ' ', vChar}, - }, - [][]rune{ - {hChar, hChar, hChar}, - {' ', ' ', vChar}, - {hChar, hChar, hChar}, - {vChar, ' ', ' '}, - {hChar, hChar, hChar}, - }, - [][]rune{ - {hChar, hChar, hChar}, - {' ', ' ', vChar}, - {hChar, hChar, hChar}, - {' ', ' ', vChar}, - {hChar, hChar, hChar}, - }, - [][]rune{ - {hChar, ' ', ' '}, - {vChar, ' ', ' '}, - {hChar, hChar, hChar}, - {' ', ' ', vChar}, - {' ', ' ', vChar}, - }, - [][]rune{ - {hChar, hChar, hChar}, - {vChar, ' ', ' '}, - {hChar, hChar, hChar}, - {' ', ' ', vChar}, - {hChar, hChar, hChar}, - }, - [][]rune{ - {hChar, ' ', ' '}, - {vChar, ' ', ' '}, - {hChar, hChar, hChar}, - {vChar, ' ', vChar}, - {hChar, hChar, hChar}, - }, - [][]rune{ - {hChar, hChar, hChar}, - {' ', ' ', vChar}, - {' ', ' ', vChar}, - {' ', ' ', vChar}, - {' ', ' ', vChar}, - }, - [][]rune{ - {hChar, hChar, hChar}, - {vChar, ' ', vChar}, - {hChar, hChar, hChar}, - {vChar, ' ', vChar}, - {hChar, hChar, hChar}, - }, - [][]rune{ - {hChar, hChar, hChar}, - {vChar, ' ', vChar}, - {hChar, hChar, hChar}, - {' ', ' ', vChar}, - {' ', ' ', vChar}, - }, -} diff --git a/internal/tchart/gauge.go b/internal/tchart/gauge.go index a241d031..233451a6 100644 --- a/internal/tchart/gauge.go +++ b/internal/tchart/gauge.go @@ -27,9 +27,9 @@ type delta int type Gauge struct { *Component - data Metric - resolution int - deltaOk, deltaFault delta + data Metric + resolution int + deltaOk, deltaS2 delta } // NewGauge returns a new gauge. @@ -53,7 +53,7 @@ func (g *Gauge) Add(m Metric) { g.mx.Lock() defer g.mx.Unlock() - g.deltaOk, g.deltaFault = computeDelta(g.data.OK, m.OK), computeDelta(g.data.Fault, m.Fault) + g.deltaOk, g.deltaS2 = computeDelta(g.data.S1, m.S1), computeDelta(g.data.S2, m.S2) g.data = m } @@ -80,12 +80,12 @@ func (g *Gauge) Draw(sc tcell.Screen) { ) s1C, s2C := g.colorForSeries() - d1, d2 := fmt.Sprintf(fmat, g.data.OK), fmt.Sprintf(fmat, g.data.Fault) + d1, d2 := fmt.Sprintf(fmat, g.data.S1), fmt.Sprintf(fmat, g.data.S2) o.X -= len(d1) * 3 - g.drawNum(sc, true, o, g.data.OK, g.deltaOk, d1, style.Foreground(s1C).Dim(false)) + g.drawNum(sc, true, o, g.data.S1, g.deltaOk, d1, style.Foreground(s1C).Dim(false)) o.X = mid.X + 1 - g.drawNum(sc, false, o, g.data.Fault, g.deltaFault, d2, style.Foreground(s2C).Dim(false)) + g.drawNum(sc, false, o, g.data.S2, g.deltaS2, d2, style.Foreground(s2C).Dim(false)) if rect.Dx() > 0 && rect.Dy() > 0 && g.legend != "" { legend := g.legend @@ -96,14 +96,14 @@ func (g *Gauge) Draw(sc tcell.Screen) { } } -func (g *Gauge) drawNum(sc tcell.Screen, ok bool, o image.Point, n int, dn delta, ns string, style tcell.Style) { +func (g *Gauge) drawNum(sc tcell.Screen, ok bool, o image.Point, n int64, dn delta, ns string, style tcell.Style) { c1, _ := g.colorForSeries() if ok { style = style.Foreground(c1) printDelta(sc, dn, o, style) } - dm, significant := NewDotMatrix(3, 3), n == 0 + dm, significant := NewDotMatrix(), n == 0 if n == 0 { style = g.dimmed } @@ -138,7 +138,7 @@ func (g *Gauge) drawDial(sc tcell.Screen, m Matrix, o image.Point, style tcell.S // ---------------------------------------------------------------------------- // Helpers... -func computeDelta(d1, d2 int) delta { +func computeDelta(d1, d2 int64) delta { if d2 == 0 { return DeltaSame } diff --git a/internal/tchart/gauge_int_test.go b/internal/tchart/gauge_int_test.go index f34876b8..a335eeea 100644 --- a/internal/tchart/gauge_int_test.go +++ b/internal/tchart/gauge_int_test.go @@ -8,7 +8,7 @@ import ( func TestComputeDeltas(t *testing.T) { uu := map[string]struct { - d1, d2 int + d1, d2 int64 e delta }{ "same": { diff --git a/internal/tchart/gauge_test.go b/internal/tchart/gauge_test.go index dcfef73d..1843947f 100644 --- a/internal/tchart/gauge_test.go +++ b/internal/tchart/gauge_test.go @@ -16,11 +16,11 @@ func TestMetricsMaxDigits(t *testing.T) { e: 1, }, "oks": { - m: tchart.Metric{OK: 100, Fault: 10}, + m: tchart.Metric{S1: 100, S2: 10}, e: 3, }, "errs": { - m: tchart.Metric{OK: 10, Fault: 1000}, + m: tchart.Metric{S1: 10, S2: 1000}, e: 4, }, } @@ -36,13 +36,13 @@ func TestMetricsMaxDigits(t *testing.T) { func TestMetricsMax(t *testing.T) { uu := map[string]struct { m tchart.Metric - e int + e int64 }{ "empty": { e: 0, }, "max_ok": { - m: tchart.Metric{OK: 100, Fault: 10}, + m: tchart.Metric{S1: 100, S2: 10}, e: 100, }, } @@ -54,35 +54,3 @@ func TestMetricsMax(t *testing.T) { }) } } - -func TestGauge(t *testing.T) { - uu := map[string]struct { - mm []tchart.Metric - e int - }{ - "empty": { - e: 1, - }, - "oks": { - mm: []tchart.Metric{{OK: 100, Fault: 10}}, - e: 3, - }, - "errs": { - mm: []tchart.Metric{{OK: 10, Fault: 1000}}, - e: 4, - }, - } - - for k := range uu { - u := uu[k] - g := tchart.NewGauge("fred") - assert.True(t, g.IsDial()) - for _, m := range u.mm { - g.Add(m) - } - t.Run(k, func(t *testing.T) { - // assert.Equal(t, u.e, u.m.MaxDigits()) - }) - } - -} diff --git a/internal/tchart/sparkline.go b/internal/tchart/sparkline.go index abbcf40e..8d49d663 100644 --- a/internal/tchart/sparkline.go +++ b/internal/tchart/sparkline.go @@ -17,29 +17,29 @@ type block struct { } type blocks struct { - oks, errs block + s1, s2 block } -// Metric tracks a good and error rates. +// Metric tracks two series. type Metric struct { - OK, Fault int + S1, S2 int64 } // MaxDigits returns the max series number of digits. func (m Metric) MaxDigits() int { - s := fmt.Sprintf("%d", m.Max()) + return len(s) } // Max returns the max of the series. -func (m Metric) Max() int { - return int(math.Max(float64(m.OK), float64(m.Fault))) +func (m Metric) Max() int64 { + return int64(math.Max(float64(m.S1), float64(m.S2))) } -// Sum returns the sum of the metrics. -func (m Metric) Sum() int { - return m.OK + m.Fault +// Sum returns the sum of series. +func (m Metric) Sum() int64 { + return m.S1 + m.S2 } // SparkLine represents a sparkline component. @@ -81,7 +81,7 @@ func (s *SparkLine) Draw(screen tcell.Screen) { return } - pad := 1 + pad := 0 if s.legend != "" { pad++ } @@ -97,18 +97,15 @@ func (s *SparkLine) Draw(screen tcell.Screen) { idx = len(s.data) - rect.Dx()/2 } - factor := 2 - if !s.multiSeries { - factor = 1 - } - scale := float64(len(sparks)*(rect.Dy()-pad)/factor) / float64(max) + scale := float64(len(sparks)*(rect.Dy()-pad)) / float64(max) c1, c2 := s.colorForSeries() for _, d := range s.data[idx:] { b := toBlocks(d, scale) cY := rect.Max.Y - pad - cY = s.drawBlock(rect, screen, cX, cY, b.oks, c1) - _ = s.drawBlock(rect, screen, cX, cY, b.errs, c2) - cX += 2 + s.drawBlock(rect, screen, cX, cY, b.s1, c1) + cX++ + s.drawBlock(rect, screen, cX, cY, b.s2, c2) + cX++ } if rect.Dx() > 0 && rect.Dy() > 0 && s.legend != "" { @@ -120,7 +117,7 @@ func (s *SparkLine) Draw(screen tcell.Screen) { } } -func (s *SparkLine) drawBlock(r image.Rectangle, screen tcell.Screen, x, y int, b block, c tcell.Color) int { +func (s *SparkLine) drawBlock(r image.Rectangle, screen tcell.Screen, x, y int, b block, c tcell.Color) { style := tcell.StyleDefault.Foreground(c).Background(s.bgColor) zeroY := r.Max.Y - r.Dy() @@ -133,12 +130,7 @@ func (s *SparkLine) drawBlock(r image.Rectangle, screen tcell.Screen, x, y int, } if b.partial != 0 { screen.SetContent(x, y, b.partial, nil, style) - if b.full == 0 { - y-- - } } - - return y } func (s *SparkLine) cutSet(width int) { @@ -151,8 +143,8 @@ func (s *SparkLine) cutSet(width int) { } } -func (s *SparkLine) computeMax() int { - var max int +func (s *SparkLine) computeMax() int64 { + var max int64 for _, d := range s.data { m := d.Max() if max < m { @@ -167,10 +159,10 @@ func toBlocks(m Metric, scale float64) blocks { if m.Sum() <= 0 { return blocks{} } - return blocks{oks: makeBlocks(m.OK, scale), errs: makeBlocks(m.Fault, scale)} + return blocks{s1: makeBlocks(m.S1, scale), s2: makeBlocks(m.S2, scale)} } -func makeBlocks(v int, scale float64) block { +func makeBlocks(v int64, scale float64) block { scaled := int(math.Round(float64(v) * scale)) p, b := scaled%len(sparks), block{full: scaled / len(sparks)} if b.full == 0 && v > 0 && p == 0 { diff --git a/internal/tchart/sparkline_int_test.go b/internal/tchart/sparkline_int_test.go index ac3278c8..36d51612 100644 --- a/internal/tchart/sparkline_int_test.go +++ b/internal/tchart/sparkline_int_test.go @@ -57,27 +57,27 @@ func TestToBlocks(t *testing.T) { e: blocks{}, }, "max_ok": { - m: Metric{OK: 100, Fault: 10}, + m: Metric{S1: 100, S2: 10}, s: 0.5, e: blocks{ - oks: block{full: 6, partial: sparks[2]}, - errs: block{full: 0, partial: sparks[5]}, + s1: block{full: 6, partial: sparks[2]}, + s2: block{full: 0, partial: sparks[5]}, }, }, "max_fault": { - m: Metric{OK: 10, Fault: 100}, + m: Metric{S1: 10, S2: 100}, s: 0.5, e: blocks{ - oks: block{full: 0, partial: sparks[5]}, - errs: block{full: 6, partial: sparks[2]}, + s1: block{full: 0, partial: sparks[5]}, + s2: block{full: 6, partial: sparks[2]}, }, }, "over": { - m: Metric{OK: 22, Fault: 999}, + m: Metric{S1: 22, S2: 999}, s: float64(8*20) / float64(999), e: blocks{ - oks: block{full: 0, partial: sparks[4]}, - errs: block{full: 20, partial: sparks[0]}, + s1: block{full: 0, partial: sparks[4]}, + s2: block{full: 20, partial: sparks[0]}, }, }, } @@ -93,26 +93,26 @@ func TestToBlocks(t *testing.T) { func TestComputeMax(t *testing.T) { uu := map[string]struct { mm []Metric - e int + e int64 }{ "empty": { e: 0, }, "max_ok": { - mm: []Metric{{OK: 100, Fault: 10}}, + mm: []Metric{{S1: 100, S2: 10}}, e: 100, }, "max_fault": { - mm: []Metric{{OK: 100, Fault: 1000}}, + mm: []Metric{{S1: 100, S2: 1000}}, e: 1000, }, "many": { mm: []Metric{ - {OK: 100, Fault: 1000}, - {OK: 110, Fault: 1010}, - {OK: 120, Fault: 1020}, - {OK: 130, Fault: 1030}, - {OK: 140, Fault: 1040}, + {S1: 100, S2: 1000}, + {S1: 110, S2: 1010}, + {S1: 120, S2: 1020}, + {S1: 130, S2: 1030}, + {S1: 140, S2: 1040}, }, e: 1040, }, diff --git a/internal/ui/indicator.go b/internal/ui/indicator.go index 74c191e5..60ec1d3e 100644 --- a/internal/ui/indicator.go +++ b/internal/ui/indicator.go @@ -56,8 +56,8 @@ func (s *StatusIndicator) ClusterInfoUpdated(data model.ClusterMeta) { data.Cluster, data.User, data.K8sVer, - render.ToPerc(data.Cpu), - render.ToPerc(data.Mem), + render.PrintPerc(data.Cpu), + render.PrintPerc(data.Mem), )) }) } @@ -131,8 +131,8 @@ func (s *StatusIndicator) setText(msg string) { // Helpers... // AsPercDelta represents a percentage with a delta indicator. -func AsPercDelta(ov, nv float64) string { - prev, cur := render.AsPerc(ov), render.AsPerc(nv) +func AsPercDelta(ov, nv int) string { + prev, cur := render.IntToStr(ov), render.IntToStr(nv) if cur == "0" { return render.NAValue } diff --git a/internal/ui/logo.go b/internal/ui/logo.go index 3bc0a83b..f29b58c4 100644 --- a/internal/ui/logo.go +++ b/internal/ui/logo.go @@ -24,8 +24,8 @@ func NewLogo(styles *config.Styles) *Logo { styles: styles, } l.SetDirection(tview.FlexRow) - l.AddItem(l.logo, 0, 6, false) - l.AddItem(l.status, 1, 0, false) + l.AddItem(l.logo, 6, 1, false) + l.AddItem(l.status, 1, 1, false) l.refreshLogo(styles.Body().LogoColor) l.SetBackgroundColor(styles.BgColor()) styles.AddListener(&l) diff --git a/internal/ui/table.go b/internal/ui/table.go index 2923523b..7fd4901a 100644 --- a/internal/ui/table.go +++ b/internal/ui/table.go @@ -271,8 +271,9 @@ func (t *Table) buildRow(r int, re, ore render.RowEvent, h render.Header, pads M cell := tview.NewTableCell(field) cell.SetExpansion(1) cell.SetAlign(h[c].Align) - cell.SetTextColor(color(t.GetModel().GetNamespace(), t.header, ore)) - if marked { + fgColor := color(t.GetModel().GetNamespace(), t.header, ore) + cell.SetTextColor(fgColor) + if marked && fgColor != render.ErrColor { cell.SetTextColor(t.styles.Table().MarkColor.Color()) } if col == 0 { diff --git a/internal/view/app.go b/internal/view/app.go index bf2ee30b..ddebb220 100644 --- a/internal/view/app.go +++ b/internal/view/app.go @@ -294,7 +294,7 @@ func (a *App) switchCtx(name string, loadPods bool) error { a.Flash().Infof("Switching context to %s", name) a.ReloadStyles(name) v := a.Config.ActiveView() - if v == "" { + if v == "" || v == "ctx" || v == "context" { v = "pod" } if err := a.gotoResource(v, ns, true); loadPods && err != nil { @@ -342,8 +342,11 @@ func (a *App) Run() error { func (a *App) Status(l model.FlashLevel, msg string) { a.QueueUpdateDraw(func() { a.Flash().SetMessage(l, msg) - a.setIndicator(l, msg) - a.setLogo(l, msg) + if a.showHeader { + a.setLogo(l, msg) + } else { + a.setIndicator(l, msg) + } }) } diff --git a/internal/view/cluster_info.go b/internal/view/cluster_info.go index f019a2ae..b2149b1f 100644 --- a/internal/view/cluster_info.go +++ b/internal/view/cluster_info.go @@ -93,6 +93,18 @@ func (c *ClusterInfo) ClusterInfoChanged(prev, curr model.ClusterMeta) { if c.app.Conn().HasMetrics() { row = c.setCell(row, ui.AsPercDelta(prev.Cpu, curr.Cpu)) _ = c.setCell(row, ui.AsPercDelta(prev.Mem, curr.Mem)) + var set bool + if c.app.Config.K9s.Thresholds.ExceedsCPUPerc(curr.Cpu) { + c.app.Status(model.FlashErr, "CPU on fire!") + set = true + } + if c.app.Config.K9s.Thresholds.ExceedsMemoryPerc(curr.Mem) { + c.app.Status(model.FlashErr, "Memory on fire!") + set = true + } + if !set { + c.app.ClearStatus(true) + } } c.updateStyle() }) diff --git a/internal/view/pulse.go b/internal/view/pulse.go index 3205c6c6..11df4398 100644 --- a/internal/view/pulse.go +++ b/internal/view/pulse.go @@ -11,6 +11,7 @@ import ( "github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/health" "github.com/derailed/k9s/internal/model" + "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/tchart" "github.com/derailed/k9s/internal/ui" "github.com/derailed/tview" @@ -87,15 +88,15 @@ func (p *Pulse) Init(ctx context.Context) error { p.makeGA(image.Point{X: 0, Y: 2}, image.Point{X: 2, Y: 2}, "apps/v1/replicasets"), p.makeGA(image.Point{X: 0, Y: 4}, image.Point{X: 2, Y: 2}, "apps/v1/statefulsets"), p.makeGA(image.Point{X: 0, Y: 6}, image.Point{X: 2, Y: 2}, "apps/v1/daemonsets"), - p.makeSP(true, image.Point{X: 2, Y: 0}, image.Point{X: 3, Y: 2}, "v1/pods"), - p.makeSP(true, image.Point{X: 2, Y: 2}, image.Point{X: 3, Y: 2}, "v1/events"), - p.makeSP(true, image.Point{X: 2, Y: 4}, image.Point{X: 3, Y: 2}, "batch/v1/jobs"), - p.makeSP(true, image.Point{X: 2, Y: 6}, image.Point{X: 3, Y: 2}, "v1/persistentvolumes"), + p.makeSP(image.Point{X: 2, Y: 0}, image.Point{X: 3, Y: 2}, "v1/pods"), + p.makeSP(image.Point{X: 2, Y: 2}, image.Point{X: 3, Y: 2}, "v1/events"), + p.makeSP(image.Point{X: 2, Y: 4}, image.Point{X: 3, Y: 2}, "batch/v1/jobs"), + p.makeSP(image.Point{X: 2, Y: 6}, image.Point{X: 3, Y: 2}, "v1/persistentvolumes"), } if p.app.Conn().HasMetrics() { p.charts = append(p.charts, - p.makeSP(false, image.Point{X: 5, Y: 0}, image.Point{X: 2, Y: 4}, "cpu"), - p.makeSP(false, image.Point{X: 5, Y: 4}, image.Point{X: 2, Y: 4}, "mem"), + p.makeSP(image.Point{X: 5, Y: 0}, image.Point{X: 2, Y: 4}, "cpu"), + p.makeSP(image.Point{X: 5, Y: 4}, image.Point{X: 2, Y: 4}, "mem"), ) } p.bindKeys() @@ -126,6 +127,14 @@ func (p *Pulse) StylesChanged(s *config.Styles) { p.app.Draw() } +const ( + genFmat = " %s([%s::]%d[white::]:[%s::b]%d[-::])" + cpuFmt = " %s [%s::b]%s[white::-]::[gray::]%s ([%s::]%sm[white::]/[%s::]%sm[-::])" + memFmt = " %s [%s::b]%s[white::-]::[gray::]%s ([%s::]%sMi[white::]/[%s::]%sMi[-::])" + okColor = "palegreen" + errColor = "orangered" +) + // PulseChanged notifies the model data changed. func (p *Pulse) PulseChanged(c *health.Check) { index, ok := findIndexGVR(p.charts, c.GVR) @@ -137,29 +146,59 @@ func (p *Pulse) PulseChanged(c *health.Check) { if !ok { return } + + nn := v.GetSeriesColorNames() + if c.Tally(health.S1) == 0 { + nn[0] = "gray" + } + if c.Tally(health.S2) == 0 { + nn[1] = "gray" + } + gvr := client.NewGVR(c.GVR) switch c.GVR { case "cpu": - v.SetLegend(fmt.Sprintf(" %s(%dm)", strings.Title(gvr.R()), c.Tally(health.OK))) + perc := client.ToPercentage(c.Tally(health.S1), c.Tally(health.S2)) + color := okColor + if p.app.Config.K9s.Thresholds.ExceedsCPUPerc(perc) { + color = errColor + } + v.SetLegend(fmt.Sprintf(cpuFmt, + strings.Title(gvr.R()), + color, + render.PrintPerc(perc), + render.PrintPerc(p.app.Config.K9s.Thresholds.CPU), + nn[0], + render.AsThousands(c.Tally(health.S1)), + nn[1], + render.AsThousands(c.Tally(health.S2)), + )) case "mem": - v.SetLegend(fmt.Sprintf(" %s(%dMi)", strings.Title(gvr.R()), c.Tally(health.OK))) + perc := client.ToPercentage(c.Tally(health.S1), c.Tally(health.S2)) + color := okColor + if p.app.Config.K9s.Thresholds.ExceedsMemoryPerc(perc) { + color = errColor + } + v.SetLegend(fmt.Sprintf(memFmt, + strings.Title(gvr.R()), + color, + render.PrintPerc(perc), + render.PrintPerc(p.app.Config.K9s.Thresholds.Memory), + nn[0], + render.AsThousands(c.Tally(health.S1)), + nn[1], + render.AsThousands(c.Tally(health.S2)), + )) default: - nn := v.GetSeriesColorNames() - if c.Tally(health.OK) == 0 { - nn[0] = "gray" - } - if c.Tally(health.Toast) == 0 { - nn[1] = "gray" - } - v.SetLegend(fmt.Sprintf(" %s([%s::]%d[white::]:[%s::b]%d[-::])", + v.SetLegend(fmt.Sprintf(genFmat, strings.Title(gvr.R()), nn[0], - c.Tally(health.OK), + c.Tally(health.S1), nn[1], - c.Tally(health.Toast), + c.Tally(health.S2), )) } - v.Add(tchart.Metric{OK: c.Tally(health.OK), Fault: c.Tally(health.Toast)}) + v.Add(tchart.Metric{S1: c.Tally(health.S1), S2: c.Tally(health.S2)}) } // PulseFailed notifies the load failed. @@ -301,7 +340,7 @@ func (p *Pulse) nextFocusCmd(direction int) func(evt *tcell.EventKey) *tcell.Eve } } -func (p *Pulse) makeSP(multi bool, loc image.Point, span image.Point, gvr string) *tchart.SparkLine { +func (p *Pulse) makeSP(loc image.Point, span image.Point, gvr string) *tchart.SparkLine { s := tchart.NewSparkLine(gvr) s.SetBackgroundColor(p.app.Styles.Charts().BgColor.Color()) s.SetBorderPadding(0, 1, 0, 1) @@ -312,9 +351,7 @@ func (p *Pulse) makeSP(multi bool, loc image.Point, span image.Point, gvr string } s.SetLegend(fmt.Sprintf(" %s ", strings.Title(client.NewGVR(gvr).R()))) s.SetInputCapture(p.keyboard) - if !multi { - s.SetMultiSeries(multi) - } + s.SetMultiSeries(true) p.AddItem(s, loc.X, loc.Y, span.X, span.Y, 0, 0, true) return s diff --git a/internal/view/restart_extender.go b/internal/view/restart_extender.go index f9b297d9..03883179 100644 --- a/internal/view/restart_extender.go +++ b/internal/view/restart_extender.go @@ -2,6 +2,7 @@ package view import ( "errors" + "fmt" "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/ui" @@ -30,19 +31,24 @@ func (r *RestartExtender) bindKeys(aa ui.KeyActions) { } func (r *RestartExtender) restartCmd(evt *tcell.EventKey) *tcell.EventKey { - path := r.GetTable().GetSelectedItem() - if path == "" { + paths := r.GetTable().GetSelectedItems() + if len(paths) == 0 { return nil } r.Stop() defer r.Start() - msg := "Please confirm rollout restart for " + path + msg := fmt.Sprintf("Restart deployment %s?" + paths[0]) + if len(paths) > 1 { + msg = fmt.Sprintf("Restart %d deployments?", len(paths)) + } dialog.ShowConfirm(r.App().Content.Pages, "", msg, func() { - if err := r.restartRollout(path); err != nil { - r.App().Flash().Err(err) - } else { - r.App().Flash().Infof("Rollout restart in progress for `%s...", path) + for _, path := range paths { + if err := r.restartRollout(path); err != nil { + r.App().Flash().Err(err) + } else { + r.App().Flash().Infof("Rollout restart in progress for `%s...", path) + } } }, func() {}) @@ -54,7 +60,6 @@ func (r *RestartExtender) restartRollout(path string) error { if err != nil { return nil } - s, ok := res.(dao.Restartable) if !ok { return errors.New("resource is not restartable")