Skip to content

Commit 066ba1d

Browse files
committed
Allow metrics cache expiry to be configured
1 parent 1e9f85d commit 066ba1d

21 files changed

+116
-59
lines changed

README.md

+3
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,8 @@ You can now override the context portForward default address configuration by se
396396
screenDumpDir: /tmp/dumps
397397
# Represents ui poll intervals. Default 2secs
398398
refreshRate: 2
399+
# How often metrics data will be cached for e.g. CPU and memory usage. Default 60s
400+
metricsCacheExpiry: 60
399401
# Number of retries once the connection to the api-server is lost. Default 15.
400402
maxConnRetry: 5
401403
# Indicates whether modification commands like delete/kill/edit are disabled. Default is false
@@ -966,6 +968,7 @@ k9s:
966968
liveViewAutoRefresh: false
967969
screenDumpDir: /tmp/dumps
968970
refreshRate: 2
971+
metricsCacheExpiry: 60
969972
maxConnRetry: 5
970973
readOnly: false
971974
noExitOnCtrlC: false

cmd/root.go

+3-2
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,8 @@ func loadConfiguration() (*config.Config, error) {
138138
errs = errors.Join(errs, err)
139139
}
140140

141-
conn, err := client.InitConnection(k8sCfg)
141+
options := client.NewOptions(k9sCfg.K9s.MetricsCacheExpiry)
142+
conn, err := client.InitConnection(k8sCfg, options)
142143

