Skip to content

Commit e63f22f

Browse files
issue-143 - fix: Net and Disc Metrics
1 parent 8ce341c commit e63f22f

File tree

10 files changed

+175
-8
lines changed

10 files changed

+175
-8
lines changed

docs/images/ktop.png

186 KB
Loading

metrics/k8s/metrics_server_source.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,11 @@ func (m *MetricsServerSource) GetMetricsForPod(ctx context.Context, pod metav1.O
135135
return m.GetPodMetrics(ctx, pod.GetNamespace(), pod.GetName())
136136
}
137137

138+
// GetPodNetworkDiskMetrics returns zero values - metrics-server doesn't provide network/disk metrics
139+
func (m *MetricsServerSource) GetPodNetworkDiskMetrics(ctx context.Context, namespace, podName string) (netRx, netTx, diskRead, diskWrite float64, err error) {
140+
return 0, 0, 0, 0, nil
141+
}
142+
138143
// GetAllPodMetrics retrieves metrics for all pods.
139144
func (m *MetricsServerSource) GetAllPodMetrics(ctx context.Context) ([]*metrics.PodMetrics, error) {
140145
// Call Metrics Server API directly

metrics/prom/prom_source.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,44 @@ func (p *PromMetricsSource) GetPodMetrics(ctx context.Context, namespace, podNam
435435
return podMetrics, nil
436436
}
437437

438+
// GetPodNetworkDiskMetrics retrieves network and disk I/O rates for a specific pod.
439+
// Network metrics are at the pod level (shared network namespace via pause container).
440+
// Disk metrics are aggregated across containers in the pod.
441+
// Returns zero values if metrics are not available.
442+
// Note: Pod-level network metrics are not available on containerd-based clusters.
443+
func (p *PromMetricsSource) GetPodNetworkDiskMetrics(ctx context.Context, namespace, podName string) (netRx, netTx, diskRead, diskWrite float64, err error) {
444+
p.mu.RLock()
445+
defer p.mu.RUnlock()
446+
447+
if !p.isHealthyLocked() {
448+
return 0, 0, 0, 0, fmt.Errorf("prometheus source is not healthy")
449+
}
450+
451+
if p.store == nil {
452+
return 0, 0, 0, 0, nil
453+
}
454+
455+
// Pod-level metrics: filter by pod and namespace
456+
labelMatchers := map[string]string{
457+
"pod": podName,
458+
"namespace": namespace,
459+
}
460+
461+
// Query Disk Read rate from cAdvisor
462+
if rate, calcErr := p.calculateCPURate("container_fs_reads_bytes_total", labelMatchers, 40*time.Second); calcErr == nil {
463+
diskRead = rate
464+
}
465+
466+
// Query Disk Write rate from cAdvisor
467+
if rate, calcErr := p.calculateCPURate("container_fs_writes_bytes_total", labelMatchers, 40*time.Second); calcErr == nil {
468+
diskWrite = rate
469+
}
470+
471+
// Note: netRx and netTx remain 0 - pod-level network metrics are not available
472+
// on containerd-based Kubernetes clusters (cAdvisor only exports node-level network)
473+
return netRx, netTx, diskRead, diskWrite, nil
474+
}
475+
438476
// GetMetricsForPod retrieves metrics for a specific pod object
439477
func (p *PromMetricsSource) GetMetricsForPod(ctx context.Context, pod metav1.Object) (*metrics.PodMetrics, error) {
440478
// Extract namespace and name from pod object

metrics/source.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ type MetricsSource interface {
2020
// Returns PodMetrics containing per-container resource usage.
2121
GetPodMetrics(ctx context.Context, namespace, podName string) (*PodMetrics, error)
2222

23+
// GetPodNetworkDiskMetrics retrieves network and disk I/O rates for a specific pod.
24+
// Network metrics are at the pod level (shared network namespace).
25+
// Disk metrics are aggregated across containers in the pod.
26+
// Returns zero values if metrics are not available (e.g., for metrics-server source).
27+
GetPodNetworkDiskMetrics(ctx context.Context, namespace, podName string) (netRx, netTx, diskRead, diskWrite float64, err error)
28+
2329
// GetMetricsForPod retrieves metrics for a specific pod object.
2430
// This method exists for compatibility with existing code that passes v1.Pod objects.
2531
// Implementations may delegate to GetPodMetrics(pod.GetNamespace(), pod.GetName()).

views/model/node_detail_data.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ type NodeDetailData struct {
2424
// MetricsSourceType indicates the active metrics source ("prometheus", "metrics-server", or "")
2525
// Used by detail panels to determine which metrics visualizations to show
2626
MetricsSourceType string
27+
28+
// Network/Disk I/O rates for this specific node (Prometheus only)
29+
NetworkRxRate float64
30+
NetworkTxRate float64
31+
DiskReadRate float64
32+
DiskWriteRate float64
2733
}
2834

2935
// MetricSample represents a single point in time for metrics history

views/model/pod_detail_data.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ type PodDetailData struct {
2626
// MetricsSourceType indicates the active metrics source ("prometheus", "metrics-server", or "")
2727
// Used by detail panels to determine which metrics visualizations to show
2828
MetricsSourceType string
29+
30+
// Network/Disk I/O rates for this pod (Prometheus only)
31+
NetworkRxRate float64
32+
NetworkTxRate float64
33+
DiskReadRate float64
34+
DiskWriteRate float64
2935
}
3036

3137
// ContainerUsage holds formatted CPU and memory usage strings for a container

views/node/detail.go

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ type DetailPanel struct {
6262
onPodSelected NodeSelectedCallback
6363
onBack func()
6464
onFooterContextChange func(focusedPanel string)
65+
66+
// Adaptive scaling for Net/Disk sparklines
67+
peakNetRate float64
68+
peakDiskRate float64
6569
}
6670

6771
// NewDetailPanel creates a new node detail panel
@@ -256,6 +260,8 @@ func (p *DetailPanel) DrawBody(data interface{}) {
256260
}
257261
if newNodeName != p.currentNodeName {
258262
p.resetSparklines()
263+
p.peakNetRate = 0 // Reset adaptive scaling peaks
264+
p.peakDiskRate = 0
259265
p.currentNodeName = newNodeName
260266

261267
// Populate sparklines from history if available
@@ -394,8 +400,45 @@ func (p *DetailPanel) drawSparklines() {
394400

395401
// Update network/disk sparklines in prometheus mode
396402
if prometheusMode {
397-
p.sparklineRow.UpdateNET(0.01, " NET [gray]↓n/a ↑n/a[-] ")
398-
p.sparklineRow.UpdateDisk(0.01, " DISK [gray]R:n/a W:n/a[-] ")
403+
// Network sparkline with adaptive scaling
404+
netTitle := fmt.Sprintf(" Net ↓%s ↑%s ",
405+
ui.FormatBytesRate(p.data.NetworkRxRate),
406+
ui.FormatBytesRate(p.data.NetworkTxRate))
407+
combinedNetRate := p.data.NetworkRxRate + p.data.NetworkTxRate
408+
409+
// Adaptive scaling: track peak, use minimum 512 KB/s baseline
410+
if combinedNetRate > p.peakNetRate {
411+
p.peakNetRate = combinedNetRate
412+
}
413+
effectiveNetMax := p.peakNetRate
414+
if effectiveNetMax < 512*1024 {
415+
effectiveNetMax = 512 * 1024
416+
}
417+
netNormalized := combinedNetRate / effectiveNetMax
418+
if netNormalized > 1 {
419+
netNormalized = 1
420+
}
421+
p.sparklineRow.UpdateNET(netNormalized, netTitle)
422+
423+
// Disk sparkline with adaptive scaling
424+
diskTitle := fmt.Sprintf(" Disk R:%s W:%s ",
425+
ui.FormatBytesRate(p.data.DiskReadRate),
426+
ui.FormatBytesRate(p.data.DiskWriteRate))
427+
combinedDiskRate := p.data.DiskReadRate + p.data.DiskWriteRate
428+
429+
// Adaptive scaling: track peak, use minimum 512 KB/s baseline
430+
if combinedDiskRate > p.peakDiskRate {
431+
p.peakDiskRate = combinedDiskRate
432+
}
433+
effectiveDiskMax := p.peakDiskRate
434+
if effectiveDiskMax < 512*1024 {
435+
effectiveDiskMax = 512 * 1024
436+
}
437+
diskNormalized := combinedDiskRate / effectiveDiskMax
438+
if diskNormalized > 1 {
439+
diskNormalized = 1
440+
}
441+
p.sparklineRow.UpdateDisk(diskNormalized, diskTitle)
399442
}
400443
}
401444

views/overview/main_panel.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -934,6 +934,15 @@ func (p *MainPanel) buildNodeDetailData(ctx context.Context, nodeName string, no
934934
// Set metrics source type for conditional display in detail panel
935935
if p.metricsSource != nil {
936936
detailData.MetricsSourceType = p.metricsSource.GetSourceInfo().Type
937+
938+
// Fetch per-node network/disk rates for sparklines (Prometheus only)
939+
// GetNodeMetrics uses label filter {node: nodeName} to get only this node's metrics
940+
if nodeMetrics, err := p.metricsSource.GetNodeMetrics(ctx, nodeName); err == nil && nodeMetrics != nil {
941+
detailData.NetworkRxRate = nodeMetrics.NetworkRxRate
942+
detailData.NetworkTxRate = nodeMetrics.NetworkTxRate
943+
detailData.DiskReadRate = nodeMetrics.DiskReadRate
944+
detailData.DiskWriteRate = nodeMetrics.DiskWriteRate
945+
}
937946
}
938947

939948
// Fetch metrics history for sparklines (if available)
@@ -1019,6 +1028,14 @@ func (p *MainPanel) buildPodDetailData(ctx context.Context, podKey string, podMo
10191028
// Set metrics source type for conditional display in detail panel
10201029
if p.metricsSource != nil {
10211030
detailData.MetricsSourceType = p.metricsSource.GetSourceInfo().Type
1031+
1032+
// Fetch pod-level network/disk rates for sparklines (Prometheus only)
1033+
if netRx, netTx, diskRead, diskWrite, err := p.metricsSource.GetPodNetworkDiskMetrics(ctx, namespace, podName); err == nil {
1034+
detailData.NetworkRxRate = netRx
1035+
detailData.NetworkTxRate = netTx
1036+
detailData.DiskReadRate = diskRead
1037+
detailData.DiskWriteRate = diskWrite
1038+
}
10221039
}
10231040

10241041
// Fetch metrics history for sparklines (if available)

views/overview/summary_panel.go

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ type clusterSummaryPanel struct {
3030

3131
// Dynamic layout tracking
3232
lastTerminalHeight int
33+
34+
// Adaptive scaling for Net/Disk sparklines
35+
peakNetRate float64
36+
peakDiskRate float64
3337
}
3438

3539
func NewClusterSummaryPanel(app *application.Application, title string) ui.Panel {
@@ -239,23 +243,41 @@ func (p *clusterSummaryPanel) DrawBody(data interface{}) {
239243

240244
// === Prometheus-only: Network, Disk, Enhanced Stats ===
241245
if p.prometheusMode {
242-
// Network sparkline
246+
// Network sparkline with adaptive scaling
243247
netTitle := fmt.Sprintf(" Net ↓%s ↑%s ",
244248
ui.FormatBytesRate(summary.NetworkRxRate),
245249
ui.FormatBytesRate(summary.NetworkTxRate))
246250
combinedNetRate := summary.NetworkRxRate + summary.NetworkTxRate
247-
netNormalized := combinedNetRate / (128 * 1024 * 1024) // 128 MB/s baseline
251+
252+
// Adaptive scaling: track peak, use minimum 512 KB/s baseline
253+
if combinedNetRate > p.peakNetRate {
254+
p.peakNetRate = combinedNetRate
255+
}
256+
effectiveNetMax := p.peakNetRate
257+
if effectiveNetMax < 512*1024 { // Min 512 KB/s baseline
258+
effectiveNetMax = 512 * 1024
259+
}
260+
netNormalized := combinedNetRate / effectiveNetMax
248261
if netNormalized > 1 {
249262
netNormalized = 1
250263
}
251264
p.sparklineRow.UpdateNET(netNormalized, netTitle)
252265

253-
// Disk sparkline
266+
// Disk sparkline with adaptive scaling
254267
diskTitle := fmt.Sprintf(" Disk R:%s W:%s ",
255268
ui.FormatBytesRate(summary.DiskReadRate),
256269
ui.FormatBytesRate(summary.DiskWriteRate))
257270
combinedDiskRate := summary.DiskReadRate + summary.DiskWriteRate
258-
diskNormalized := combinedDiskRate / (500 * 1024 * 1024) // 500 MB/s baseline
271+
272+
// Adaptive scaling: track peak, use minimum 512 KB/s baseline
273+
if combinedDiskRate > p.peakDiskRate {
274+
p.peakDiskRate = combinedDiskRate
275+
}
276+
effectiveDiskMax := p.peakDiskRate
277+
if effectiveDiskMax < 512*1024 { // Min 512 KB/s baseline
278+
effectiveDiskMax = 512 * 1024
279+
}
280+
diskNormalized := combinedDiskRate / effectiveDiskMax
259281
if diskNormalized > 1 {
260282
diskNormalized = 1
261283
}

views/pod/detail.go

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ type DetailPanel struct {
6161
// Sparkline row component for metrics visualization
6262
sparklineRow *ui.SparklineRow
6363

64+
// Adaptive scaling for Disk sparkline
65+
peakDiskRate float64
66+
6467
// Callbacks
6568
onNodeNavigate NodeNavigationCallback
6669
onContainerSelected ContainerSelectedCallback
@@ -324,6 +327,7 @@ func (p *DetailPanel) DrawBody(data interface{}) {
324327
}
325328
if newPodKey != p.currentPodKey {
326329
p.resetSparklines()
330+
p.peakDiskRate = 0 // Reset adaptive scaling peak
327331
p.currentPodKey = newPodKey
328332

329333
// Populate sparklines from history if available
@@ -460,8 +464,28 @@ func (p *DetailPanel) drawSparklines() {
460464

461465
// Update network/disk sparklines in prometheus mode
462466
if prometheusMode {
463-
p.sparklineRow.UpdateNET(0.01, " NET [gray]↓n/a ↑n/a[-] ")
464-
p.sparklineRow.UpdateDisk(0.01, " DISK [gray]R:n/a W:n/a[-] ")
467+
// Network sparkline - pod-level network metrics unavailable on containerd
468+
p.sparklineRow.UpdateNET(0, " Net [gray](unavailable)[-] ")
469+
470+
// Disk sparkline with adaptive scaling
471+
diskTitle := fmt.Sprintf(" Disk R:%s W:%s ",
472+
ui.FormatBytesRate(p.data.DiskReadRate),
473+
ui.FormatBytesRate(p.data.DiskWriteRate))
474+
combinedDiskRate := p.data.DiskReadRate + p.data.DiskWriteRate
475+
476+
// Adaptive scaling: track peak, use minimum 512 KB/s baseline
477+
if combinedDiskRate > p.peakDiskRate {
478+
p.peakDiskRate = combinedDiskRate
479+
}
480+
effectiveDiskMax := p.peakDiskRate
481+
if effectiveDiskMax < 512*1024 { // Min 512 KB/s baseline
482+
effectiveDiskMax = 512 * 1024
483+
}
484+
diskNormalized := combinedDiskRate / effectiveDiskMax
485+
if diskNormalized > 1 {
486+
diskNormalized = 1
487+
}
488+
p.sparklineRow.UpdateDisk(diskNormalized, diskTitle)
465489
}
466490
}
467491

0 commit comments

Comments
 (0)