Compare commits

..

16 Commits

Author SHA1 Message Date
34669ac4de add support for human readable /_cat endpoints: 'api repl -H' 2026-07-10 11:11:49 +02:00
45ba38bda9 make full linter the default 2026-07-10 11:11:39 +02:00
T. von Dein
53abe666e8 fix 'node show' and 'shard explain' crashes (#86) 2026-07-09 13:41:07 +02:00
1e9e419e9b fix crash in ByteSize conversion 2026-07-08 15:07:01 +02:00
T. von Dein
c128e32ca1 add more important node stats (#82) 2026-07-08 15:03:31 +02:00
2969522913 rename var 2026-07-08 15:00:21 +02:00
51480e6b35 add 'index du <index>' 2026-07-08 14:58:07 +02:00
99bf703997 add table.AddRowLate() to add rows after sorting 2026-07-08 14:57:46 +02:00
T. von Dein
76ad6c7afa remove workaround to load config in root.Before() (#83)
see: https://github.com/urfave/cli/issues/2348
2026-07-08 14:13:47 +02:00
T. von Dein
8614e09004 show green and failed indices in status, rename red to failed in index ls (#81) 2026-07-08 10:23:01 +02:00
03cfeb4336 fix go tool 2026-07-08 09:34:58 +02:00
T. von Dein
3d3dfb7a42 fix #79: remove compatibility header (#80) 2026-07-08 09:33:22 +02:00
1d45a3cd4c set go version for releaser 2026-07-08 00:00:37 +02:00
0104e1bf58 fix go releaser go version 2026-07-07 23:53:47 +02:00
2dc716d1c9 fix version newline 2026-07-07 23:49:57 +02:00
T. von Dein
0d63e7c4d2 satisfy full linter (#77) 2026-07-07 23:46:43 +02:00
27 changed files with 321 additions and 100 deletions

View File

@@ -67,10 +67,10 @@ test: clean buildlocal
testlint: test lint
lint:
lint-basic:
golangci-lint run --enable-only errcheck,govet,ineffassign,staticcheck,unused
lint-full:
lint:
golangci-lint run --show-stats=false
testfuzzy: clean

View File

@@ -72,7 +72,7 @@ func ApiShow(conf *cfg.Config) *cli.Command {
},
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
complete(cmd, Capi)
complete(conf, cmd, Capi)
},
}
}
@@ -91,6 +91,12 @@ func ApiRepl(conf *cfg.Config) *cli.Command {
Aliases: []string{"p"},
Sources: cli.EnvVars("PAGER", "ES_JSON_PAGER"),
},
&cli.BoolFlag{
Name: "human-readable-cat",
Usage: "enable human readable /_cat output",
Destination: &conf.HumanCat,
Aliases: []string{"H"},
},
},
Action: func(ctx context.Context, cmd *cli.Command) error {

View File

@@ -59,7 +59,7 @@ func CcrStatus(conf *cfg.Config) *cli.Command {
},
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
complete(cmd, Ccluster)
complete(conf, cmd, Ccluster)
},
Action: func(ctx context.Context, cmd *cli.Command) error {
@@ -115,7 +115,7 @@ func CcrRemoteInfo(conf *cfg.Config) *cli.Command {
UsageText: "info [options] [<index>]",
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
complete(cmd, Cindex)
complete(conf, cmd, Cindex)
},
Action: func(ctx context.Context, cmd *cli.Command) error {

View File

@@ -60,7 +60,7 @@ func CcrFollowerRenew(conf *cfg.Config) *cli.Command {
},
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
complete(cmd, Cindex)
complete(conf, cmd, Cindex)
},
Action: func(ctx context.Context, cmd *cli.Command) error {
@@ -82,7 +82,7 @@ func CcrFollowerResume(conf *cfg.Config) *cli.Command {
UsageText: "resume [options] <index>",
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
complete(cmd, Cindex)
complete(conf, cmd, Cindex)
},
Action: func(ctx context.Context, cmd *cli.Command) error {
@@ -104,7 +104,7 @@ func CcrFollowerPause(conf *cfg.Config) *cli.Command {
UsageText: "pause [options] <index>",
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
complete(cmd, Cindex)
complete(conf, cmd, Cindex)
},
Action: func(ctx context.Context, cmd *cli.Command) error {
@@ -126,7 +126,7 @@ func CcrFollowerUnfollow(conf *cfg.Config) *cli.Command {
UsageText: "unfollow [options] <index>",
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
complete(cmd, Cindex)
complete(conf, cmd, Cindex)
},
Action: func(ctx context.Context, cmd *cli.Command) error {
@@ -158,7 +158,7 @@ func CcrFollowerAdd(conf *cfg.Config) *cli.Command {
},
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
complete(cmd, Cindex)
complete(conf, cmd, Cindex)
},
Action: func(ctx context.Context, cmd *cli.Command) error {
@@ -181,7 +181,7 @@ func CcrFollowerDelete(conf *cfg.Config) *cli.Command {
UsageText: "delete <index>",
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
complete(cmd, Cindex)
complete(conf, cmd, Cindex)
},
Action: func(ctx context.Context, cmd *cli.Command) error {
@@ -205,7 +205,7 @@ func CcrFollowerShow(conf *cfg.Config) *cli.Command {
UsageText: "show <index>",
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
complete(cmd, Cindex)
complete(conf, cmd, Cindex)
},
Action: func(ctx context.Context, cmd *cli.Command) error {

View File

@@ -83,7 +83,7 @@ func ClusterSwitch(conf *cfg.Config) *cli.Command {
Aliases: []string{"ctx"},
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
complete(cmd, Ccluster)
complete(conf, cmd, Ccluster)
},
Action: func(ctx context.Context, cmd *cli.Command) error {

View File

@@ -50,7 +50,7 @@ func ClusterRerouteMove(conf *cfg.Config) *cli.Command {
UsageText: "move [options] <index>",
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
complete(cmd, Cindex)
complete(conf, cmd, Cindex)
},
Flags: []cli.Flag{
@@ -95,7 +95,7 @@ func ClusterRerouteAllocateReplica(conf *cfg.Config) *cli.Command {
UsageText: "allocate-replica [options] <index>",
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
complete(cmd, Cindex)
complete(conf, cmd, Cindex)
},
Flags: []cli.Flag{
@@ -133,7 +133,7 @@ func ClusterRerouteCancel(conf *cfg.Config) *cli.Command {
UsageText: "cancel [options] <index>",
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
complete(cmd, Cindex)
complete(conf, cmd, Cindex)
},
Flags: []cli.Flag{
@@ -185,7 +185,7 @@ func ClusterRerouteAllocatePrimary(conf *cfg.Config, stale bool) *cli.Command {
UsageText: name + " [options] <index>",
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
complete(cmd, Cindex)
complete(conf, cmd, Cindex)
},
Flags: []cli.Flag{

View File

@@ -105,7 +105,7 @@ func ClusterSettingsSet(conf *cfg.Config) *cli.Command {
},
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
complete(cmd, Cclustersettings)
complete(conf, cmd, Cclustersettings)
},
Action: func(ctx context.Context, cmd *cli.Command) error {

View File

@@ -36,18 +36,11 @@ const (
Cclustersettings
)
func complete(cmd *cli.Command, what int) {
func complete(conf *cfg.Config, cmd *cli.Command, what int) {
if cmd.NArg() > 0 {
return
}
// FIXME: config should load from root.Before(), see https://github.com/urfave/cli/issues/2348
// workaround: load it directly here
conf := cfg.NewConfig()
if err := conf.Init(); err != nil {
return
}
var (
list []string
err error

View File

@@ -91,7 +91,7 @@ func DatastreamShow(conf *cfg.Config) *cli.Command {
},
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
complete(cmd, Cdatastream)
complete(conf, cmd, Cdatastream)
},
}
}
@@ -113,7 +113,7 @@ func DatastreamCreate(conf *cfg.Config) *cli.Command {
},
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
complete(cmd, Cdatastream)
complete(conf, cmd, Cdatastream)
},
}
}
@@ -135,7 +135,7 @@ func DatastreamDelete(conf *cfg.Config) *cli.Command {
},
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
complete(cmd, Cdatastream)
complete(conf, cmd, Cdatastream)
},
}
}
@@ -148,7 +148,7 @@ func DatastreamRollover(conf *cfg.Config) *cli.Command {
UsageText: "rollover <data stream>",
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
complete(cmd, Cindex)
complete(conf, cmd, Cindex)
},
Flags: []cli.Flag{
@@ -205,7 +205,7 @@ func DatastreamIlm(conf *cfg.Config) *cli.Command {
UsageText: "ds ilm <name>",
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
complete(cmd, Cdatastream)
complete(conf, cmd, Cdatastream)
},
Action: func(ctx context.Context, cmd *cli.Command) error {

View File

@@ -123,7 +123,7 @@ func IlmShow(conf *cfg.Config) *cli.Command {
},
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
complete(cmd, Cilm)
complete(conf, cmd, Cilm)
},
}
}

View File

@@ -41,6 +41,7 @@ func Index(conf *cfg.Config) *cli.Command {
IndexClose(conf),
IndexFields(conf),
IndexIlm(conf),
IndexDu(conf),
// sub commands
IndexAlias(conf),
@@ -75,10 +76,10 @@ func IndexList(conf *cfg.Config) *cli.Command {
Aliases: []string{"H"},
},
&cli.BoolFlag{
Name: "reds",
Name: "failed",
Usage: "include only red failed indicies",
Destination: &conf.Failed,
Aliases: []string{"r"},
Aliases: []string{"f"},
},
&cli.StringSliceFlag{
Name: "filter",
@@ -110,7 +111,7 @@ func IndexShow(conf *cfg.Config) *cli.Command {
},
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
complete(cmd, Cindex)
complete(conf, cmd, Cindex)
},
}
}
@@ -180,7 +181,7 @@ func IndexDelete(conf *cfg.Config) *cli.Command {
UsageText: "delete <index>",
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
complete(cmd, Cindex)
complete(conf, cmd, Cindex)
},
Action: func(ctx context.Context, cmd *cli.Command) error {
@@ -201,7 +202,7 @@ func IndexClose(conf *cfg.Config) *cli.Command {
UsageText: "close <index>",
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
complete(cmd, Cindex)
complete(conf, cmd, Cindex)
},
Action: func(ctx context.Context, cmd *cli.Command) error {
@@ -243,7 +244,7 @@ func IndexFields(conf *cfg.Config) *cli.Command {
},
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
complete(cmd, Cindex)
complete(conf, cmd, Cindex)
},
Action: func(ctx context.Context, cmd *cli.Command) error {
@@ -264,7 +265,7 @@ func IndexIlm(conf *cfg.Config) *cli.Command {
UsageText: "index ilm <index>",
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
complete(cmd, Cindex)
complete(conf, cmd, Cindex)
},
Action: func(ctx context.Context, cmd *cli.Command) error {
@@ -277,3 +278,24 @@ func IndexIlm(conf *cfg.Config) *cli.Command {
},
}
}
func IndexDu(conf *cfg.Config) *cli.Command {
return &cli.Command{
Name: "du",
Usage: "show index disk usage",
UsageText: "index du <index>",
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
complete(conf, cmd, Cindex)
},
Action: func(ctx context.Context, cmd *cli.Command) error {
index := cmd.Args().Get(0)
if index == "" {
return errors.New("no index specified")
}
return es.IndexDiskusage(conf, index)
},
}
}

View File

@@ -52,7 +52,7 @@ func IndexAliasCreate(conf *cfg.Config) *cli.Command {
UsageText: "create <index> <alias>",
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
complete(cmd, Cindex)
complete(conf, cmd, Cindex)
},
Action: func(ctx context.Context, cmd *cli.Command) error {
@@ -76,7 +76,7 @@ func IndexAliasDelete(conf *cfg.Config) *cli.Command {
UsageText: "delete <index> <alias>",
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
complete(cmd, Cindex)
complete(conf, cmd, Cindex)
},
Action: func(ctx context.Context, cmd *cli.Command) error {
@@ -100,7 +100,7 @@ func IndexAliasRollover(conf *cfg.Config) *cli.Command {
UsageText: "rollover <alias>",
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
complete(cmd, Cindex)
complete(conf, cmd, Cindex)
},
Flags: []cli.Flag{

View File

@@ -71,7 +71,7 @@ func IndexTemplateShow(conf *cfg.Config) *cli.Command {
},
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
complete(cmd, Cindex)
complete(conf, cmd, Cindex)
},
}
}
@@ -215,7 +215,7 @@ func IndexTemplateDelete(conf *cfg.Config) *cli.Command {
},
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
complete(cmd, Cindex)
complete(conf, cmd, Cindex)
},
}
}

View File

@@ -60,7 +60,7 @@ func NodeShow(conf *cfg.Config) *cli.Command {
UsageText: "show [options] <node>",
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
complete(cmd, Cnode)
complete(conf, cmd, Cnode)
},
Action: func(ctx context.Context, cmd *cli.Command) error {
@@ -90,7 +90,7 @@ func NodeClients(conf *cfg.Config) *cli.Command {
},
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
complete(cmd, Cnode)
complete(conf, cmd, Cnode)
},
Action: func(ctx context.Context, cmd *cli.Command) error {

View File

@@ -68,7 +68,7 @@ func RoleShow(conf *cfg.Config) *cli.Command {
UsageText: "show [options] <role>",
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
complete(cmd, Crole)
complete(conf, cmd, Crole)
},
Action: func(ctx context.Context, cmd *cli.Command) error {

2
go.mod
View File

@@ -14,7 +14,7 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
module codeberg.org/scip/esctl
go 1.26.4
go 1.26
require (
github.com/MichaelMure/go-term-markdown v0.1.4

View File

@@ -189,6 +189,7 @@ func (cluster *Cluster) getDefaultOptions() []elasticsearch.Option {
cluster.getTransport(),
elastictransport.WithHeader(headers),
),
elasticsearch.WithCompatibilityMode(),
}
}
@@ -203,7 +204,9 @@ func (cluster *Cluster) getTransport() elastictransport.Option {
)
}
return elastictransport.WithTransport(transport)
return elastictransport.WithTransport(
&CompatibilityTransport{Transport: transport},
)
}
func (conf *Config) SetupES() error {

View File

@@ -82,8 +82,9 @@ type Config struct {
SortBy string // sort: -k
Ascending bool // sort: -a
All bool // doc + node ls: -a
Exclude string // cluster compare: -e (regexp)
All, Verbose bool // cluster status: -a -v
Verbose bool // cluster status: -v
Persistent, Transient, Default bool // cluster settings set: -p -t -D
Force bool // ccr follower renew: -f
@@ -101,6 +102,7 @@ type Config struct {
DryRun bool // rollover: -n
Tag string // api ls: -t
HumanCat bool // api repl: -H
Ilm Ilm // ilm create

View File

@@ -53,6 +53,8 @@ func (t *DebugTransport) RoundTrip(req *http.Request) (*http.Response, error) {
contentline = buf.String()
}
req = fixRequestHeaders(req)
slog.Info("req", "host", req.URL.Host, "uri", req.URL.Path,
"body", content, "bodyline", contentline,
"headers", req.Header,
@@ -60,3 +62,32 @@ func (t *DebugTransport) RoundTrip(req *http.Request) (*http.Response, error) {
return t.Transport.RoundTrip(req)
}
type CompatibilityTransport struct {
Transport http.RoundTripper
}
func (t *CompatibilityTransport) RoundTrip(req *http.Request) (*http.Response, error) {
req = fixRequestHeaders(req)
return t.Transport.RoundTrip(req)
}
// Fixes https://codeberg.org/scip/esctl/issues/79:
// the API client has this Accept header hardcoded everywhere:
// req.Header.Set("Accept", "application/vnd.elasticsearch+json;compatible-with=9")
// While this works pretty well with ES9, it doesn't with ES8. So, we replace
// this header with a new one without the compatibility part.
func fixRequestHeaders(req *http.Request) *http.Request {
if req.Header.Get("Accept") != "" {
req.Header.Del("Accept")
req.Header.Add("Accept", "application/json")
}
if req.Header.Get("Content-Type") != "" {
req.Header.Del("Content-Type")
req.Header.Add("Content-Type", "application/json")
}
return req
}

View File

@@ -45,22 +45,25 @@ import (
)
const (
intro = `Input format: verb path [data]"
Example:
post /yourindex/_ccr/pause_follow
put /yourindex/_settings {"number_of_replicas": 1}
You can also put multiline JSON after the path like:
put /yourindex/_settings
{
"number_of_replicas": 1
}
If you do NOT supply a JSON in the first line, you need to hit ENTER
twice to complete.`
intro = `# Input format: verb path [data]"
#
# Example:
#
# post /yourindex/_ccr/pause_follow
# put /yourindex/_settings {"number_of_replicas": 1}
#
# You can also put multiline JSON after the path like:
#
# put /yourindex/_settings
# {
# "number_of_replicas": 1
# }
#
# If you do NOT supply a JSON in the first line, you need to hit ENTER
# twice to complete.
#
# Supply the flag --human-readable-cat, -H to view /_cat API calls in
# human readable form.`
)
// holds an API operation via go-openapi/spec
@@ -143,8 +146,12 @@ func ApiRepl(conf *cfg.Config) error {
fmt.Printf("failed to call API: %s\n", esErrorString(err))
}
if conf.HumanCat && strings.HasPrefix(parts[1], "/_cat") {
fmt.Println(string(raw))
} else {
pageJsonOutput(conf, raw)
}
}
//nolint:nilerr
return nil
@@ -216,8 +223,10 @@ func CallAPI(conf *cfg.Config, verb, path, data string) ([]byte, error) {
return nil, err
}
if !conf.HumanCat && strings.HasPrefix(path, "/_cat") {
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Accept", "application/json")
}
// make sure we have got all we need
if err := conf.DefaultCluster.CheckAuth(); err != nil {

View File

@@ -174,11 +174,16 @@ func ClusterStatus(conf *cfg.Config) error {
}
// look for red indices, if any
redindices := 0
failedIndices := 0
greenIndices := 0
for _, index := range *res.indices {
if *index.Health == "red" {
redindices++
switch {
case *index.Health != "green":
failedIndices++
case !strings.HasPrefix(*index.Index, "."):
// don't count internal indices
greenIndices++
}
}
@@ -205,7 +210,8 @@ func ClusterStatus(conf *cfg.Config) error {
{"Unassigned Primary Shards", res.health.UnassignedPrimaryShards},
{"Pending Tasks", res.health.NumberOfPendingTasks},
{"Nodes", res.health.NumberOfNodes},
{"Red Indices", redindices},
{"Green Indices", greenIndices},
{"Failed Indices", failedIndices},
{"Long Running Tasks", longtasks},
}
@@ -233,6 +239,10 @@ func ClusterStatus(conf *cfg.Config) error {
for resource, items := range diag.AffectedResources {
table.Entries = append(table.Entries, []any{" -> affected " + resource, strings.Join(items, ",")})
}
if diag.Action != "" {
table.AddRow(" -> suggested action to fix", diag.Action)
}
}
}
}

View File

@@ -18,6 +18,8 @@ package es
import (
"context"
"encoding/json"
"errors"
"fmt"
"log/slog"
"regexp"
@@ -28,6 +30,7 @@ import (
"codeberg.org/scip/esctl/pkg/cfg"
"codeberg.org/scip/esctl/pkg/printer"
"github.com/charmbracelet/lipgloss"
"github.com/elastic/go-elasticsearch/v9/typedapi/cat/indices"
"github.com/elastic/go-elasticsearch/v9/typedapi/esdsl"
"github.com/elastic/go-elasticsearch/v9/typedapi/types/enums/healthstatus"
@@ -306,3 +309,79 @@ func IndexFields(conf *cfg.Config, index string) error {
return nil
}
type Diskusage struct {
Total int64 `json:"total_in_bytes"`
Points int64 `json:"points_in_bytes"`
Norms int64 `json:"norms_in_bytes"`
TermVectors int64 `json:"term_vectors_in_bytes"`
KnnVectors int64 `json:"knn_vectors_in_bytes"`
BloomFilter int64 `json:"bloom_filter_in_bytes"`
}
type IndexDiskUsage struct {
AllFields Diskusage `json:"all_fields"`
Fields map[string]Diskusage `json:"fields"`
}
type ResIndexDiskUsage map[string]IndexDiskUsage
func IndexDiskusage(conf *cfg.Config, index string) error {
var bold = lipgloss.NewStyle().Bold(true)
res, err := conf.DefaultCluster.ES().Indices.DiskUsage(index).
RunExpensiveTasks(true).
Do(context.Background())
if err != nil {
return fmt.Errorf("failed to retrieve index disk usage: %w", esErrorString(err))
}
duRes := ResIndexDiskUsage{}
if err := json.Unmarshal(res, &duRes); err != nil {
return fmt.Errorf("failed to unmarshal disk usage response: %w", err)
}
diskusage, exists := duRes[index]
if !exists {
return errors.New("no disk usage reported for index")
}
table := printer.NewTableEmpty(conf).
WithHeaders("field", "bloom filter", "norms", "points", "term vectors", "knn vectors", "total")
for name, field := range diskusage.Fields {
if strings.HasPrefix(name, "_") || strings.HasSuffix(name, ".keyword") {
continue
}
table.AddRow(
name,
printer.Bytes(field.BloomFilter),
printer.Bytes(field.Norms),
printer.Bytes(field.Points),
printer.Bytes(field.TermVectors),
printer.Bytes(field.KnnVectors),
printer.Bytes(field.Total),
)
}
all := diskusage.AllFields
table.Sort()
table.AddRowLate(
bold.Render("Summary"),
printer.Bytes(all.BloomFilter),
printer.Bytes(all.Norms),
printer.Bytes(all.Points),
printer.Bytes(all.TermVectors),
printer.Bytes(all.KnnVectors),
printer.Bytes(all.Total),
)
if err := table.Print(); err != nil {
return err
}
return nil
}

View File

@@ -26,7 +26,6 @@ import (
"codeberg.org/scip/esctl/pkg/cfg"
"codeberg.org/scip/esctl/pkg/printer"
"github.com/dustin/go-humanize"
)
func NodeList(conf *cfg.Config) error {
@@ -114,18 +113,27 @@ func NodeShow(conf *cfg.Config, nodename string) error {
}
k8snode := info.Attributes["k8s_node_name"]
rank := "none"
adsel, exists := stat.AdaptiveSelection[id]
if exists {
rank = *adsel.Rank
}
table.Entries = [][]any{
{"Id", id},
{"Name", nodename},
{"Kubernetes node", k8snode},
{"Ip address", info.Ip},
{"Node rank", *stat.AdaptiveSelection[id].Rank},
{"Node rank", rank},
{"JVM", info.Jvm.VmName + " " + info.Jvm.Version},
{"JVM Started", time.UnixMilli(info.Jvm.StartTimeInMillis)},
{"OS", info.Os.PrettyName + " " + info.Os.Version},
{"Node roles", roles},
{"Node version", info.Version},
// FIXME: not implemented upstream
// see: https://github.com/elastic/go-elasticsearch/issues/1526
// {"Allocated shards", stat.Allocations.XXX},
{"HTTP clients", *stat.Http.CurrentOpen},
{"CPUs", *info.Os.AllocatedProcessors},
{"Load 15m/5m/1m", fmt.Sprintf("%.2f/%.2f/%.2f",
@@ -134,18 +142,41 @@ func NodeShow(conf *cfg.Config, nodename string) error {
stat.Os.Cpu.LoadAverage["1m"],
)},
{"Open FD's", *stat.Process.OpenFileDescriptors},
{"Response time avg", fmt.Sprintf("%dns", *stat.AdaptiveSelection[id].AvgResponseTimeNs)},
{"HTTP sesssions current/total", fmt.Sprintf("%d/%d",
*stat.Http.CurrentOpen,
*stat.Http.TotalOpened,
)},
{"Traffic rx/tx",
printer.ByteString(*stat.Transport.RxSizeInBytes) + " / " + printer.ByteString(*stat.Transport.TxSizeInBytes)},
{"Response time avg", time.Duration(*stat.AdaptiveSelection[id].AvgResponseTimeNs)},
{"Memory usage (used/avail)",
humanize.Bytes(uint64(*stat.Os.Mem.UsedInBytes)) + " / " + humanize.Bytes(uint64(*stat.Os.Mem.TotalInBytes))},
printer.ByteString(*stat.Os.Mem.UsedInBytes) + " / " + printer.ByteString(*stat.Os.Mem.TotalInBytes)},
{"Search queries current/total", fmt.Sprintf("%d/%d",
stat.Indices.Search.QueryCurrent,
stat.Indices.Search.QueryTotal,
)},
{"Search efficiency", stat.Indices.Search.QueryTimeInMillis / stat.Indices.Search.QueryTotal},
{"Docs count", stat.Indices.Docs.Count},
{"Merges current/total", fmt.Sprintf("%d/%d",
stat.Indices.Merges.Current,
stat.Indices.Merges.Total,
)},
{"Merge docs count current/total", fmt.Sprintf("%d/%d",
stat.Indices.Merges.CurrentDocs,
stat.Indices.Merges.TotalDocs,
)},
{"Merge size current/total", fmt.Sprintf("%s/%s",
printer.ByteString(stat.Indices.Merges.CurrentSizeInBytes),
printer.ByteString(stat.Indices.Merges.TotalSizeInBytes),
)},
{"CircuitBreaker trip count", *stat.Breakers["fielddata"].Tripped},
}
if len(stat.Fs.Data) > 0 {
fs := stat.Fs.Data[0]
table.Entries = append(table.Entries, [][]any{
{"Storage usage (used/avail)",
humanize.Bytes(uint64(*fs.AvailableInBytes)) + " / " + humanize.Bytes(uint64(*fs.TotalInBytes))},
{"Storage mount", *fs.Mount},
}...)
table.AddRow("Storage usage (used/avail)",
printer.ByteString(*fs.AvailableInBytes)+" / "+printer.ByteString(*fs.TotalInBytes))
table.AddRow("Storage mount", *fs.Mount)
}
if err := table.Print(); err != nil {

View File

@@ -163,28 +163,35 @@ func ShardAllocation(conf *cfg.Config, index string) error {
slog.Debug("ES result", "explain", res)
currentNode := res.CurrentNode
table := printer.NewTable(conf, 2, 10)
table.Addheaders("shard allocation setting", "value")
roles := make([]string, len(currentNode.Roles))
for idx, role := range currentNode.Roles {
roles[idx] = role.Name
}
table.Entries = [][]any{
{"Index", index},
{"Current state", res.CurrentState},
{"Can rebalance cluster", res.CanRebalanceCluster.Name},
{"Can rebalance to another node", res.CanRebalanceToOtherNode.Name},
{"Can remain on current node", res.CanRemainOnCurrentNode.Name},
}
if res.CurrentNode != nil {
currentNode := res.CurrentNode
roles := make([]string, len(currentNode.Roles))
for idx, role := range currentNode.Roles {
roles[idx] = role.Name
}
table.Entries = append(table.Entries, [][]any{
{"Current node", currentNode.Name},
{"Current k8s node", currentNode.Attributes["k8s_node_name"]},
{"Current node address", currentNode.TransportAddress},
{"Current node id", currentNode.Id},
{"Current node weight", currentNode.WeightRanking},
{"Current node roles", roles},
{"Can rebalance cluster", res.CanRebalanceCluster.Name},
{"Can rebalance to another node", res.CanRebalanceToOtherNode.Name},
{"Can remain on current node", res.CanRemainOnCurrentNode.Name},
}...)
} else {
table.AddRow("Current node", "not currently assigned to any node")
}
if res.CurrentState == "unassigned" {

View File

@@ -22,10 +22,16 @@ type ByteSize struct {
size uint64
}
func Bytes(size int64) ByteSize {
return ByteSize{size: uint64(size)}
}
func (b *ByteSize) String() string {
return humanize.Bytes(b.size)
}
func Bytes(size int64) *ByteSize {
return &ByteSize{size: uint64(size)}
}
func ByteString(size int64) string {
b := ByteSize{size: uint64(size)}
return b.String()
}

View File

@@ -38,12 +38,18 @@ func any2string(in any) string {
return strconv.Itoa(val)
case float64:
return fmt.Sprintf("%.2f", val)
case float32:
return fmt.Sprintf("%.2f", val)
case []string:
return strings.Join(val, ",")
case ByteSize:
return val.String()
case *ByteSize:
return val.String()
case time.Time:
return val.Format("2006-01-02 15:04:05")
case time.Duration:
return val.String()
case []byte:
return string(val)
case nil:

View File

@@ -206,6 +206,22 @@ func (table *Table) AddRow(fields ...any) {
table.Entries = append(table.Entries, fields)
}
func (table *Table) AddRowLate(fields ...any) {
table.AddRow(fields)
if !table.processed {
return
}
row := make([]string, len(fields))
for idx, field := range fields {
row[idx] = any2string(field)
}
table.rows = append(table.rows, row)
}
// needed for json and yaml output
func (table *Table) toMap() []map[string]any {
raw := make([]map[string]any, len(table.Entries))