143144
if err != nil {
144145
errs = errors.Join(errs, err)
@@ -376,7 +377,7 @@ func initK8sFlagCompletion() {
376377

377378
_ = rootCmd.RegisterFlagCompletionFunc("namespace", func(cmd *cobra.Command, args []string, s string) ([]string, cobra.ShellCompDirective) {
378379
conn := client.NewConfig(k8sFlags)
379-
if c, err := client.InitConnection(conn); err == nil {
380+
if c, err := client.InitConnection(conn, client.Options{}); err == nil {
380381
if nss, err := c.ValidNamespaceNames(); err == nil {
381382
return filterFlagCompletions(nss, s)
382383
}

internal/client/client.go

+35-5
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,13 @@ type APIClient struct {
4646
nsClient dynamic.NamespaceableResourceInterface
4747
mxsClient *versioned.Clientset
4848
cachedClient *disk.CachedDiscoveryClient
49+
metricsServer *MetricsServer
4950
config *Config
5051
mx sync.RWMutex
5152
cache *cache.LRUExpireCache
5253
connOK bool
54+
55+
options Options
5356
}
5457

5558
// NewTestAPIClient for testing ONLY!!
@@ -62,11 +65,12 @@ func NewTestAPIClient() *APIClient {
6265

6366
// InitConnection initialize connection from command line args.
6467
// Checks for connectivity with the api server.
65-
func InitConnection(config *Config) (*APIClient, error) {
68+
func InitConnection(config *Config, options Options) (*APIClient, error) {
6669
a := APIClient{
67-
config: config,
68-
cache: cache.NewLRUExpireCache(cacheSize),
69-
connOK: true,
70+
config: config,
71+
cache: cache.NewLRUExpireCache(cacheSize),
72+
connOK: true,
73+
options: options,
7074
}
7175
err := a.supportsMetricsResources()
7276
if err != nil {
@@ -394,6 +398,20 @@ func (a *APIClient) getLogClient() kubernetes.Interface {
394398
return a.logClient
395399
}
396400

401+
func (a *APIClient) getMetricsServer() *MetricsServer {
402+
a.mx.RLock()
403+
defer a.mx.RUnlock()
404+
405+
return a.metricsServer
406+
}
407+
408+
func (a *APIClient) setMetricsServer(ms *MetricsServer) {
409+
a.mx.Lock()
410+
defer a.mx.Unlock()
411+
412+
a.metricsServer = ms
413+
}
414+
397415
func (a *APIClient) setClient(k kubernetes.Interface) {
398416
a.mx.Lock()
399417
defer a.mx.Unlock()
@@ -453,6 +471,18 @@ func (a *APIClient) Dial() (kubernetes.Interface, error) {
453471
return a.getClient(), nil
454472
}
455473

474+
func (a *APIClient) DialMetrics() *MetricsServer {
475+
if ms := a.getMetricsServer(); ms != nil {
476+
return ms
477+
}
478+
479+
cacheExpiry := time.Duration(a.options.MetricsCacheExpiry) * time.Second
480+
ms := NewMetricsServer(a, cacheExpiry)
481+
a.setMetricsServer(ms)
482+
483+
return a.getMetricsServer()
484+
}
485+
456486
// RestConfig returns a rest api client.
457487
func (a *APIClient) RestConfig() (*restclient.Config, error) {
458488
return a.config.RESTConfig()
@@ -552,7 +582,6 @@ func (a *APIClient) SwitchContext(name string) error {
552582
return err
553583
}
554584
a.reset()
555-
ResetMetrics()
556585

557586
// Need reload to pick up any kubeconfig changes.
558587
a.config = NewConfig(a.config.flags)
@@ -570,6 +599,7 @@ func (a *APIClient) reset() {
570599
a.setCachedClient(nil)
571600
a.setClient(nil)
572601
a.setLogClient(nil)
602+
a.setMetricsServer(nil)
573603
a.setConnOK(true)
574604
}
575605

internal/client/metrics.go

+11-27
Original file line numberDiff line numberDiff line change
@@ -18,41 +18,25 @@ import (
1818
)
1919

2020
const (
21-
mxCacheSize = 100
22-
mxCacheExpiry = 1 * time.Minute
23-
podMXGVR = "metrics.k8s.io/v1beta1/pods"
24-
nodeMXGVR = "metrics.k8s.io/v1beta1/nodes"
21+
mxCacheSize = 100
22+
podMXGVR = "metrics.k8s.io/v1beta1/pods"
23+
nodeMXGVR = "metrics.k8s.io/v1beta1/nodes"
2524
)
2625

27-
// MetricsDial tracks global metric server handle.
28-
var MetricsDial *MetricsServer
29-
30-
// DialMetrics dials the metrics server.
31-
func DialMetrics(c Connection) *MetricsServer {
32-
if MetricsDial == nil {
33-
MetricsDial = NewMetricsServer(c)
34-
}
35-
36-
return MetricsDial
37-
}
38-
39-
// ResetMetrics resets the metric server handle.
40-
func ResetMetrics() {
41-
MetricsDial = nil
42-
}
43-
4426
// MetricsServer serves cluster metrics for nodes and pods.
4527
type MetricsServer struct {
4628
Connection
4729

48-
cache *cache.LRUExpireCache
30+
cache *cache.LRUExpireCache
31+
cacheExpiry time.Duration
4932
}
5033

5134
// NewMetricsServer return a metric server instance.
52-
func NewMetricsServer(c Connection) *MetricsServer {
35+
func NewMetricsServer(c Connection, cacheExpiry time.Duration) *MetricsServer {
5336
return &MetricsServer{
54-
Connection: c,
55-
cache: cache.NewLRUExpireCache(mxCacheSize),
37+
Connection: c,
38+
cache: cache.NewLRUExpireCache(mxCacheSize),
39+
cacheExpiry: cacheExpiry,
5640
}
5741
}
5842

@@ -172,7 +156,7 @@ func (m *MetricsServer) FetchNodesMetrics(ctx context.Context) (*mv1beta1.NodeMe
172156
if err != nil {
173157
return mx, err
174158
}
175-
m.cache.Add(key, mxList, mxCacheExpiry)
159+
m.cache.Add(key, mxList, m.cacheExpiry)
176160

177161
return mxList, nil
178162
}
@@ -243,7 +227,7 @@ func (m *MetricsServer) FetchPodsMetrics(ctx context.Context, ns string) (*mv1be
243227
if err != nil {
244228
return mx, err
245229
}
246-
m.cache.Add(key, mxList, mxCacheExpiry)
230+
m.cache.Add(key, mxList, m.cacheExpiry)
247231

248232
return mxList, err
249233
}

internal/client/metrics_test.go

+11-6
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package client_test
55

66
import (
77
"testing"
8+
"time"
89

910
"github.com/derailed/k9s/internal/client"
1011
"github.com/stretchr/testify/assert"
@@ -72,7 +73,7 @@ func TestPodsMetrics(t *testing.T) {
7273
},
7374
}
7475

75-
m := client.NewMetricsServer(nil)
76+
m := makeMetricsServer()
7677
for k := range uu {
7778
u := uu[k]
7879
t.Run(k, func(t *testing.T) {
@@ -91,7 +92,7 @@ func TestPodsMetrics(t *testing.T) {
9192
}
9293

9394
func BenchmarkPodsMetrics(b *testing.B) {
94-
m := client.NewMetricsServer(nil)
95+
m := makeMetricsServer()
9596

9697
metrics := v1beta1.PodMetricsList{
9798
Items: []v1beta1.PodMetrics{
@@ -168,7 +169,7 @@ func TestNodesMetrics(t *testing.T) {
168169
},
169170
}
170171

171-
m := client.NewMetricsServer(nil)
172+
m := makeMetricsServer()
172173
for k := range uu {
173174
u := uu[k]
174175
t.Run(k, func(t *testing.T) {
@@ -201,7 +202,7 @@ func BenchmarkNodesMetrics(b *testing.B) {
201202
},
202203
}
203204

204-
m := client.NewMetricsServer(nil)
205+
m := makeMetricsServer()
205206
mmx := make(client.NodesMetrics)
206207

207208
b.ResetTimer()
@@ -261,7 +262,7 @@ func TestClusterLoad(t *testing.T) {
261262
},
262263
}
263264

264-
m := client.NewMetricsServer(nil)
265+
m := makeMetricsServer()
265266
for k := range uu {
266267
u := uu[k]
267268
t.Run(k, func(t *testing.T) {
@@ -287,7 +288,7 @@ func BenchmarkClusterLoad(b *testing.B) {
287288
},
288289
}
289290

290-
m := client.NewMetricsServer(nil)
291+
m := makeMetricsServer()
291292
var mx client.ClusterMetrics
292293
b.ResetTimer()
293294
b.ReportAllocs()
@@ -299,6 +300,10 @@ func BenchmarkClusterLoad(b *testing.B) {
299300
// ----------------------------------------------------------------------------
300301
// Helpers...
301302

303+
func makeMetricsServer() *client.MetricsServer {
304+
return client.NewMetricsServer(nil, 1*time.Minute)
305+
}
306+
302307
func makeMxPod(name, cpu, mem string) *v1beta1.PodMetrics {
303308
return &v1beta1.PodMetrics{
304309
ObjectMeta: metav1.ObjectMeta{

internal/client/options.go

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// Copyright Authors of K9s
3+
4+
package client
5+
6+
type Options struct {
7+
MetricsCacheExpiry int
8+
}
9+
10+
func NewOptions(metricsCacheExpiry int) Options {
11+
return Options{MetricsCacheExpiry: metricsCacheExpiry}
12+
}

internal/client/types.go

+3
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,9 @@ type Connection interface {
102102
// DialLogs connects to api server for logs.
103103
DialLogs() (kubernetes.Interface, error)
104104

105+
// DialMetrics connects to metrics server.
106+
DialMetrics() *MetricsServer
107+
105108
// SwitchContext switches cluster based on context.
106109
SwitchContext(ctx string) error
107110

internal/config/config_test.go

+2
Original file line numberDiff line numberDiff line change
@@ -537,6 +537,7 @@ func TestConfigLoad(t *testing.T) {
537537

538538
assert.Nil(t, cfg.Load("testdata/configs/k9s.yaml", true))
539539
assert.Equal(t, 2, cfg.K9s.RefreshRate)
540+
assert.Equal(t, 60, cfg.K9s.MetricsCacheExpiry)
540541
assert.Equal(t, int64(200), cfg.K9s.Logger.TailCount)
541542
assert.Equal(t, 2000, cfg.K9s.Logger.BufferSize)
542543
}
@@ -553,6 +554,7 @@ func TestConfigSaveFile(t *testing.T) {
553554
assert.Nil(t, cfg.Load("testdata/configs/k9s.yaml", true))
554555

555556
cfg.K9s.RefreshRate = 100
557+
cfg.K9s.MetricsCacheExpiry = 100
556558
cfg.K9s.ReadOnly = true
557559
cfg.K9s.Logger.TailCount = 500
558560
cfg.K9s.Logger.BufferSize = 800

internal/config/json/schemas/k9s.json

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"liveViewAutoRefresh": { "type": "boolean" },
1111
"screenDumpDir": {"type": "string"},
1212
"refreshRate": { "type": "integer" },
13+
"metricsCacheExpiry": { "type": "integer" },
1314
"maxConnRetry": { "type": "integer" },
1415
"readOnly": { "type": "boolean" },
1516
"noExitOnCtrlC": { "type": "boolean" },

internal/config/k9s.go

+16-10
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ type K9s struct {
2323
LiveViewAutoRefresh bool `json:"liveViewAutoRefresh" yaml:"liveViewAutoRefresh"`
2424
ScreenDumpDir string `json:"screenDumpDir" yaml:"screenDumpDir,omitempty"`
2525
RefreshRate int `json:"refreshRate" yaml:"refreshRate"`
26+
MetricsCacheExpiry int `json:"metricsCacheExpiry" yaml:"metricsCacheExpiry"`
2627
MaxConnRetry int `json:"maxConnRetry" yaml:"maxConnRetry"`
2728
ReadOnly bool `json:"readOnly" yaml:"readOnly"`
2829
NoExitOnCtrlC bool `json:"noExitOnCtrlC" yaml:"noExitOnCtrlC"`
@@ -51,16 +52,17 @@ type K9s struct {
5152
// NewK9s create a new K9s configuration.
5253
func NewK9s(conn client.Connection, ks data.KubeSettings) *K9s {
5354
return &K9s{
54-
RefreshRate: defaultRefreshRate,
55-
MaxConnRetry: defaultMaxConnRetry,
56-
ScreenDumpDir: AppDumpsDir,
57-
Logger: NewLogger(),
58-
Thresholds: NewThreshold(),
59-
ShellPod: NewShellPod(),
60-
ImageScans: NewImageScans(),
61-
dir: data.NewDir(AppContextsDir),
62-
conn: conn,
63-
ks: ks,
55+
RefreshRate: defaultRefreshRate,
56+
MetricsCacheExpiry: defaultMetricsCacheExpiry,
57+
MaxConnRetry: defaultMaxConnRetry,
58+
ScreenDumpDir: AppDumpsDir,
59+
Logger: NewLogger(),
60+
Thresholds: NewThreshold(),
61+
ShellPod: NewShellPod(),
62+
ImageScans: NewImageScans(),
63+
dir: data.NewDir(AppContextsDir),
64+
conn: conn,
65+
ks: ks,
6466
}
6567
}
6668

@@ -98,6 +100,7 @@ func (k *K9s) Merge(k1 *K9s) {
98100
k.LiveViewAutoRefresh = k1.LiveViewAutoRefresh
99101
k.ScreenDumpDir = k1.ScreenDumpDir
100102
k.RefreshRate = k1.RefreshRate
103+
k.MetricsCacheExpiry = k1.MetricsCacheExpiry
101104
k.MaxConnRetry = k1.MaxConnRetry
102105
k.ReadOnly = k1.ReadOnly
103106
k.NoExitOnCtrlC = k1.NoExitOnCtrlC
@@ -344,6 +347,9 @@ func (k *K9s) Validate(c client.Connection, ks data.KubeSettings) {
344347
if k.RefreshRate <= 0 {
345348
k.RefreshRate = defaultRefreshRate
346349
}
350+
if k.MetricsCacheExpiry <= 0 {
351+
k.MetricsCacheExpiry = defaultMetricsCacheExpiry
352+
}
347353
if k.MaxConnRetry <= 0 {
348354
k.MaxConnRetry = defaultMaxConnRetry
349355
}

internal/config/k9s_test.go

+3
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ func TestK9sMerge(t *testing.T) {
8888
LiveViewAutoRefresh: false,
8989
ScreenDumpDir: "",
9090
RefreshRate: 0,
91+
MetricsCacheExpiry: 0,
9192
MaxConnRetry: 0,
9293
ReadOnly: false,
9394
NoExitOnCtrlC: false,
@@ -102,12 +103,14 @@ func TestK9sMerge(t *testing.T) {
102103
k2: &config.K9s{
103104
LiveViewAutoRefresh: true,
104105
MaxConnRetry: 100,
106+
MetricsCacheExpiry: 100,
105107
ShellPod: config.NewShellPod(),
106108
},
107109
ek: &config.K9s{
108110
LiveViewAutoRefresh: true,
109111
ScreenDumpDir: "",
110112
RefreshRate: 0,
113+
MetricsCacheExpiry: 100,
111114
MaxConnRetry: 100,
112115
ReadOnly: false,
113116
NoExitOnCtrlC: false,

internal/config/mock/test_helpers.go

+3
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,9 @@ func (m mockConnection) Dial() (kubernetes.Interface, error) {
136136
func (m mockConnection) DialLogs() (kubernetes.Interface, error) {
137137
return nil, nil
138138
}
139+
func (m mockConnection) DialMetrics() *client.MetricsServer {
140+
return nil
141+
}
139142
func (m mockConnection) SwitchContext(ctx string) error {
140143
return nil
141144
}

0 commit comments

Comments
 (0)