mirror of
https://codeberg.org/scip/esctl.git
synced 2026-08-24 05:04:18 +02:00
enhance cluster status (#11)
This commit is contained in:
@@ -31,7 +31,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
Version string = `v0.0.5`
|
Version string = `v0.0.6`
|
||||||
)
|
)
|
||||||
|
|
||||||
type Cluster struct {
|
type Cluster struct {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user