Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 273fed1776 | |||
| 4bbf0c6bf1 | |||
| 23a7c216ab | |||
| 66e5e4a1b3 | |||
| 0912d77018 | |||
| 1d08518847 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,6 +4,7 @@
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
elastop
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
35
README.md
35
README.md
@@ -32,12 +32,35 @@ go build
|
||||
```
|
||||
|
||||
### Command Line Flags
|
||||
| Flag | Description | Default |
|
||||
| ----------- | ---------------------- | ------------- |
|
||||
| `-host` | Elasticsearch host | `localhost` |
|
||||
| `-port` | Elasticsearch port | `9200` |
|
||||
| `-user` | Elasticsearch username | `elastic` |
|
||||
| `-password` | Elasticsearch password | `ES_PASSWORD` |
|
||||
| Flag | Description | Default |
|
||||
| ----------- | ------------------------------------- | ------------- |
|
||||
| `-host` | Elasticsearch host | `localhost` |
|
||||
| `-port` | Elasticsearch port | `9200` |
|
||||
| `-user` | Elasticsearch username | `elastic` |
|
||||
| `-password` | Elasticsearch password | `ES_PASSWORD` |
|
||||
| `-apikey` | Elasticsearch API key | `ES_API_KEY` |
|
||||
| `-cert` | Path to client certificate file | |
|
||||
| `-key` | Path to client private key file | |
|
||||
| `-ca` | Path to CA certificate file | |
|
||||
| `-insecure` | Skip TLS certificate verification | `false` |
|
||||
|
||||
Note: Only one authentication method (username/password, API key, or certificates) can be used at a time.
|
||||
|
||||
### Authentication Examples
|
||||
|
||||
```bash
|
||||
# Using username/password
|
||||
./elastop -host https://elasticsearch.example.com -user elastic -password secret
|
||||
|
||||
# Using API key
|
||||
./elastop -host https://elasticsearch.example.com -apikey your_api_key
|
||||
|
||||
# Using certificate authentication
|
||||
./elastop -host https://elasticsearch.example.com -cert /path/to/client.crt -key /path/to/client.key -ca /path/to/ca.crt
|
||||
|
||||
# Using certificate authentication with insecure SSL (not recommended for production)
|
||||
./elastop -host https://elasticsearch.example.com -cert /path/to/client.crt -key /path/to/client.key -insecure
|
||||
```
|
||||
|
||||
## Dashboard Layout
|
||||
|
||||
|
||||
692
elastop.go
692
elastop.go
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
@@ -38,6 +39,19 @@ type ClusterStats struct {
|
||||
Successful int `json:"successful"`
|
||||
Failed int `json:"failed"`
|
||||
} `json:"_nodes"`
|
||||
Process struct {
|
||||
CPU struct {
|
||||
Percent int `json:"percent"`
|
||||
} `json:"cpu"`
|
||||
OpenFileDescriptors struct {
|
||||
Min int `json:"min"`
|
||||
Max int `json:"max"`
|
||||
Avg int `json:"avg"`
|
||||
} `json:"open_file_descriptors"`
|
||||
} `json:"process"`
|
||||
Snapshots struct {
|
||||
Count int `json:"count"`
|
||||
} `json:"snapshots"`
|
||||
}
|
||||
|
||||
type NodesInfo struct {
|
||||
@@ -54,18 +68,8 @@ type NodesInfo struct {
|
||||
PrettyName string `json:"pretty_name"`
|
||||
} `json:"os"`
|
||||
Process struct {
|
||||
ID int `json:"id"`
|
||||
Mlockall bool `json:"mlockall"`
|
||||
ID int `json:"id"`
|
||||
} `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"`
|
||||
}
|
||||
|
||||
@@ -80,7 +84,6 @@ type IndexStats []struct {
|
||||
|
||||
type IndexActivity struct {
|
||||
LastDocsCount int
|
||||
IsActive bool
|
||||
InitialDocsCount int
|
||||
StartTime time.Time
|
||||
}
|
||||
@@ -153,6 +156,7 @@ type NodesStats struct {
|
||||
} `json:"old"`
|
||||
} `json:"collectors"`
|
||||
} `json:"gc"`
|
||||
UptimeInMillis int64 `json:"uptime_in_millis"`
|
||||
} `json:"jvm"`
|
||||
Transport struct {
|
||||
RxSizeInBytes int64 `json:"rx_size_in_bytes"`
|
||||
@@ -195,17 +199,6 @@ var (
|
||||
|
||||
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 (
|
||||
showNodes = true
|
||||
showRoles = true
|
||||
@@ -237,6 +230,11 @@ var (
|
||||
apiKey string
|
||||
)
|
||||
|
||||
type CatNodesStats struct {
|
||||
Load1m string `json:"load_1m"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func bytesToHuman(bytes int64) string {
|
||||
const unit = 1024
|
||||
if bytes < unit {
|
||||
@@ -278,24 +276,6 @@ func convertSizeFormat(sizeStr string) string {
|
||||
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 {
|
||||
switch {
|
||||
case percent < 30:
|
||||
@@ -364,9 +344,9 @@ var roleColors = map[string]string{
|
||||
"data_warm": "#bd93f9", // purple
|
||||
"data_cold": "#f1fa8c", // yellow
|
||||
"data_frozen": "#ff79c6", // pink
|
||||
"ingest": "#87cefa", // light sky blue (was gray)
|
||||
"ingest": "#87cefa", // light sky blue
|
||||
"ml": "#6272a4", // blue gray
|
||||
"remote_cluster_client": "#dda0dd", // plum (was burgundy)
|
||||
"remote_cluster_client": "#dda0dd", // plum
|
||||
"transform": "#689d6a", // forest green
|
||||
"voting_only": "#458588", // teal
|
||||
"coordinating_only": "#d65d0e", // burnt orange
|
||||
@@ -389,6 +369,7 @@ var legendLabels = map[string]string{
|
||||
}
|
||||
|
||||
func formatNodeRoles(roles []string) string {
|
||||
// Define all possible roles and their letters in the desired order
|
||||
roleMap := map[string]string{
|
||||
"master": "M",
|
||||
"data": "D",
|
||||
@@ -405,35 +386,38 @@ func formatNodeRoles(roles []string) string {
|
||||
"coordinating_only": "O",
|
||||
}
|
||||
|
||||
// Get the role letters and sort them
|
||||
var letters []string
|
||||
// Create a map of the node's roles for quick lookup
|
||||
nodeRoles := make(map[string]bool)
|
||||
for _, role := range roles {
|
||||
if letter, exists := roleMap[role]; exists {
|
||||
letters = append(letters, letter)
|
||||
}
|
||||
}
|
||||
sort.Strings(letters)
|
||||
|
||||
formattedRoles := " "
|
||||
runeRoles := []rune(formattedRoles)
|
||||
|
||||
for i, letter := range letters {
|
||||
if i < 13 {
|
||||
runeRoles[i] = []rune(letter)[0]
|
||||
}
|
||||
nodeRoles[role] = true
|
||||
}
|
||||
|
||||
var result string
|
||||
for _, r := range runeRoles {
|
||||
if r == ' ' {
|
||||
result += " "
|
||||
// Create ordered list of role keys based on their letters
|
||||
orderedRoles := []string{
|
||||
"data_content", // C
|
||||
"data", // D
|
||||
"data_frozen", // F
|
||||
"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 := ""
|
||||
for _, role := range orderedRoles {
|
||||
letter := roleMap[role]
|
||||
if nodeRoles[role] {
|
||||
// Node has this role - use the role's color
|
||||
result += fmt.Sprintf("[%s]%s[white]", roleColors[role], letter)
|
||||
} else {
|
||||
for role, shortRole := range roleMap {
|
||||
if string(r) == shortRole {
|
||||
result += fmt.Sprintf("[%s]%s[white]", roleColors[role], string(r))
|
||||
break
|
||||
}
|
||||
}
|
||||
// Node doesn't have this role - use dark grey
|
||||
result += fmt.Sprintf("[#444444]%s[white]", letter)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -464,9 +448,6 @@ type indexInfo struct {
|
||||
indexingRate float64
|
||||
}
|
||||
|
||||
// Add startTime at package level
|
||||
var startTime = time.Now()
|
||||
|
||||
func updateGridLayout(grid *tview.Grid, showRoles, showIndices, showMetrics bool) {
|
||||
// Start with clean grid
|
||||
grid.Clear()
|
||||
@@ -482,7 +463,18 @@ func updateGridLayout(grid *tview.Grid, showRoles, showIndices, showMetrics bool
|
||||
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 {
|
||||
grid.SetRows(3, 0, 0) // Header, nodes, bottom panels
|
||||
} else {
|
||||
@@ -493,16 +485,16 @@ func updateGridLayout(grid *tview.Grid, showRoles, showIndices, showMetrics bool
|
||||
switch {
|
||||
case visiblePanels == 3:
|
||||
if showRoles {
|
||||
grid.SetColumns(30, -2, -1) // Changed from 20 to 30 for roles panel width
|
||||
grid.SetColumns(30, -2, -1)
|
||||
}
|
||||
case visiblePanels == 2:
|
||||
if showRoles {
|
||||
grid.SetColumns(30, 0) // Changed from 20 to 30 for roles panel width
|
||||
grid.SetColumns(30, 0)
|
||||
} else {
|
||||
grid.SetColumns(-1, -1) // Equal split between two panels
|
||||
grid.SetColumns(-1, -1)
|
||||
}
|
||||
case visiblePanels == 1:
|
||||
grid.SetColumns(0) // Single column takes full width
|
||||
grid.SetColumns(0)
|
||||
}
|
||||
|
||||
// Always show header at top spanning all columns
|
||||
@@ -546,6 +538,13 @@ func main() {
|
||||
user := flag.String("user", os.Getenv("ES_USER"), "Elasticsearch username")
|
||||
password := flag.String("password", os.Getenv("ES_PASSWORD"), "Elasticsearch password")
|
||||
flag.StringVar(&apiKey, "apikey", os.Getenv("ES_API_KEY"), "Elasticsearch API key")
|
||||
|
||||
// Add new certificate-related flags
|
||||
certFile := flag.String("cert", "", "Path to client certificate file")
|
||||
keyFile := flag.String("key", "", "Path to client private key file")
|
||||
caFile := flag.String("ca", "", "Path to CA certificate file")
|
||||
skipVerify := flag.Bool("insecure", false, "Skip TLS certificate verification")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
// Validate and process the host URL
|
||||
@@ -554,25 +553,65 @@ func main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Validate authentication
|
||||
if apiKey != "" && (*user != "" || *password != "") {
|
||||
fmt.Fprintf(os.Stderr, "Error: Cannot use both API key and username/password authentication\n")
|
||||
// Validate authentication methods - only one should be used
|
||||
authMethods := 0
|
||||
if apiKey != "" {
|
||||
authMethods++
|
||||
}
|
||||
if *user != "" || *password != "" {
|
||||
authMethods++
|
||||
}
|
||||
if *certFile != "" || *keyFile != "" {
|
||||
authMethods++
|
||||
}
|
||||
if authMethods > 1 {
|
||||
fmt.Fprintf(os.Stderr, "Error: Cannot use multiple authentication methods simultaneously (API key, username/password, or certificates)\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if apiKey == "" && (*user == "" || *password == "") {
|
||||
fmt.Fprintf(os.Stderr, "Error: Must provide either API key or both username and password\n")
|
||||
// Validate certificate files if specified
|
||||
if (*certFile != "" && *keyFile == "") || (*certFile == "" && *keyFile != "") {
|
||||
fmt.Fprintf(os.Stderr, "Error: Both certificate and key files must be specified together\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Strip any trailing slash from the host
|
||||
*host = strings.TrimRight(*host, "/")
|
||||
|
||||
// Create TLS config
|
||||
tlsConfig := &tls.Config{
|
||||
InsecureSkipVerify: *skipVerify,
|
||||
}
|
||||
|
||||
// Load client certificates if specified
|
||||
if *certFile != "" && *keyFile != "" {
|
||||
cert, err := tls.LoadX509KeyPair(*certFile, *keyFile)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error loading client certificates: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
tlsConfig.Certificates = []tls.Certificate{cert}
|
||||
}
|
||||
|
||||
// Load CA certificate if specified
|
||||
if *caFile != "" {
|
||||
caCert, err := os.ReadFile(*caFile)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error reading CA certificate: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
caCertPool := x509.NewCertPool()
|
||||
if !caCertPool.AppendCertsFromPEM(caCert) {
|
||||
fmt.Fprintf(os.Stderr, "Error parsing CA certificate\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
tlsConfig.RootCAs = caCertPool
|
||||
}
|
||||
|
||||
// Create custom HTTP client with SSL configuration
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true, // Allow self-signed certificates
|
||||
},
|
||||
TLSClientConfig: tlsConfig,
|
||||
}
|
||||
client := &http.Client{
|
||||
Transport: tr,
|
||||
@@ -628,7 +667,7 @@ func main() {
|
||||
// Set authentication
|
||||
if apiKey != "" {
|
||||
req.Header.Set("Authorization", fmt.Sprintf("ApiKey %s", apiKey))
|
||||
} else {
|
||||
} else if *user != "" && *password != "" {
|
||||
req.SetBasicAuth(*user, *password)
|
||||
}
|
||||
|
||||
@@ -692,35 +731,35 @@ func main() {
|
||||
return
|
||||
}
|
||||
|
||||
// Calculate aggregate metrics
|
||||
// Query and indexing metrics
|
||||
var (
|
||||
totalQueries int64
|
||||
totalQueryTime float64
|
||||
totalIndexing int64
|
||||
totalIndexingTime float64
|
||||
totalCPUPercent int
|
||||
totalMemoryUsed int64
|
||||
totalMemoryTotal int64
|
||||
totalHeapUsed int64
|
||||
totalHeapMax int64
|
||||
totalGCCollections int64
|
||||
totalGCTime float64
|
||||
nodeCount int
|
||||
totalQueries int64
|
||||
totalQueryTime int64
|
||||
totalIndexing int64
|
||||
totalIndexTime int64
|
||||
totalSegments int64
|
||||
)
|
||||
|
||||
for _, node := range nodesStats.Nodes {
|
||||
totalQueries += node.Indices.Search.QueryTotal
|
||||
totalQueryTime += float64(node.Indices.Search.QueryTimeInMillis) / 1000
|
||||
totalQueryTime += node.Indices.Search.QueryTimeInMillis
|
||||
totalIndexing += node.Indices.Indexing.IndexTotal
|
||||
totalIndexingTime += float64(node.Indices.Indexing.IndexTimeInMillis) / 1000
|
||||
totalCPUPercent += node.OS.CPU.Percent
|
||||
totalMemoryUsed += node.OS.Memory.UsedInBytes
|
||||
totalMemoryTotal += node.OS.Memory.TotalInBytes
|
||||
totalHeapUsed += node.JVM.Memory.HeapUsedInBytes
|
||||
totalHeapMax += node.JVM.Memory.HeapMaxInBytes
|
||||
totalIndexTime += node.Indices.Indexing.IndexTimeInMillis
|
||||
totalSegments += node.Indices.Segments.Count
|
||||
}
|
||||
|
||||
queryRate := float64(totalQueries) / float64(totalQueryTime) * 1000 // queries per second
|
||||
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
|
||||
totalGCTime += float64(node.JVM.GC.Collectors.Young.CollectionTimeInMillis+node.JVM.GC.Collectors.Old.CollectionTimeInMillis) / 1000
|
||||
nodeCount++
|
||||
totalGCTime += node.JVM.GC.Collectors.Young.CollectionTimeInMillis + node.JVM.GC.Collectors.Old.CollectionTimeInMillis
|
||||
}
|
||||
|
||||
// Update header
|
||||
@@ -731,7 +770,7 @@ func main() {
|
||||
}[clusterStats.Status]
|
||||
|
||||
// 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
|
||||
header.Clear()
|
||||
@@ -812,25 +851,7 @@ func main() {
|
||||
nodeLoads[node.Name] = node.Load1m
|
||||
}
|
||||
|
||||
// In the update() function, add this request before processing nodes:
|
||||
var threadPoolStats []ThreadPoolStats
|
||||
if err := makeRequest("/_cat/thread_pool/generic?format=json&h=node_name,name,active,queue,rejected,completed", &threadPoolStats); err != nil {
|
||||
nodesPanel.SetText(fmt.Sprintf("[red]Error getting thread pool stats: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// Create a map for quick lookup of thread pool stats by node name
|
||||
threadPoolMap := make(map[string]ThreadPoolStats)
|
||||
for _, stat := range threadPoolStats {
|
||||
threadPoolMap[stat.NodeName] = stat
|
||||
}
|
||||
|
||||
active, _ := strconv.Atoi(threadPoolMap[nodeInfo.Name].Active)
|
||||
queue, _ := strconv.Atoi(threadPoolMap[nodeInfo.Name].Queue)
|
||||
rejected, _ := strconv.Atoi(threadPoolMap[nodeInfo.Name].Rejected)
|
||||
completed, _ := strconv.Atoi(threadPoolMap[nodeInfo.Name].Completed)
|
||||
|
||||
fmt.Fprintf(nodesPanel, "[#5555ff]%-*s [white] [#444444]│[white] %s [#444444]│[white] [white]%-*s[white] [#444444]│[white] [%s]%-7s[white] [#444444]│[white] [%s]%3d%% [#444444](%d)[white] [#444444]│[white] %4s [#444444]│[white] %4s / %4s [%s]%3d%%[white] [#444444]│[white] %4s / %4s [%s]%3d%%[white] [#444444]│[white] %4s / %4s [%s]%3d%%[white] [#444444]│[white] %6s [#444444]│[white] %5s [#444444]│[white] %8s [#444444]│[white] %9s [#444444]│[white] %s [#bd93f9]%s[white] [#444444](%s)[white]\n",
|
||||
fmt.Fprintf(nodesPanel, "[#5555ff]%-*s [white] [#444444]│[white] %s [#444444]│[white] [white]%*s[white] [#444444]│[white] [%s]%-7s[white] [#444444]│[white] [%s]%3d%% [#444444](%d)[white] [#444444]│[white] %4s / %4s [%s]%3d%%[white] [#444444]│[white] %4s / %4s [%s]%3d%%[white] [#444444]│[white] %4s / %4s [%s]%3d%%[white] [#444444]│[white] %-8s[white] [#444444]│[white] %s [#bd93f9]%s[white] [#444444](%s)[white]\n",
|
||||
maxNodeNameLen,
|
||||
nodeInfo.Name,
|
||||
formatNodeRoles(nodeInfo.Roles),
|
||||
@@ -841,7 +862,6 @@ func main() {
|
||||
getPercentageColor(float64(cpuPercent)),
|
||||
cpuPercent,
|
||||
nodeInfo.OS.AvailableProcessors,
|
||||
nodeLoads[nodeInfo.Name],
|
||||
formatResourceSize(nodeStats.OS.Memory.UsedInBytes),
|
||||
formatResourceSize(nodeStats.OS.Memory.TotalInBytes),
|
||||
getPercentageColor(memPercent),
|
||||
@@ -854,10 +874,7 @@ func main() {
|
||||
formatResourceSize(diskTotal),
|
||||
getPercentageColor(diskPercent),
|
||||
int(diskPercent),
|
||||
formatNumber(active),
|
||||
formatNumber(queue),
|
||||
formatNumber(rejected),
|
||||
formatNumber(completed),
|
||||
formatUptime(nodeStats.JVM.UptimeInMillis),
|
||||
nodeInfo.OS.PrettyName,
|
||||
nodeInfo.OS.Version,
|
||||
nodeInfo.OS.Arch)
|
||||
@@ -873,7 +890,7 @@ func main() {
|
||||
// Update indices panel with dynamic width
|
||||
indicesPanel.Clear()
|
||||
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
|
||||
var indices []indexInfo
|
||||
@@ -932,8 +949,13 @@ func main() {
|
||||
totalSize += node.FS.Total.TotalInBytes - node.FS.Total.AvailableInBytes
|
||||
}
|
||||
|
||||
// Sort indices by document count (descending)
|
||||
// Sort indices - active ones first, then alphabetically within each group
|
||||
sort.Slice(indices, func(i, j int) bool {
|
||||
// If one is active and the other isn't, active goes first
|
||||
if (indices[i].indexingRate > 0) != (indices[j].indexingRate > 0) {
|
||||
return indices[i].indexingRate > 0
|
||||
}
|
||||
// Within the same group (both active or both inactive), sort alphabetically
|
||||
return indices[i].index < indices[j].index
|
||||
})
|
||||
|
||||
@@ -947,17 +969,17 @@ func main() {
|
||||
// Add data stream indicator
|
||||
streamIndicator := " "
|
||||
if isDataStream(idx.index, dataStreamResp) {
|
||||
streamIndicator = "[#bd93f9]⚡[white]"
|
||||
streamIndicator = "[#bd93f9]⚫[white]"
|
||||
}
|
||||
|
||||
// Calculate document changes
|
||||
// Calculate document changes with dynamic padding
|
||||
activity := indexActivities[idx.index]
|
||||
ingestedStr := ""
|
||||
if activity != nil && activity.InitialDocsCount < idx.docs {
|
||||
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 {
|
||||
ingestedStr = fmt.Sprintf("%-12s", "")
|
||||
ingestedStr = fmt.Sprintf("%-*s", maxIngestedLen, "")
|
||||
}
|
||||
|
||||
// Format indexing rate
|
||||
@@ -975,7 +997,7 @@ func main() {
|
||||
// Convert the size format before display
|
||||
sizeStr := convertSizeFormat(idx.storeSize)
|
||||
|
||||
fmt.Fprintf(indicesPanel, "%s %s[%s]%-*s[white] [#444444]│[white] %15s [#444444]│[white] %12s [#444444]│[white] %8s [#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,
|
||||
streamIndicator,
|
||||
getHealthColor(idx.health),
|
||||
@@ -985,6 +1007,7 @@ func main() {
|
||||
sizeStr,
|
||||
idx.priShards,
|
||||
idx.replicas,
|
||||
maxIngestedLen,
|
||||
ingestedStr,
|
||||
rateStr)
|
||||
}
|
||||
@@ -1028,109 +1051,106 @@ func main() {
|
||||
metricsPanel.Clear()
|
||||
fmt.Fprintf(metricsPanel, "[::b][#00ffff][[#ff5555]5[#00ffff]] Cluster Metrics[::-]\n\n")
|
||||
|
||||
// Helper function to format metric lines with consistent alignment
|
||||
formatMetric := func(name string, value string) string {
|
||||
return fmt.Sprintf("[#00ffff]%-25s[white] %s\n", name+":", value)
|
||||
// Define metrics keys with proper grouping
|
||||
metricKeys := []string{
|
||||
// System metrics
|
||||
"CPU",
|
||||
"Memory",
|
||||
"Heap",
|
||||
"Disk",
|
||||
|
||||
// Network metrics
|
||||
"Network TX",
|
||||
"Network RX",
|
||||
"HTTP Connections",
|
||||
|
||||
// Performance metrics
|
||||
"Query Rate",
|
||||
"Index Rate",
|
||||
|
||||
// Miscellaneous
|
||||
"Snapshots",
|
||||
}
|
||||
|
||||
// Search metrics
|
||||
fmt.Fprint(metricsPanel, formatMetric("Search Queries", formatNumber(int(totalQueries))))
|
||||
fmt.Fprint(metricsPanel, formatMetric("Query Rate", fmt.Sprintf("%s/s", formatNumber(int(float64(totalQueries)/time.Since(startTime).Seconds())))))
|
||||
fmt.Fprint(metricsPanel, formatMetric("Total Query Time", fmt.Sprintf("%.1fs", totalQueryTime)))
|
||||
fmt.Fprint(metricsPanel, formatMetric("Avg Query Latency", fmt.Sprintf("%.2fms", totalQueryTime*1000/float64(totalQueries+1))))
|
||||
// Find the longest key for proper alignment
|
||||
maxKeyLength := 0
|
||||
for _, key := range metricKeys {
|
||||
if len(key) > maxKeyLength {
|
||||
maxKeyLength = len(key)
|
||||
}
|
||||
}
|
||||
|
||||
// Indexing metrics
|
||||
fmt.Fprint(metricsPanel, formatMetric("Index Operations", formatNumber(int(totalIndexing))))
|
||||
fmt.Fprint(metricsPanel, formatMetric("Indexing Rate", fmt.Sprintf("%s/s", formatNumber(int(float64(totalIndexing)/time.Since(startTime).Seconds())))))
|
||||
fmt.Fprint(metricsPanel, formatMetric("Total Index Time", fmt.Sprintf("%.1fs", totalIndexingTime)))
|
||||
fmt.Fprint(metricsPanel, formatMetric("Avg Index Latency", fmt.Sprintf("%.2fms", totalIndexingTime*1000/float64(totalIndexing+1))))
|
||||
// Add padding for better visual separation
|
||||
maxKeyLength += 2
|
||||
|
||||
// GC metrics
|
||||
fmt.Fprint(metricsPanel, formatMetric("GC Collections", formatNumber(int(totalGCCollections))))
|
||||
fmt.Fprint(metricsPanel, formatMetric("Total GC Time", fmt.Sprintf("%.1fs", totalGCTime)))
|
||||
fmt.Fprint(metricsPanel, formatMetric("Avg GC Time", fmt.Sprintf("%.2fms", totalGCTime*1000/float64(totalGCCollections+1))))
|
||||
// Helper function for metric lines with proper alignment
|
||||
formatMetric := func(name string, value string) string {
|
||||
return fmt.Sprintf("[#00ffff]%-*s[white] %s\n", maxKeyLength, name+":", value)
|
||||
}
|
||||
|
||||
// CPU metrics
|
||||
totalProcessors := 0
|
||||
for _, node := range nodesInfo.Nodes {
|
||||
totalProcessors += node.OS.AvailableProcessors
|
||||
}
|
||||
cpuPercent := float64(clusterStats.Process.CPU.Percent)
|
||||
fmt.Fprint(metricsPanel, formatMetric("CPU", fmt.Sprintf("%7.1f%% [#444444](%d processors)[white]", cpuPercent, totalProcessors)))
|
||||
|
||||
// Disk metrics
|
||||
diskUsed := getTotalSize(nodesStats)
|
||||
diskTotal := getTotalDiskSpace(nodesStats)
|
||||
diskPercent := float64(diskUsed) / float64(diskTotal) * 100
|
||||
fmt.Fprint(metricsPanel, formatMetric("Disk", fmt.Sprintf("%8s / %8s [%s]%5.1f%%[white]",
|
||||
bytesToHuman(diskUsed),
|
||||
bytesToHuman(diskTotal),
|
||||
getPercentageColor(diskPercent),
|
||||
diskPercent)))
|
||||
|
||||
// Calculate heap and memory totals
|
||||
var (
|
||||
totalHeapUsed int64
|
||||
totalHeapMax int64
|
||||
totalMemoryUsed int64
|
||||
totalMemoryTotal int64
|
||||
)
|
||||
|
||||
for _, node := range nodesStats.Nodes {
|
||||
totalHeapUsed += node.JVM.Memory.HeapUsedInBytes
|
||||
totalHeapMax += node.JVM.Memory.HeapMaxInBytes
|
||||
totalMemoryUsed += node.OS.Memory.UsedInBytes
|
||||
totalMemoryTotal += node.OS.Memory.TotalInBytes
|
||||
}
|
||||
|
||||
// Heap metrics
|
||||
heapPercent := float64(totalHeapUsed) / float64(totalHeapMax) * 100
|
||||
fmt.Fprint(metricsPanel, formatMetric("Heap", fmt.Sprintf("%8s / %8s [%s]%5.1f%%[white]",
|
||||
bytesToHuman(totalHeapUsed),
|
||||
bytesToHuman(totalHeapMax),
|
||||
getPercentageColor(heapPercent),
|
||||
heapPercent)))
|
||||
|
||||
// Memory metrics
|
||||
totalMemoryPercent := float64(totalMemoryUsed) / float64(totalMemoryTotal) * 100
|
||||
totalHeapPercent := float64(totalHeapUsed) / float64(totalHeapMax) * 100
|
||||
fmt.Fprint(metricsPanel, formatMetric("Memory Usage", fmt.Sprintf("%s / %s (%.1f%%)", bytesToHuman(totalMemoryUsed), bytesToHuman(totalMemoryTotal), totalMemoryPercent)))
|
||||
fmt.Fprint(metricsPanel, formatMetric("Heap Usage", fmt.Sprintf("%s / %s (%.1f%%)", bytesToHuman(totalHeapUsed), bytesToHuman(totalHeapMax), totalHeapPercent)))
|
||||
|
||||
// Segment metrics
|
||||
fmt.Fprint(metricsPanel, formatMetric("Total Segments", formatNumber(int(getTotalSegments(nodesStats)))))
|
||||
fmt.Fprint(metricsPanel, formatMetric("Open File Descriptors", formatNumber(int(getTotalOpenFiles(nodesStats)))))
|
||||
memoryPercent := float64(totalMemoryUsed) / float64(totalMemoryTotal) * 100
|
||||
fmt.Fprint(metricsPanel, formatMetric("Memory", fmt.Sprintf("%8s / %8s [%s]%5.1f%%[white]",
|
||||
bytesToHuman(totalMemoryUsed),
|
||||
bytesToHuman(totalMemoryTotal),
|
||||
getPercentageColor(memoryPercent),
|
||||
memoryPercent)))
|
||||
|
||||
// Network metrics
|
||||
fmt.Fprint(metricsPanel, formatMetric("Network TX", bytesToHuman(getTotalNetworkTX(nodesStats))))
|
||||
fmt.Fprint(metricsPanel, formatMetric("Network RX", bytesToHuman(getTotalNetworkRX(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)))))
|
||||
|
||||
// Disk I/O metrics
|
||||
totalDiskReads := int64(0)
|
||||
totalDiskWrites := int64(0)
|
||||
for _, node := range nodesStats.Nodes {
|
||||
totalDiskReads += node.FS.DiskReads
|
||||
totalDiskWrites += node.FS.DiskWrites
|
||||
}
|
||||
fmt.Fprint(metricsPanel, formatMetric("Disk Reads", formatNumber(int(totalDiskReads))))
|
||||
fmt.Fprint(metricsPanel, formatMetric("Disk Writes", formatNumber(int(totalDiskWrites))))
|
||||
// HTTP Connections and Shard metrics - right aligned to match Network RX 'G'
|
||||
fmt.Fprint(metricsPanel, formatMetric("HTTP Connections", fmt.Sprintf("%8s", formatNumber(int(getTotalHTTPConnections(nodesStats))))))
|
||||
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)))))
|
||||
|
||||
// HTTP connections
|
||||
totalHTTPConnections := int64(0)
|
||||
for _, node := range nodesStats.Nodes {
|
||||
totalHTTPConnections += node.HTTP.CurrentOpen
|
||||
}
|
||||
fmt.Fprint(metricsPanel, formatMetric("HTTP Connections", formatNumber(int(totalHTTPConnections))))
|
||||
// Snapshots
|
||||
fmt.Fprint(metricsPanel, formatMetric("Snapshots", fmt.Sprintf("%8s", formatNumber(clusterStats.Snapshots.Count))))
|
||||
|
||||
// Average CPU usage across nodes
|
||||
avgCPUPercent := float64(totalCPUPercent) / float64(nodeCount)
|
||||
fmt.Fprint(metricsPanel, formatMetric("Average CPU Usage", fmt.Sprintf("%.1f%%", avgCPUPercent)))
|
||||
|
||||
// Pending tasks
|
||||
fmt.Fprint(metricsPanel, formatMetric("Pending Tasks", formatNumber(clusterHealth.NumberOfPendingTasks)))
|
||||
if clusterHealth.TaskMaxWaitingTime != "" && clusterHealth.TaskMaxWaitingTime != "0s" {
|
||||
fmt.Fprint(metricsPanel, formatMetric("Max Task Wait Time", clusterHealth.TaskMaxWaitingTime))
|
||||
}
|
||||
|
||||
// Update roles panel
|
||||
rolesPanel.Clear()
|
||||
fmt.Fprintf(rolesPanel, "[::b][#00ffff][[#ff5555]3[#00ffff]] Node Roles[::-]\n\n")
|
||||
|
||||
// Create a map of used roles
|
||||
usedRoles := make(map[string]bool)
|
||||
for _, nodeInfo := range nodesInfo.Nodes {
|
||||
for _, role := range nodeInfo.Roles {
|
||||
usedRoles[role] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Display roles in the roles panel
|
||||
roleLegend := [][2]string{
|
||||
{"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]])
|
||||
}
|
||||
if showRoles {
|
||||
updateRolesPanel(rolesPanel, nodesInfo)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1178,22 +1198,6 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
func getTotalSegments(stats NodesStats) int64 {
|
||||
var total int64
|
||||
for _, node := range stats.Nodes {
|
||||
total += node.Indices.Segments.Count
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
func getTotalOpenFiles(stats NodesStats) int64 {
|
||||
var total int64
|
||||
for _, node := range stats.Nodes {
|
||||
total += node.Process.OpenFileDescriptors
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
func getTotalNetworkTX(stats NodesStats) int64 {
|
||||
var total int64
|
||||
for _, node := range stats.Nodes {
|
||||
@@ -1210,10 +1214,11 @@ func getTotalNetworkRX(stats NodesStats) int64 {
|
||||
return total
|
||||
}
|
||||
|
||||
func getMaxLengths(nodesInfo NodesInfo, indicesStats IndexStats) (int, int, int) {
|
||||
func getMaxLengths(nodesInfo NodesInfo, indicesStats IndexStats) (int, int, int, int) {
|
||||
maxNodeNameLen := 0
|
||||
maxIndexNameLen := 0
|
||||
maxTransportLen := 0
|
||||
maxIngestedLen := 8 // Start with "Ingested" header length
|
||||
|
||||
// Get max node name and transport address length
|
||||
for _, nodeInfo := range nodesInfo.Nodes {
|
||||
@@ -1225,26 +1230,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 {
|
||||
// Only consider indices that should be visible based on showHiddenIndices
|
||||
if (showHiddenIndices || !strings.HasPrefix(index.Index, ".")) && index.DocsCount != "0" {
|
||||
if len(index.Index) > maxIndexNameLen {
|
||||
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
|
||||
maxNodeNameLen += 2
|
||||
maxIndexNameLen += 1 // Single space before separator
|
||||
maxTransportLen += 2 // Add some padding for transport address
|
||||
maxIndexNameLen += 1 // Changed from 2 to 1 for minimal padding
|
||||
maxTransportLen += 2
|
||||
maxIngestedLen += 1 // Minimal padding for ingested column
|
||||
|
||||
return maxNodeNameLen, maxIndexNameLen, maxTransportLen
|
||||
return maxNodeNameLen, maxIndexNameLen, maxTransportLen, maxIngestedLen
|
||||
}
|
||||
|
||||
func getNodesPanelHeader(maxNodeNameLen, maxTransportLen int) string {
|
||||
return fmt.Sprintf("[::b]%-*s [#444444]│[#00ffff] %-13s [#444444]│[#00ffff] %-*s [#444444]│[#00ffff] %-7s [#444444]│[#00ffff] %4s [#444444]│[#00ffff] %4s [#444444]│[#00ffff] %-16s [#444444]│[#00ffff] %-16s [#444444]│[#00ffff] %-16s [#444444]│[#00ffff] %6s [#444444]│[#00ffff] %5s [#444444]│[#00ffff] %8s [#444444]│[#00ffff] %9s [#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,
|
||||
"Node Name",
|
||||
"Roles",
|
||||
@@ -1252,25 +1269,22 @@ func getNodesPanelHeader(maxNodeNameLen, maxTransportLen int) string {
|
||||
"Transport Address",
|
||||
"Version",
|
||||
"CPU",
|
||||
"Load",
|
||||
"Memory",
|
||||
"Heap",
|
||||
"Disk",
|
||||
"Active",
|
||||
"Queue",
|
||||
"Rejected",
|
||||
"Completed",
|
||||
"Uptime",
|
||||
"OS")
|
||||
}
|
||||
|
||||
func getIndicesPanelHeader(maxIndexNameLen int) string {
|
||||
return fmt.Sprintf(" [::b] %-*s [#444444]│[#00ffff] %15s [#444444]│[#00ffff] %12s [#444444]│[#00ffff] %8s [#444444]│[#00ffff] %8s [#444444]│[#00ffff] %-12s [#444444]│[#00ffff] %-8s[white]\n",
|
||||
func getIndicesPanelHeader(maxIndexNameLen, maxIngestedLen int) string {
|
||||
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,
|
||||
"Index Name",
|
||||
"Documents",
|
||||
"Size",
|
||||
"Shards",
|
||||
"Replicas",
|
||||
maxIngestedLen,
|
||||
"Ingested",
|
||||
"Rate")
|
||||
}
|
||||
@@ -1284,12 +1298,152 @@ func isDataStream(name string, dataStreams DataStreamResponse) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// Add this with the other type definitions near the top of the file
|
||||
type ThreadPoolStats struct {
|
||||
NodeName string `json:"node_name"`
|
||||
Name string `json:"name"`
|
||||
Active string `json:"active"`
|
||||
Queue string `json:"queue"`
|
||||
Rejected string `json:"rejected"`
|
||||
Completed string `json:"completed"`
|
||||
func getTotalSize(stats NodesStats) int64 {
|
||||
var total int64
|
||||
for _, node := range stats.Nodes {
|
||||
if len(node.FS.Data) > 0 {
|
||||
total += node.FS.Data[0].TotalInBytes - node.FS.Data[0].AvailableInBytes
|
||||
}
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
func getTotalDiskSpace(stats NodesStats) int64 {
|
||||
var total int64
|
||||
for _, node := range stats.Nodes {
|
||||
if len(node.FS.Data) > 0 {
|
||||
total += node.FS.Data[0].TotalInBytes
|
||||
}
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
func formatUptime(uptimeMillis int64) string {
|
||||
uptime := time.Duration(uptimeMillis) * time.Millisecond
|
||||
days := int(uptime.Hours() / 24)
|
||||
hours := int(uptime.Hours()) % 24
|
||||
minutes := int(uptime.Minutes()) % 60
|
||||
|
||||
var result string
|
||||
if days > 0 {
|
||||
result = fmt.Sprintf("%d[#ff99cc]d[white]%d[#ff99cc]h[white]", days, hours)
|
||||
} else if hours > 0 {
|
||||
result = fmt.Sprintf("%d[#ff99cc]h[white]%d[#ff99cc]m[white]", hours, minutes)
|
||||
} else {
|
||||
result = fmt.Sprintf("%d[#ff99cc]m[white]", minutes)
|
||||
}
|
||||
|
||||
// Calculate the actual display length by removing all color codes in one pass
|
||||
displayLen := len(strings.NewReplacer(
|
||||
"[#ff99cc]", "",
|
||||
"[white]", "",
|
||||
).Replace(result))
|
||||
|
||||
// Add padding to make all uptime strings align (6 chars for display)
|
||||
padding := 6 - displayLen
|
||||
if padding > 0 {
|
||||
result = strings.TrimRight(result, " ") + strings.Repeat(" ", padding)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
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])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user