Compare commits

...

4 Commits

Author SHA1 Message Date
9e27fd4348 satisfy linter 2026-07-10 13:32:09 +02:00
6134fcfd22 add node usage 2026-07-10 13:29:46 +02:00
5644046c78 api repl: print output directly if not json 2026-07-10 11:28:25 +02:00
T. von Dein
9a42b86d62 add support for human readable /_cat endpoints: 'api repl -H' (#87) 2026-07-10 11:21:45 +02:00
7 changed files with 133 additions and 28 deletions

View File

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

View File

@@ -538,6 +538,7 @@ index - manage indicies
close - close an index close - close an index
fields - show info about field capabilities fields - show info about field capabilities
ilm - show ilm status ilm - show ilm status
du - show index disk usage
alias - manage index aliases alias - manage index aliases
create - create an index alias create - create an index alias
list - list index aliases list - list index aliases
@@ -555,6 +556,7 @@ node - manage nodes
list - list nodes list - list nodes
show - show details about a node show - show details about a node
clients - show node http clients clients - show node http clients
usage - show node usage stats
role - manage roles role - manage roles
list - list roles list - list roles
show - show details about a role show - show details about a role

View File

@@ -91,6 +91,12 @@ func ApiRepl(conf *cfg.Config) *cli.Command {
Aliases: []string{"p"}, Aliases: []string{"p"},
Sources: cli.EnvVars("PAGER", "ES_JSON_PAGER"), 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 { Action: func(ctx context.Context, cmd *cli.Command) error {

View File

@@ -36,6 +36,7 @@ func Node(conf *cfg.Config) *cli.Command {
NodeList(conf), NodeList(conf),
NodeShow(conf), NodeShow(conf),
NodeClients(conf), NodeClients(conf),
NodeUsage(conf),
}, },
} }
} }
@@ -103,3 +104,19 @@ func NodeClients(conf *cfg.Config) *cli.Command {
}, },
} }
} }
func NodeUsage(conf *cfg.Config) *cli.Command {
return &cli.Command{
Name: "usage",
Usage: "show node usage stats",
UsageText: "usage [options] [<node>]",
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
complete(conf, cmd, Cnode)
},
Action: func(ctx context.Context, cmd *cli.Command) error {
return es.NodeUsage(conf, cmd.Args().Get(0))
},
}
}

View File

@@ -102,6 +102,7 @@ type Config struct {
DryRun bool // rollover: -n DryRun bool // rollover: -n
Tag string // api ls: -t Tag string // api ls: -t
HumanCat bool // api repl: -H
Ilm Ilm // ilm create Ilm Ilm // ilm create

View File

@@ -45,22 +45,25 @@ import (
) )
const ( const (
intro = `Input format: verb path [data]" intro = `# Input format: verb path [data]"
#
Example: # Example:
#
post /yourindex/_ccr/pause_follow # post /yourindex/_ccr/pause_follow
put /yourindex/_settings {"number_of_replicas": 1} # put /yourindex/_settings {"number_of_replicas": 1}
#
You can also put multiline JSON after the path like: # You can also put multiline JSON after the path like:
#
put /yourindex/_settings # put /yourindex/_settings
{ # {
"number_of_replicas": 1 # "number_of_replicas": 1
} # }
#
If you do NOT supply a JSON in the first line, you need to hit ENTER # If you do NOT supply a JSON in the first line, you need to hit ENTER
twice to complete.` # 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 // 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)) 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) pageJsonOutput(conf, raw)
} }
}
//nolint:nilerr //nolint:nilerr
return nil return nil
@@ -216,8 +223,10 @@ func CallAPI(conf *cfg.Config, verb, path, data string) ([]byte, error) {
return nil, err return nil, err
} }
if !conf.HumanCat && strings.HasPrefix(path, "/_cat") {
req.Header.Add("Content-Type", "application/json") req.Header.Add("Content-Type", "application/json")
req.Header.Add("Accept", "application/json") req.Header.Add("Accept", "application/json")
}
// make sure we have got all we need // make sure we have got all we need
if err := conf.DefaultCluster.CheckAuth(); err != nil { if err := conf.DefaultCluster.CheckAuth(); err != nil {
@@ -271,7 +280,8 @@ func prettyfiJson(conf *cfg.Config, raw []byte) (string, error) {
err := json.Indent(&pretty, raw, "", "\t") err := json.Indent(&pretty, raw, "", "\t")
if err != nil { if err != nil {
return "", fmt.Errorf("json parse error: %w", err) //nolint:nilerr
return string(raw), nil
} }
return pretty.String(), nil return pretty.String(), nil

View File

@@ -37,11 +37,11 @@ func NodeList(conf *cfg.Config) error {
slog.Debug("ES result", "nodes", nodes) slog.Debug("ES result", "nodes", nodes)
table := printer.NewTable(conf, 7, len(nodes)) table := printer.NewTableEmpty(conf).WithHeaders(
table.Addheaders("name", "ip", "load1m", "load5m", "load15m", "ram %", "heap %") "name", "ip", "load1m", "load5m", "load15m", "ram %", "heap %")
for idx, node := range nodes { for _, node := range nodes {
table.Entries[idx] = []any{ table.AddRow(
*node.Name, *node.Name,
*node.Ip, *node.Ip,
*node.Load1M, *node.Load1M,
@@ -49,7 +49,7 @@ func NodeList(conf *cfg.Config) error {
*node.Load15M, *node.Load15M,
node.RamPercent, node.RamPercent,
node.HeapPercent, node.HeapPercent,
} )
} }
table.Sort() table.Sort()
@@ -235,3 +235,72 @@ func NodeClients(conf *cfg.Config, nodename string) error {
return table.Print() return table.Print()
} }
func NodeUsage(conf *cfg.Config, nodeid string) error {
usage := conf.DefaultCluster.ES().Nodes.Usage()
if nodeid != "" {
usage.NodeId(nodeid)
}
stats, err := usage.Do(context.Background())
if err != nil {
return fmt.Errorf("failed to get node usage: %w", esErrorString(err))
}
slog.Debug("ES result", "usage", stats)
table := printer.NewTableEmpty(conf).WithHeaders(
"node",
"bulk",
"doc get",
"doc mget",
"doc update",
"index doc",
"index stats",
"search",
"msearch",
"open pit",
)
for id, actions := range stats.Nodes {
node, err := getNodeName(conf, id)
if err != nil {
return err
}
stat := actions.RestActions
table.AddRow(
node,
stat["bulk_action"],
stat["document_get_action"],
stat["document_mget_action"],
stat["document_update_action"],
stat["document_index_action"],
stat["indices_stats_action"],
stat["search_action"],
stat["msearch_action"],
stat["open_point_in_time"],
)
}
return table.Print()
}
func getNodeName(conf *cfg.Config, id string) (string, error) {
res, err := conf.DefaultCluster.ES().Nodes.Info().
Metric("os").
Do(context.Background())
if err != nil {
return "", fmt.Errorf("failed to get node info: %w", esErrorString(err))
}
for nodeid, node := range res.Nodes {
if id == nodeid {
return node.Name, nil
}
}
return "", nil
}