Add complete TUI application for monitoring Kubernetes clusters and host systems. Features include: Core features: - Collector framework with concurrent scheduling - Host collectors: disk, memory, load, network - Kubernetes collectors: pods, nodes, workloads, events with informers - Issue deduplication, state management, and resolve-after logic - Bubble Tea TUI with table view, details pane, and filtering - JSON export functionality UX improvements: - Help overlay with keybindings - Priority/category filters with visual indicators - Direct priority jump (0/1/2/3) - Bulk acknowledge (Shift+A) - Clipboard copy (y) - Theme toggle (T) - Age format toggle (d) - Wide title toggle (t) - Vi-style navigation (j/k) - Home/End jump (g/G) - Rollup drill-down in details Robustness: - Grace period for unreachable clusters - Rollups for high-volume issues - Flap suppression - RBAC error handling Files: All core application code with tests for host collectors, engine, store, model, and export packages.
129 lines
3.2 KiB
Go
129 lines
3.2 KiB
Go
package k8s
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
|
|
"tower/internal/model"
|
|
)
|
|
|
|
// RollupKey groups similar issues to reduce UI noise.
|
|
// Required grouping per prompt: (namespace, reason, kind).
|
|
type RollupKey struct {
|
|
Namespace string
|
|
Reason string
|
|
Kind string
|
|
}
|
|
|
|
// Rollup groups issues by (namespace, reason, kind). For any group with size >=
|
|
// threshold, it emits a single rollup issue and removes the individual issues
|
|
// from the output.
|
|
//
|
|
// Rollup issues use Priority of the max priority in the group.
|
|
func Rollup(issues []model.Issue, threshold int, sampleN int) []model.Issue {
|
|
if threshold <= 0 {
|
|
threshold = 20
|
|
}
|
|
if sampleN <= 0 {
|
|
sampleN = 5
|
|
}
|
|
|
|
groups := make(map[RollupKey][]model.Issue, 32)
|
|
ungrouped := make([]model.Issue, 0, len(issues))
|
|
|
|
for _, iss := range issues {
|
|
kind := strings.TrimSpace(iss.Evidence["kind"])
|
|
reason := strings.TrimSpace(iss.Evidence["reason"])
|
|
ns := strings.TrimSpace(iss.Evidence["namespace"])
|
|
if kind == "" || reason == "" {
|
|
ungrouped = append(ungrouped, iss)
|
|
continue
|
|
}
|
|
k := RollupKey{Namespace: ns, Reason: reason, Kind: kind}
|
|
groups[k] = append(groups[k], iss)
|
|
}
|
|
|
|
rolled := make([]model.Issue, 0, len(issues))
|
|
rolled = append(rolled, ungrouped...)
|
|
|
|
// Stable order for determinism.
|
|
keys := make([]RollupKey, 0, len(groups))
|
|
for k := range groups {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Slice(keys, func(i, j int) bool {
|
|
if keys[i].Namespace != keys[j].Namespace {
|
|
return keys[i].Namespace < keys[j].Namespace
|
|
}
|
|
if keys[i].Kind != keys[j].Kind {
|
|
return keys[i].Kind < keys[j].Kind
|
|
}
|
|
return keys[i].Reason < keys[j].Reason
|
|
})
|
|
|
|
for _, k := range keys {
|
|
grp := groups[k]
|
|
if len(grp) < threshold {
|
|
rolled = append(rolled, grp...)
|
|
continue
|
|
}
|
|
|
|
// determine max priority
|
|
maxP := model.PriorityP3
|
|
for _, iss := range grp {
|
|
if iss.Priority.Weight() > maxP.Weight() {
|
|
maxP = iss.Priority
|
|
}
|
|
}
|
|
|
|
titleNS := ""
|
|
if k.Namespace != "" {
|
|
titleNS = fmt.Sprintf(" (ns=%s)", k.Namespace)
|
|
}
|
|
title := fmt.Sprintf("%d %ss %s%s", len(grp), strings.ToLower(k.Kind), k.Reason, titleNS)
|
|
|
|
samples := make([]string, 0, sampleN)
|
|
for i := 0; i < len(grp) && i < sampleN; i++ {
|
|
s := grp[i].Title
|
|
if s == "" {
|
|
s = grp[i].ID
|
|
}
|
|
samples = append(samples, s)
|
|
}
|
|
|
|
rolled = append(rolled, model.Issue{
|
|
ID: fmt.Sprintf("k8s:rollup:%s:%s:%s", k.Namespace, k.Kind, k.Reason),
|
|
Category: model.CategoryKubernetes,
|
|
Priority: maxP,
|
|
Title: title,
|
|
Details: "Many similar Kubernetes issues were aggregated into this rollup.",
|
|
Evidence: map[string]string{
|
|
"kind": k.Kind,
|
|
"reason": k.Reason,
|
|
"namespace": k.Namespace,
|
|
"count": fmt.Sprintf("%d", len(grp)),
|
|
"samples": strings.Join(samples, " | "),
|
|
},
|
|
SuggestedFix: "Filter events/pods and inspect samples with kubectl describe.",
|
|
})
|
|
}
|
|
|
|
return rolled
|
|
}
|
|
|
|
// CapIssues enforces a hard cap after rollups. This should be applied after
|
|
// sorting by default sort order (priority desc, recency desc), but we keep this
|
|
// helper pure and simple.
|
|
func CapIssues(issues []model.Issue, max int) []model.Issue {
|
|
if max <= 0 {
|
|
max = 200
|
|
}
|
|
if len(issues) <= max {
|
|
return issues
|
|
}
|
|
out := make([]model.Issue, max)
|
|
copy(out, issues[:max])
|
|
return out
|
|
}
|