enhance cluster status (#11)

This commit is contained in:
T. von Dein
2026-05-07 12:54:27 +02:00
parent 0122009774
commit 5e82a3706b
3 changed files with 113 additions and 19 deletions

View File

@@ -31,7 +31,7 @@ import (
) )
const ( const (
Version string = `v0.0.5` Version string = `v0.0.6`
) )
type Cluster struct { type Cluster struct {

View File

@@ -22,8 +22,12 @@ import (
"errors" "errors"
"fmt" "fmt"
"log/slog" "log/slog"
"sync"
"codeberg.org/scip/esctl/pkg/cfg" "codeberg.org/scip/esctl/pkg/cfg"
"github.com/elastic/go-elasticsearch/v9/typedapi/ccr/stats"
"github.com/elastic/go-elasticsearch/v9/typedapi/cluster/health"
"github.com/elastic/go-elasticsearch/v9/typedapi/core/info"
"github.com/elastic/go-elasticsearch/v9/typedapi/types" "github.com/elastic/go-elasticsearch/v9/typedapi/types"
"github.com/urfave/cli/v3" "github.com/urfave/cli/v3"
) )
@@ -32,8 +36,22 @@ const (
DefaultExclude = `(part|monitoring|.internal|metrics-endpoint)` DefaultExclude = `(part|monitoring|.internal|metrics-endpoint)`
) )
const (
ResponseHealth = iota
ResponseInfo
ResponseCcr
)
type ClusterIndices map[string]map[string]*types.IndicesRecord type ClusterIndices map[string]map[string]*types.IndicesRecord
type apiResponse struct {
error error
info *info.Response
health *health.Response
ccr *stats.Response
which int
}
func ClusterCompare(conf *cfg.Config, leader, follower string) error { func ClusterCompare(conf *cfg.Config, leader, follower string) error {
if !checkClusterFollower(conf, leader) { if !checkClusterFollower(conf, leader) {
return errors.New("leader/follower attribution is invalid, reverse cluster attribution and retry") return errors.New("leader/follower attribution is invalid, reverse cluster attribution and retry")
@@ -102,6 +120,8 @@ func ClusterList(conf *cfg.Config) error {
return nil return nil
} }
// 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 ClusterStatus(conf *cfg.Config) error { func ClusterStatus(conf *cfg.Config) error {
clusters := []string{} clusters := []string{}
@@ -119,34 +139,60 @@ func ClusterStatus(conf *cfg.Config) error {
es = conf.Clusters[cluster].ES es = conf.Clusters[cluster].ES
} }
res, err := es.Cluster.Health(). responses := make(chan apiResponse, 3)
Header("content-type", "application/json"). wg := &sync.WaitGroup{}
Header("accept", "application/json").
Do(context.Background()) wg.Add(3)
if err != nil { go getClusterData(es, wg, responses, "health")
return fmt.Errorf("failed to get cluster health: %s", err) go getClusterData(es, wg, responses, "info")
go getClusterData(es, wg, responses, "ccrstats")
wg.Wait()
var clusterhealth *health.Response
var info *info.Response
var ccrstats *stats.Response
for i := 0; i < 3; i++ {
r := <-responses
if r.error != nil {
return r.error
} }
slog.Debug("ES result", "cluster health", res) switch r.which {
case ResponseHealth:
clusterhealth = r.health
case ResponseCcr:
ccrstats = r.ccr
case ResponseInfo:
info = r.info
}
}
info, err := es.Info(). slog.Debug("ES result", "cluster health", clusterhealth)
Header("content-type", "application/json").
Header("accept", "application/json"). ccrfollowing := ""
Do(context.Background()) if len(ccrstats.AutoFollowStats.AutoFollowedClusters) > 0 {
if err != nil { // is following another cluster
return fmt.Errorf("failed to get cluster info: %s", err) ccrfollowing = fmt.Sprintf("%s (%d/%d)",
ccrstats.AutoFollowStats.AutoFollowedClusters[0].ClusterName,
ccrstats.AutoFollowStats.NumberOfSuccessfulFollowIndices,
ccrstats.AutoFollowStats.NumberOfFailedFollowIndices,
)
} }
table := NewTable(2, 5) table := NewTable(2, 5)
table.Addheaders(cluster, "status") table.Addheaders(cluster, "status")
table.entries = [][]string{ table.entries = [][]string{
{"Cluster Name", Colorize(*&res.Status.Name, res.ClusterName)}, {"Cluster Name", Colorize(*&clusterhealth.Status.Name, clusterhealth.ClusterName)},
{"ES Version", fmt.Sprintf("%s", info.Version.Int)}, {"ES Version", fmt.Sprintf("%s", info.Version.Int)},
{"Active Shards", fmt.Sprintf("%d", res.ActiveShards)}, {"Active Shards", fmt.Sprintf("%d", clusterhealth.ActiveShards)},
{"Active Primary Shards", fmt.Sprintf("%d", res.ActivePrimaryShards)}, {"Active Primary Shards", fmt.Sprintf("%d", clusterhealth.ActivePrimaryShards)},
{"Indicies", fmt.Sprintf("%d", len(res.Indices))}, {"Indicies", fmt.Sprintf("%d", len(clusterhealth.Indices))},
{"Nodes", fmt.Sprintf("%d", res.NumberOfNodes)}, {"Nodes", fmt.Sprintf("%d", clusterhealth.NumberOfNodes)},
{"AutoFollow (success/failed indices)", ccrfollowing},
} }
table.PrintMarkdown() table.PrintMarkdown()

View File

@@ -18,11 +18,14 @@ package es
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"regexp" "regexp"
"strings" "strings"
"sync"
"codeberg.org/scip/esctl/pkg/cfg" "codeberg.org/scip/esctl/pkg/cfg"
"github.com/elastic/go-elasticsearch/v9"
"github.com/elastic/go-elasticsearch/v9/typedapi/cluster/health" "github.com/elastic/go-elasticsearch/v9/typedapi/cluster/health"
"github.com/elastic/go-elasticsearch/v9/typedapi/types" "github.com/elastic/go-elasticsearch/v9/typedapi/types"
) )
@@ -377,3 +380,48 @@ func splitArg(arg string) (string, string) {
return parts[0], parts[1] return parts[0], parts[1]
} }
} }
func getClusterData(es *elasticsearch.TypedClient, wg *sync.WaitGroup, reschan chan apiResponse, which string) {
defer wg.Done()
ar := apiResponse{}
arerr := errors.New("")
switch which {
case "health":
res, err := es.Cluster.Health().
Header("content-type", "application/json").
Header("accept", "application/json").
Do(context.Background())
ar.health = res
ar.which = ResponseHealth
arerr = err
case "info":
res, err := es.Info().
Header("content-type", "application/json").
Header("accept", "application/json").
Do(context.Background())
ar.info = res
ar.which = ResponseInfo
arerr = err
case "ccrstats":
res, err := es.Ccr.Stats().
Header("content-type", "application/json").
Header("accept", "application/json").
Do(context.Background())
ar.ccr = res
ar.which = ResponseCcr
arerr = err
}
if arerr != nil {
ar.error = fmt.Errorf("failed to get cluster health: %s", arerr)
}
reschan <- ar
}