Fixed more padding & layout issues, renamed 'Node Roles' to 'Legend', cleaned up cluster metrics, etc

This commit is contained in:
Dionysus 2024-12-06 12:21:56 -05:00
parent 0912d77018
commit 66e5e4a1b3
Signed by: acidvegas
GPG Key ID: EF4B922DB85DC9DE
2 changed files with 249 additions and 171 deletions

BIN
elastop

Binary file not shown.

View File

@ -32,10 +32,6 @@ type ClusterStats struct {
SizeInBytes int64 `json:"size_in_bytes"` SizeInBytes int64 `json:"size_in_bytes"`
TotalSizeInBytes int64 `json:"total_size_in_bytes"` TotalSizeInBytes int64 `json:"total_size_in_bytes"`
} `json:"store"` } `json:"store"`
Mappings struct {
TotalDeduplicatedFieldCount int `json:"total_deduplicated_field_count"`
TotalDeduplicatedMappingSizeInBytes int64 `json:"total_deduplicated_mapping_size_in_bytes"`
} `json:"mappings"`
} `json:"indices"` } `json:"indices"`
Nodes struct { Nodes struct {
Total int `json:"total"` Total int `json:"total"`
@ -71,18 +67,8 @@ type NodesInfo struct {
PrettyName string `json:"pretty_name"` PrettyName string `json:"pretty_name"`
} `json:"os"` } `json:"os"`
Process struct { Process struct {
ID int `json:"id"` ID int `json:"id"`
Mlockall bool `json:"mlockall"`
} `json:"process"` } `json:"process"`
Settings struct {
Node struct {
Attr struct {
ML struct {
MachineMem string `json:"machine_memory"`
} `json:"ml"`
} `json:"attr"`
} `json:"node"`
} `json:"settings"`
} `json:"nodes"` } `json:"nodes"`
} }
@ -97,7 +83,6 @@ type IndexStats []struct {
type IndexActivity struct { type IndexActivity struct {
LastDocsCount int LastDocsCount int
IsActive bool
InitialDocsCount int InitialDocsCount int
StartTime time.Time StartTime time.Time
} }
@ -213,17 +198,6 @@ var (
var indexActivities = make(map[string]*IndexActivity) var indexActivities = make(map[string]*IndexActivity)
type IngestionEvent struct {
Index string
DocCount int
Timestamp time.Time
}
type CatNodesStats struct {
Load1m string `json:"load_1m"`
Name string `json:"name"`
}
var ( var (
showNodes = true showNodes = true
showRoles = true showRoles = true
@ -255,6 +229,11 @@ var (
apiKey string apiKey string
) )
type CatNodesStats struct {
Load1m string `json:"load_1m"`
Name string `json:"name"`
}
func bytesToHuman(bytes int64) string { func bytesToHuman(bytes int64) string {
const unit = 1024 const unit = 1024
if bytes < unit { if bytes < unit {
@ -296,24 +275,6 @@ func convertSizeFormat(sizeStr string) string {
return fmt.Sprintf("%d%s", int(size), unit) return fmt.Sprintf("%d%s", int(size), unit)
} }
func formatResourceSize(bytes int64) string {
const unit = 1024
if bytes < unit {
return fmt.Sprintf("%4d B", bytes)
}
units := []string{"B", "K", "M", "G", "T", "P"}
exp := 0
val := float64(bytes)
for val >= unit && exp < len(units)-1 {
val /= unit
exp++
}
return fmt.Sprintf("%3d%s", int(val), units[exp])
}
func getPercentageColor(percent float64) string { func getPercentageColor(percent float64) string {
switch { switch {
case percent < 30: case percent < 30:
@ -382,9 +343,9 @@ var roleColors = map[string]string{
"data_warm": "#bd93f9", // purple "data_warm": "#bd93f9", // purple
"data_cold": "#f1fa8c", // yellow "data_cold": "#f1fa8c", // yellow
"data_frozen": "#ff79c6", // pink "data_frozen": "#ff79c6", // pink
"ingest": "#87cefa", // light sky blue (was gray) "ingest": "#87cefa", // light sky blue
"ml": "#6272a4", // blue gray "ml": "#6272a4", // blue gray
"remote_cluster_client": "#dda0dd", // plum (was burgundy) "remote_cluster_client": "#dda0dd", // plum
"transform": "#689d6a", // forest green "transform": "#689d6a", // forest green
"voting_only": "#458588", // teal "voting_only": "#458588", // teal
"coordinating_only": "#d65d0e", // burnt orange "coordinating_only": "#d65d0e", // burnt orange
@ -430,12 +391,21 @@ func formatNodeRoles(roles []string) string {
nodeRoles[role] = true nodeRoles[role] = true
} }
// Create ordered list of role keys for consistent display // Create ordered list of role keys based on their letters
orderedRoles := []string{ orderedRoles := []string{
"master", "data", "data_content", "data_hot", "data_content", // C
"data_warm", "data_cold", "data_frozen", "ingest", "data", // D
"ml", "remote_cluster_client", "transform", "data_frozen", // F
"voting_only", "coordinating_only", "data_hot", // H
"ingest", // I
"data_cold", // K
"ml", // L
"master", // M
"coordinating_only", // O
"remote_cluster_client", // R
"transform", // T
"voting_only", // V
"data_warm", // W
} }
result := "" result := ""
@ -492,7 +462,18 @@ func updateGridLayout(grid *tview.Grid, showRoles, showIndices, showMetrics bool
visiblePanels++ visiblePanels++
} }
// Adjust row configuration based on whether nodes panel is shown // When only nodes panel is visible, use a single column layout
if showNodes && visiblePanels == 0 {
grid.SetRows(3, 0) // Header and nodes only
grid.SetColumns(0) // Single full-width column
// Add header and nodes panel
grid.AddItem(header, 0, 0, 1, 1, 0, 0, false)
grid.AddItem(nodesPanel, 1, 0, 1, 1, 0, 0, false)
return
}
// Rest of the layout logic for when bottom panels are visible
if showNodes { if showNodes {
grid.SetRows(3, 0, 0) // Header, nodes, bottom panels grid.SetRows(3, 0, 0) // Header, nodes, bottom panels
} else { } else {
@ -503,16 +484,16 @@ func updateGridLayout(grid *tview.Grid, showRoles, showIndices, showMetrics bool
switch { switch {
case visiblePanels == 3: case visiblePanels == 3:
if showRoles { if showRoles {
grid.SetColumns(30, -2, -1) // Changed from 20 to 30 for roles panel width grid.SetColumns(30, -2, -1)
} }
case visiblePanels == 2: case visiblePanels == 2:
if showRoles { if showRoles {
grid.SetColumns(30, 0) // Changed from 20 to 30 for roles panel width grid.SetColumns(30, 0)
} else { } else {
grid.SetColumns(-1, -1) // Equal split between two panels grid.SetColumns(-1, -1)
} }
case visiblePanels == 1: case visiblePanels == 1:
grid.SetColumns(0) // Single column takes full width grid.SetColumns(0)
} }
// Always show header at top spanning all columns // Always show header at top spanning all columns
@ -702,35 +683,35 @@ func main() {
return return
} }
// Calculate aggregate metrics // Query and indexing metrics
var ( var (
totalQueries int64 totalQueries int64
totalQueryTime float64 totalQueryTime int64
totalIndexing int64 totalIndexing int64
totalIndexingTime float64 totalIndexTime int64
totalCPUPercent int totalSegments int64
totalMemoryUsed int64
totalMemoryTotal int64
totalHeapUsed int64
totalHeapMax int64
totalGCCollections int64
totalGCTime float64
nodeCount int
) )
for _, node := range nodesStats.Nodes { for _, node := range nodesStats.Nodes {
totalQueries += node.Indices.Search.QueryTotal totalQueries += node.Indices.Search.QueryTotal
totalQueryTime += float64(node.Indices.Search.QueryTimeInMillis) / 1000 totalQueryTime += node.Indices.Search.QueryTimeInMillis
totalIndexing += node.Indices.Indexing.IndexTotal totalIndexing += node.Indices.Indexing.IndexTotal
totalIndexingTime += float64(node.Indices.Indexing.IndexTimeInMillis) / 1000 totalIndexTime += node.Indices.Indexing.IndexTimeInMillis
totalCPUPercent += node.OS.CPU.Percent totalSegments += node.Indices.Segments.Count
totalMemoryUsed += node.OS.Memory.UsedInBytes }
totalMemoryTotal += node.OS.Memory.TotalInBytes
totalHeapUsed += node.JVM.Memory.HeapUsedInBytes queryRate := float64(totalQueries) / float64(totalQueryTime) * 1000 // queries per second
totalHeapMax += node.JVM.Memory.HeapMaxInBytes indexRate := float64(totalIndexing) / float64(totalIndexTime) * 1000 // docs per second
// GC metrics
var (
totalGCCollections int64
totalGCTime int64
)
for _, node := range nodesStats.Nodes {
totalGCCollections += node.JVM.GC.Collectors.Young.CollectionCount + node.JVM.GC.Collectors.Old.CollectionCount totalGCCollections += node.JVM.GC.Collectors.Young.CollectionCount + node.JVM.GC.Collectors.Old.CollectionCount
totalGCTime += float64(node.JVM.GC.Collectors.Young.CollectionTimeInMillis+node.JVM.GC.Collectors.Old.CollectionTimeInMillis) / 1000 totalGCTime += node.JVM.GC.Collectors.Young.CollectionTimeInMillis + node.JVM.GC.Collectors.Old.CollectionTimeInMillis
nodeCount++
} }
// Update header // Update header
@ -741,7 +722,7 @@ func main() {
}[clusterStats.Status] }[clusterStats.Status]
// Get max lengths after fetching node and index info // Get max lengths after fetching node and index info
maxNodeNameLen, maxIndexNameLen, maxTransportLen := getMaxLengths(nodesInfo, indicesStats) maxNodeNameLen, maxIndexNameLen, maxTransportLen, maxIngestedLen := getMaxLengths(nodesInfo, indicesStats)
// Update header with dynamic padding // Update header with dynamic padding
header.Clear() header.Clear()
@ -861,7 +842,7 @@ func main() {
// Update indices panel with dynamic width // Update indices panel with dynamic width
indicesPanel.Clear() indicesPanel.Clear()
fmt.Fprintf(indicesPanel, "[::b][#00ffff][[#ff5555]4[#00ffff]] Indices Information[::-]\n\n") fmt.Fprintf(indicesPanel, "[::b][#00ffff][[#ff5555]4[#00ffff]] Indices Information[::-]\n\n")
fmt.Fprint(indicesPanel, getIndicesPanelHeader(maxIndexNameLen)) fmt.Fprint(indicesPanel, getIndicesPanelHeader(maxIndexNameLen, maxIngestedLen))
// Update index entries with dynamic width // Update index entries with dynamic width
var indices []indexInfo var indices []indexInfo
@ -943,14 +924,14 @@ func main() {
streamIndicator = "[#bd93f9]⚫[white]" streamIndicator = "[#bd93f9]⚫[white]"
} }
// Calculate document changes // Calculate document changes with dynamic padding
activity := indexActivities[idx.index] activity := indexActivities[idx.index]
ingestedStr := "" ingestedStr := ""
if activity != nil && activity.InitialDocsCount < idx.docs { if activity != nil && activity.InitialDocsCount < idx.docs {
docChange := idx.docs - activity.InitialDocsCount docChange := idx.docs - activity.InitialDocsCount
ingestedStr = fmt.Sprintf("[green]%-12s", fmt.Sprintf("+%s", formatNumber(docChange))) ingestedStr = fmt.Sprintf("[green]%-*s", maxIngestedLen, fmt.Sprintf("+%s", formatNumber(docChange)))
} else { } else {
ingestedStr = fmt.Sprintf("%-12s", "") ingestedStr = fmt.Sprintf("%-*s", maxIngestedLen, "")
} }
// Format indexing rate // Format indexing rate
@ -968,16 +949,17 @@ func main() {
// Convert the size format before display // Convert the size format before display
sizeStr := convertSizeFormat(idx.storeSize) sizeStr := convertSizeFormat(idx.storeSize)
fmt.Fprintf(indicesPanel, "%s %s[%s]%-*s[white] [#444444]│[white] %15s [#444444]│[white] %5s [#444444]│[white] %6s [#444444]│[white] %8s [#444444]│[white] %s [#444444]│[white] %-8s\n", fmt.Fprintf(indicesPanel, "%s %s[%s]%-*s[white] [#444444]│[white] %13s [#444444]│[white] %5s [#444444]│[white] %6s [#444444]│[white] %8s [#444444]│[white] %-*s [#444444]│[white] %-8s\n",
writeIcon, writeIcon,
streamIndicator, streamIndicator,
getHealthColor(idx.health), getHealthColor(idx.health),
maxIndexNameLen-1, maxIndexNameLen,
idx.index, idx.index,
formatNumber(idx.docs), formatNumber(idx.docs),
sizeStr, sizeStr,
idx.priShards, idx.priShards,
idx.replicas, idx.replicas,
maxIngestedLen,
ingestedStr, ingestedStr,
rateStr) rateStr)
} }
@ -1021,17 +1003,28 @@ func main() {
metricsPanel.Clear() metricsPanel.Clear()
fmt.Fprintf(metricsPanel, "[::b][#00ffff][[#ff5555]5[#00ffff]] Cluster Metrics[::-]\n\n") fmt.Fprintf(metricsPanel, "[::b][#00ffff][[#ff5555]5[#00ffff]] Cluster Metrics[::-]\n\n")
// Define metrics keys and find the longest one // Define metrics keys with proper grouping
metricKeys := []string{ metricKeys := []string{
// System metrics
"CPU", "CPU",
"Disk",
"Heap",
"Memory", "Memory",
"Heap",
"Disk",
// Network metrics
"Network TX", "Network TX",
"Network RX", "Network RX",
"HTTP Connections",
// Performance metrics
"Query Rate",
"Index Rate",
// Miscellaneous
"Snapshots", "Snapshots",
} }
// Find the longest key for proper alignment
maxKeyLength := 0 maxKeyLength := 0
for _, key := range metricKeys { for _, key := range metricKeys {
if len(key) > maxKeyLength { if len(key) > maxKeyLength {
@ -1039,21 +1032,21 @@ func main() {
} }
} }
// Helper function for metric lines with dynamic key padding // Add padding for better visual separation
maxKeyLength += 2
// Helper function for metric lines with proper alignment
formatMetric := func(name string, value string) string { formatMetric := func(name string, value string) string {
return fmt.Sprintf("[#00ffff]%-*s[white] %s\n", maxKeyLength, name+":", value) return fmt.Sprintf("[#00ffff]%-*s[white] %s\n", maxKeyLength, name+":", value)
} }
// CPU metrics - show only CPU percent and total processors // CPU metrics
totalProcessors := 0 totalProcessors := 0
for _, node := range nodesInfo.Nodes { for _, node := range nodesInfo.Nodes {
totalProcessors += node.OS.AvailableProcessors totalProcessors += node.OS.AvailableProcessors
} }
cpuPercent := float64(clusterStats.Process.CPU.Percent) cpuPercent := float64(clusterStats.Process.CPU.Percent)
fmt.Fprint(metricsPanel, formatMetric("CPU", fmt.Sprintf("[%s]%7.1f%%[white] [#444444](%d processors)[white]", fmt.Fprint(metricsPanel, formatMetric("CPU", fmt.Sprintf("%7.1f%% [#444444](%d processors)[white]", cpuPercent, totalProcessors)))
getPercentageColor(cpuPercent),
cpuPercent,
totalProcessors)))
// Disk metrics // Disk metrics
diskUsed := getTotalSize(nodesStats) diskUsed := getTotalSize(nodesStats)
@ -1065,13 +1058,22 @@ func main() {
getPercentageColor(diskPercent), getPercentageColor(diskPercent),
diskPercent))) diskPercent)))
// Calculate heap totals // Calculate heap and memory totals
totalHeapUsed = 0 var (
totalHeapMax = 0 totalHeapUsed int64
totalHeapMax int64
totalMemoryUsed int64
totalMemoryTotal int64
)
for _, node := range nodesStats.Nodes { for _, node := range nodesStats.Nodes {
totalHeapUsed += node.JVM.Memory.HeapUsedInBytes totalHeapUsed += node.JVM.Memory.HeapUsedInBytes
totalHeapMax += node.JVM.Memory.HeapMaxInBytes totalHeapMax += node.JVM.Memory.HeapMaxInBytes
totalMemoryUsed += node.OS.Memory.UsedInBytes
totalMemoryTotal += node.OS.Memory.TotalInBytes
} }
// Heap metrics
heapPercent := float64(totalHeapUsed) / float64(totalHeapMax) * 100 heapPercent := float64(totalHeapUsed) / float64(totalHeapMax) * 100
fmt.Fprint(metricsPanel, formatMetric("Heap", fmt.Sprintf("%8s / %8s [%s]%5.1f%%[white]", fmt.Fprint(metricsPanel, formatMetric("Heap", fmt.Sprintf("%8s / %8s [%s]%5.1f%%[white]",
bytesToHuman(totalHeapUsed), bytesToHuman(totalHeapUsed),
@ -1079,13 +1081,7 @@ func main() {
getPercentageColor(heapPercent), getPercentageColor(heapPercent),
heapPercent))) heapPercent)))
// Calculate memory totals // Memory metrics
totalMemoryUsed = 0
totalMemoryTotal = 0
for _, node := range nodesStats.Nodes {
totalMemoryUsed += node.OS.Memory.UsedInBytes
totalMemoryTotal += node.OS.Memory.TotalInBytes
}
memoryPercent := float64(totalMemoryUsed) / float64(totalMemoryTotal) * 100 memoryPercent := float64(totalMemoryUsed) / float64(totalMemoryTotal) * 100
fmt.Fprint(metricsPanel, formatMetric("Memory", fmt.Sprintf("%8s / %8s [%s]%5.1f%%[white]", fmt.Fprint(metricsPanel, formatMetric("Memory", fmt.Sprintf("%8s / %8s [%s]%5.1f%%[white]",
bytesToHuman(totalMemoryUsed), bytesToHuman(totalMemoryUsed),
@ -1094,50 +1090,19 @@ func main() {
memoryPercent))) memoryPercent)))
// Network metrics // Network metrics
fmt.Fprint(metricsPanel, formatMetric("Network TX", fmt.Sprintf("%7s", bytesToHuman(getTotalNetworkTX(nodesStats))))) fmt.Fprint(metricsPanel, formatMetric("Network TX", fmt.Sprintf(" %7s", bytesToHuman(getTotalNetworkTX(nodesStats)))))
fmt.Fprint(metricsPanel, formatMetric("Network RX", fmt.Sprintf("%7s", bytesToHuman(getTotalNetworkRX(nodesStats))))) fmt.Fprint(metricsPanel, formatMetric("Network RX", fmt.Sprintf(" %7s", bytesToHuman(getTotalNetworkRX(nodesStats)))))
fmt.Fprint(metricsPanel, formatMetric("Snapshots", fmt.Sprintf("%7d", clusterStats.Snapshots.Count)))
// Update roles panel // HTTP Connections and Shard metrics - right aligned to match Network RX 'G'
rolesPanel.Clear() fmt.Fprint(metricsPanel, formatMetric("HTTP Connections", fmt.Sprintf("%8s", formatNumber(int(getTotalHTTPConnections(nodesStats))))))
fmt.Fprintf(rolesPanel, "[::b][#00ffff][[#ff5555]3[#00ffff]] Node Roles[::-]\n\n") fmt.Fprint(metricsPanel, formatMetric("Query Rate", fmt.Sprintf("%6s/s", formatNumber(int(queryRate)))))
fmt.Fprint(metricsPanel, formatMetric("Index Rate", fmt.Sprintf("%6s/s", formatNumber(int(indexRate)))))
// Create a map of used roles // Snapshots
usedRoles := make(map[string]bool) fmt.Fprint(metricsPanel, formatMetric("Snapshots", fmt.Sprintf("%8s", formatNumber(clusterStats.Snapshots.Count))))
for _, nodeInfo := range nodesInfo.Nodes {
for _, role := range nodeInfo.Roles {
usedRoles[role] = true
}
}
// Display roles in the roles panel if showRoles {
roleLegend := [][2]string{ updateRolesPanel(rolesPanel, nodesInfo)
{"C", "data_content"},
{"D", "data"},
{"F", "data_frozen"},
{"H", "data_hot"},
{"I", "ingest"},
{"K", "data_cold"},
{"L", "ml"},
{"M", "master"},
{"O", "coordinating_only"},
{"R", "remote_cluster_client"},
{"T", "transform"},
{"V", "voting_only"},
{"W", "data_warm"},
}
for _, role := range roleLegend {
if usedRoles[role[1]] {
fmt.Fprintf(rolesPanel, "[%s]%s[white] %s\n",
roleColors[role[1]],
role[0],
legendLabels[role[1]])
} else {
fmt.Fprintf(rolesPanel, "[#444444]%s %s\n",
role[0],
legendLabels[role[1]])
}
} }
} }
@ -1201,10 +1166,11 @@ func getTotalNetworkRX(stats NodesStats) int64 {
return total return total
} }
func getMaxLengths(nodesInfo NodesInfo, indicesStats IndexStats) (int, int, int) { func getMaxLengths(nodesInfo NodesInfo, indicesStats IndexStats) (int, int, int, int) {
maxNodeNameLen := 0 maxNodeNameLen := 0
maxIndexNameLen := 0 maxIndexNameLen := 0
maxTransportLen := 0 maxTransportLen := 0
maxIngestedLen := 8 // Start with "Ingested" header length
// Get max node name and transport address length // Get max node name and transport address length
for _, nodeInfo := range nodesInfo.Nodes { for _, nodeInfo := range nodesInfo.Nodes {
@ -1216,26 +1182,38 @@ func getMaxLengths(nodesInfo NodesInfo, indicesStats IndexStats) (int, int, int)
} }
} }
// Get max index name length only for visible indices // Get max index name length and calculate max ingested length
for _, index := range indicesStats { for _, index := range indicesStats {
// Only consider indices that should be visible based on showHiddenIndices
if (showHiddenIndices || !strings.HasPrefix(index.Index, ".")) && index.DocsCount != "0" { if (showHiddenIndices || !strings.HasPrefix(index.Index, ".")) && index.DocsCount != "0" {
if len(index.Index) > maxIndexNameLen { if len(index.Index) > maxIndexNameLen {
maxIndexNameLen = len(index.Index) maxIndexNameLen = len(index.Index)
} }
docs := 0
fmt.Sscanf(index.DocsCount, "%d", &docs)
if activity := indexActivities[index.Index]; activity != nil {
if activity.InitialDocsCount < docs {
docChange := docs - activity.InitialDocsCount
ingestedStr := fmt.Sprintf("+%s", formatNumber(docChange))
if len(ingestedStr) > maxIngestedLen {
maxIngestedLen = len(ingestedStr)
}
}
}
} }
} }
// Add padding // Add padding
maxNodeNameLen += 2 maxNodeNameLen += 2
maxIndexNameLen += 1 // Single space before separator maxIndexNameLen += 1 // Changed from 2 to 1 for minimal padding
maxTransportLen += 2 // Add some padding for transport address maxTransportLen += 2
maxIngestedLen += 1 // Minimal padding for ingested column
return maxNodeNameLen, maxIndexNameLen, maxTransportLen return maxNodeNameLen, maxIndexNameLen, maxTransportLen, maxIngestedLen
} }
func getNodesPanelHeader(maxNodeNameLen, maxTransportLen int) string { func getNodesPanelHeader(maxNodeNameLen, maxTransportLen int) string {
return fmt.Sprintf("[::b]%-*s [#444444]│[#00ffff] %-13s [#444444]│[#00ffff] %*s [#444444]│[#00ffff] %-7s [#444444]│[#00ffff] %-9s [#444444]│[#00ffff] %-16s [#444444]│[#00ffff] %-16s [#444444]│[#00ffff] %-16s [#444444]│[#00ffff] %-6s [#444444][#00ffff] %-25s[white]\n", return fmt.Sprintf("[::b]%-*s [#444444]│[#00ffff] %-13s [#444444]│[#00ffff] %*s [#444444]│[#00ffff] %-7s [#444444]│[#00ffff] %-9s [#444444]│[#00ffff] %-16s [#444444]│[#00ffff] %-16s [#444444]│[#00ffff] %-16s [#444444]│[#00ffff] %-6s [#444444][#00ffff] %-25s[white]\n",
maxNodeNameLen, maxNodeNameLen,
"Node Name", "Node Name",
"Roles", "Roles",
@ -1250,14 +1228,15 @@ func getNodesPanelHeader(maxNodeNameLen, maxTransportLen int) string {
"OS") "OS")
} }
func getIndicesPanelHeader(maxIndexNameLen int) string { func getIndicesPanelHeader(maxIndexNameLen, maxIngestedLen int) string {
return fmt.Sprintf(" [::b] %-*s [#444444]│[#00ffff] %15s [#444444]│[#00ffff] %5s [#444444]│[#00ffff] %6s [#444444]│[#00ffff] %8s [#444444]│[#00ffff] %-12s [#444444]│[#00ffff] %-8s[white]\n", return fmt.Sprintf(" [::b] %-*s [#444444]│[#00ffff] %13s [#444444]│[#00ffff] %5s [#444444]│[#00ffff] %6s [#444444]│[#00ffff] %8s [#444444]│[#00ffff] %-*s [#444444][#00ffff] %-8s[white]\n",
maxIndexNameLen-1, maxIndexNameLen,
"Index Name", "Index Name",
"Documents", "Documents",
"Size", "Size",
"Shards", "Shards",
"Replicas", "Replicas",
maxIngestedLen,
"Ingested", "Ingested",
"Rate") "Rate")
} }
@ -1299,25 +1278,124 @@ func formatUptime(uptimeMillis int64) string {
var result string var result string
if days > 0 { if days > 0 {
result = fmt.Sprintf("%d[#ff66cc]d[white]%d[#ff66cc]h[white]", days, hours) result = fmt.Sprintf("%d[#ff99cc]d[white]%d[#ff99cc]h[white]", days, hours)
} else if hours > 0 { } else if hours > 0 {
result = fmt.Sprintf("%d[#ff66cc]h[white]%d[#ff66cc]m[white]", hours, minutes) result = fmt.Sprintf("%d[#ff99cc]h[white]%d[#ff99cc]m[white]", hours, minutes)
} else { } else {
result = fmt.Sprintf("%d[#ff66cc]m[white]", minutes) result = fmt.Sprintf("%d[#ff99cc]m[white]", minutes)
} }
// Calculate the actual visible length (excluding color codes) // Calculate the actual display length by removing all color codes in one pass
visibleLen := 0 displayLen := len(strings.NewReplacer(
if days > 0 { "[#ff99cc]", "",
visibleLen = len(fmt.Sprintf("%dd%dh", days, hours)) "[white]", "",
} else if hours > 0 { ).Replace(result))
visibleLen = len(fmt.Sprintf("%dh%dm", hours, minutes))
} else { // Add padding to make all uptime strings align (6 chars for display)
visibleLen = len(fmt.Sprintf("%dm", minutes)) padding := 6 - displayLen
if padding > 0 {
result = strings.TrimRight(result, " ") + strings.Repeat(" ", padding)
} }
// Use max of "Uptime" length (6) or longest possible uptime string return result
minWidth := 6 }
padding := strings.Repeat(" ", minWidth-visibleLen)
return result + padding func getTotalHTTPConnections(stats NodesStats) int64 {
var total int64
for _, node := range stats.Nodes {
total += node.HTTP.CurrentOpen
}
return total
}
func updateRolesPanel(rolesPanel *tview.TextView, nodesInfo NodesInfo) {
rolesPanel.Clear()
fmt.Fprintf(rolesPanel, "[::b][#00ffff][[#ff5555]3[#00ffff]] Legend[::-]\n\n")
// Add Node Roles title in cyan
fmt.Fprintf(rolesPanel, "[::b][#00ffff]Node Roles[::-]\n")
// Define role letters (same as in formatNodeRoles)
roleMap := map[string]string{
"master": "M",
"data": "D",
"data_content": "C",
"data_hot": "H",
"data_warm": "W",
"data_cold": "K",
"data_frozen": "F",
"ingest": "I",
"ml": "L",
"remote_cluster_client": "R",
"transform": "T",
"voting_only": "V",
"coordinating_only": "O",
}
// Create a map of active roles in the cluster
activeRoles := make(map[string]bool)
for _, node := range nodesInfo.Nodes {
for _, role := range node.Roles {
activeRoles[role] = true
}
}
// Sort roles alphabetically by their letters
var roles []string
for role := range legendLabels {
roles = append(roles, role)
}
sort.Slice(roles, func(i, j int) bool {
return roleMap[roles[i]] < roleMap[roles[j]]
})
// Display each role with its color and description
for _, role := range roles {
color := roleColors[role]
label := legendLabels[role]
letter := roleMap[role]
// If role is not active in cluster, use grey color for the label
labelColor := "[white]"
if !activeRoles[role] {
labelColor = "[#444444]"
}
fmt.Fprintf(rolesPanel, "[%s]%s[white] %s%s\n", color, letter, labelColor, label)
}
// Add version status information
fmt.Fprintf(rolesPanel, "\n[::b][#00ffff]Version Status[::-]\n")
fmt.Fprintf(rolesPanel, "[green]⚫[white] Up to date\n")
fmt.Fprintf(rolesPanel, "[yellow]⚫[white] Outdated\n")
// Add index health status information
fmt.Fprintf(rolesPanel, "\n[::b][#00ffff]Index Health[::-]\n")
fmt.Fprintf(rolesPanel, "[green]⚫[white] All shards allocated\n")
fmt.Fprintf(rolesPanel, "[#ffff00]⚫[white] Replica shards unallocated\n")
fmt.Fprintf(rolesPanel, "[#ff5555]⚫[white] Primary shards unallocated\n")
// Add index status indicators
fmt.Fprintf(rolesPanel, "\n[::b][#00ffff]Index Status[::-]\n")
fmt.Fprintf(rolesPanel, "[#5555ff]⚫[white] Active indexing\n")
fmt.Fprintf(rolesPanel, "[#444444]⚪[white] No indexing\n")
fmt.Fprintf(rolesPanel, "[#bd93f9]⚫[white] Data stream\n")
}
func formatResourceSize(bytes int64) string {
const unit = 1024
if bytes < unit {
return fmt.Sprintf("%4d B", bytes)
}
units := []string{"B", "K", "M", "G", "T", "P"}
exp := 0
val := float64(bytes)
for val >= unit && exp < len(units)-1 {
val /= unit
exp++
}
return fmt.Sprintf("%3d%s", int(val), units[exp])
} }