add thresholds + pulses tlc + fix #596 #593 #560

mine
derailed 2020-03-04 12:27:47 -07:00
parent e213ba645e
commit 5fcfecb38e
36 changed files with 441 additions and 536 deletions

View File

@ -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))
}

View File

@ -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,
},
},
},

View File

@ -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.

View File

@ -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
`

View File

@ -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 = ""

View File

@ -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 {

View File

@ -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
}

View File

@ -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]
}

View File

@ -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))
}

View File

@ -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 {

View File

@ -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) {

View File

@ -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)
}
}

View File

@ -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.

View File

@ -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

View File

@ -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),

View File

@ -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))
}

View File

@ -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)
}
}

View File

@ -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 := "<auto>"
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 := "<auto>"
if spec.Resource.TargetAverageUtilization != nil {
target = AsPerc(float64(*spec.Resource.TargetAverageUtilization))
target = IntToStr(int(*spec.Resource.TargetAverageUtilization))
}
return current + "/" + target

View File

@ -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

View File

@ -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

View File

@ -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(),
}

View File

@ -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 {

View File

@ -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

View File

@ -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},
},
}

View File

@ -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
}

View File

@ -8,7 +8,7 @@ import (
func TestComputeDeltas(t *testing.T) {
uu := map[string]struct {
d1, d2 int
d1, d2 int64
e delta
}{
"same": {

View File

@ -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())
})
}
}

View File

@ -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 {

View File

@ -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,
},

View File

@ -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
}

View File

@ -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)

View File

@ -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 {

View File

@ -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)
}
})
}

View File

@ -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()
})

View File

@ -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

View File

@ -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, "<Confirm Restart>", 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")