Files
esctl/pkg/es/cluster.go

290 lines
7.5 KiB
Go
Raw Permalink Normal View History

/*
Copyright © 2026 Thomas von Dein
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package es
import (
"errors"
"fmt"
"log/slog"
"strings"
2026-05-07 12:54:27 +02:00
"sync"
"codeberg.org/scip/esctl/pkg/cfg"
"codeberg.org/scip/esctl/pkg/printer"
"github.com/dustin/go-humanize"
clusterstats "github.com/elastic/go-elasticsearch/v9/typedapi/cluster/stats"
"github.com/elastic/go-elasticsearch/v9/typedapi/types"
)
type ClusterIndices map[string]map[string]*types.IndicesRecord
type clusterReachable struct {
reachable bool
err error
}
func ClusterList(conf *cfg.Config) error {
2026-07-07 23:45:14 +02:00
var (
mu sync.Mutex
wg sync.WaitGroup
)
reachable := make(map[string]clusterReachable, len(conf.Clusters))
// check endpoints in parallel to speed things up
for name, cluster := range conf.Clusters {
wg.Go(func() {
online, err := cluster.IsReachable()
mu.Lock()
reachable[name] = clusterReachable{reachable: online, err: err}
mu.Unlock()
})
}
wg.Wait()
table := printer.NewTable(conf, 5, len(conf.Clusters))
table.Addheaders("cluster", "uri", "reachable", "current", "error")
idx := 0
2026-05-11 14:03:30 +02:00
for name, cluster := range conf.Clusters {
reachableStr := "no"
current := "no"
errmsg := ""
2026-05-11 14:03:30 +02:00
if reachable[name].reachable {
reachableStr = printer.Colorize(conf, "green", "reachable")
}
if cluster.Default {
current = printer.Colorize(conf, "green", "yes")
if !reachable[name].reachable {
reachableStr = printer.Colorize(conf, "red", "no")
errmsg = reachable[name].err.Error()
}
}
2026-07-07 07:29:03 +02:00
table.Entries[idx] = []any{name, cluster.Uri, reachableStr, current, errmsg}
idx++
}
table.Sort()
return table.Print()
}
2026-05-07 12:54:27 +02:00
// We're using goroutines here to parallelize API requests, since we
// have to do 3 of'em for each cluster. This speeds things up.
func getClusterStatus(conf *cfg.Config) (*apiResponse, error) {
gocount := 6
if conf.Verbose {
gocount++
}
es := conf.DefaultCluster.ES()
responses := make(chan apiResponse, gocount)
wg := &sync.WaitGroup{}
2026-05-07 12:54:27 +02:00
wg.Add(gocount)
go getApiData(conf, es, wg, responses, "health")
go getApiData(conf, es, wg, responses, "healthreport")
go getApiData(conf, es, wg, responses, "info")
go getApiData(conf, es, wg, responses, "ccr")
go getApiData(conf, es, wg, responses, "indices")
go getApiData(conf, es, wg, responses, "tasks")
2026-05-07 12:54:27 +02:00
if conf.Verbose {
go getApiData(conf, es, wg, responses, "stats")
}
wg.Wait()
2026-05-07 12:54:27 +02:00
all := apiResponse{}
2026-05-07 12:54:27 +02:00
var err error
2026-07-07 23:45:14 +02:00
for range gocount {
res := <-responses
2026-05-07 12:54:27 +02:00
2026-07-07 23:45:14 +02:00
err = errors.Join(err, res.error)
2026-05-07 12:54:27 +02:00
2026-07-07 23:45:14 +02:00
switch res.which {
case ResponseHealth:
2026-07-07 23:45:14 +02:00
all.health = res.health
case ResponseCcr:
2026-07-07 23:45:14 +02:00
all.ccr = res.ccr
case ResponseInfo:
2026-07-07 23:45:14 +02:00
all.info = res.info
case ResponseStats:
2026-07-07 23:45:14 +02:00
all.stats = res.stats
case ResponseIndices:
2026-07-07 23:45:14 +02:00
all.indices = res.indices
case ResponseTasks:
2026-07-07 23:45:14 +02:00
all.tasks = res.tasks
case ResponseHealthReport:
2026-07-07 23:45:14 +02:00
all.healthreport = res.healthreport
}
}
return &all, err
}
func ClusterStatus(conf *cfg.Config) error {
res, err := getClusterStatus(conf)
if err != nil && !strings.Contains(err.Error(), "current license is non-compliant") {
return err
}
slog.Debug("ES result", "cluster health", res.health)
2026-07-07 23:45:14 +02:00
var (
isleader bool
ccrfollowing string
)
2026-06-22 14:11:32 +02:00
if res.ccr != nil {
isleader = len(res.ccr.AutoFollowStats.AutoFollowedClusters) == 0
if len(res.ccr.AutoFollowStats.AutoFollowedClusters) > 0 {
// is following another cluster
ccrfollowing = fmt.Sprintf("%s (%d/%d)",
res.ccr.AutoFollowStats.AutoFollowedClusters[0].ClusterName,
res.ccr.AutoFollowStats.NumberOfSuccessfulFollowIndices,
res.ccr.AutoFollowStats.NumberOfFailedFollowIndices,
)
}
}
2026-05-06 13:10:02 +02:00
// look for red indices, if any
redindices := 0
2026-07-07 23:45:14 +02:00
for _, index := range *res.indices {
if *index.Health == "red" {
redindices++
2026-06-22 14:11:32 +02:00
}
}
2026-06-22 14:11:32 +02:00
// look for long running tasks
longtasks := 0
2026-07-07 23:45:14 +02:00
for _, task := range *res.tasks {
if strings.Contains(*task.RunningTime, "d") {
longtasks++
2026-06-22 14:11:32 +02:00
}
}
2026-06-22 14:11:32 +02:00
table := printer.NewTable(conf, 2, 7)
table.Addheaders(conf.DefaultCluster.Name, "status")
2026-07-07 07:29:03 +02:00
table.Entries = [][]any{
{"Cluster Name", res.health.ClusterName},
{"ES Status", printer.Colorize(conf, res.health.Status.Name, res.health.Status.Name)},
{"ES Version", res.info.Version.Int},
2026-07-07 07:29:03 +02:00
{"Is Leader", isleader},
{"Active Shards", res.health.ActiveShards},
{"Active Primary Shards", res.health.ActivePrimaryShards},
{"Unassigned Shards", res.health.UnassignedShards},
{"Unassigned Primary Shards", res.health.UnassignedPrimaryShards},
{"Pending Tasks", res.health.NumberOfPendingTasks},
{"Nodes", res.health.NumberOfNodes},
{"Red Indices", redindices},
{"Long Running Tasks", longtasks},
}
2026-06-22 14:11:32 +02:00
if !isleader && res.ccr != nil {
2026-07-07 07:29:03 +02:00
table.Entries = append(table.Entries, [][]any{
{"AutoFollow (success/failed indices)", ccrfollowing},
2026-07-07 07:29:03 +02:00
{"Followed Indices", len(res.ccr.FollowStats.Indices)},
}...)
}
if conf.Verbose {
2026-07-07 23:45:14 +02:00
table = gatherClusterStats(res.stats, table)
}
if res.health.Status.Name != "green" {
for name, indicator := range res.healthreport.Indicators {
if indicator.Status != "green" {
2026-07-07 07:29:03 +02:00
table.Entries = append(table.Entries, []any{
printer.Colorize(conf, indicator.Status, "Bad health "+name), indicator.Symptom,
})
for _, diag := range indicator.Diagnosis {
2026-07-07 07:29:03 +02:00
table.Entries = append(table.Entries, []any{" -> cause", diag.Cause})
for resource, items := range diag.AffectedResources {
2026-07-07 07:29:03 +02:00
table.Entries = append(table.Entries, []any{" -> affected " + resource, strings.Join(items, ",")})
}
}
}
}
}
if err := table.Print(); err != nil {
return err
}
return nil
}
2026-07-07 23:45:14 +02:00
func gatherClusterStats(clusterstats *clusterstats.Response, table *printer.Table) *printer.Table {
var (
querycount int64
vmversion string
)
for _, count := range clusterstats.Indices.Search.Queries {
querycount += count
}
if len(clusterstats.Nodes.Jvm.Versions) > 0 {
vmversion = strings.Join([]string{
clusterstats.Nodes.Jvm.Versions[0].VmName,
clusterstats.Nodes.Jvm.Versions[0].VmVersion}, " ")
}
2026-07-07 07:29:03 +02:00
table.Entries = append(table.Entries, [][]any{
{"Indicies", clusterstats.Indices.Count},
{"Docs", clusterstats.Indices.Docs.Count},
{"Total Size", printer.Bytes(clusterstats.Indices.Docs.TotalSizeInBytes)},
{"Total Queries", "%d", querycount},
{"Shards Primaries", clusterstats.Indices.Shards.Primaries},
{"Shards Total", clusterstats.Indices.Shards.Total},
{"Storage", fmt.Sprintf(
"%s/%s",
humanize.Bytes(uint64(clusterstats.Indices.Store.SizeInBytes)),
humanize.Bytes(uint64(*clusterstats.Indices.Store.TotalDataSetSizeInBytes)),
)},
2026-06-22 14:11:32 +02:00
{"JVM Heap Memory", fmt.Sprintf(
"%s/%s",
humanize.Bytes(uint64(clusterstats.Nodes.Jvm.Mem.HeapUsedInBytes)),
humanize.Bytes(uint64(clusterstats.Nodes.Jvm.Mem.HeapMaxInBytes)),
)},
2026-07-07 07:29:03 +02:00
{"JVM Threads", clusterstats.Nodes.Jvm.Threads},
{"JVM Version", vmversion},
2026-07-07 07:29:03 +02:00
{"CPUs", clusterstats.Nodes.Os.AllocatedProcessors},
{"CPU Usage", fmt.Sprintf("%d%%", clusterstats.Nodes.Process.Cpu.Percent)},
2026-07-07 07:29:03 +02:00
{"Open FDs", clusterstats.Nodes.Process.OpenFileDescriptors.Avg},
}...)
return table
}