mirror of
https://codeberg.org/scip/esctl.git
synced 2026-08-24 10:04:18 +02:00
enhance cluster status, add word wrap to wide table rows (#62)
This commit is contained in:
1
go.mod
1
go.mod
@@ -85,6 +85,7 @@ require (
|
|||||||
github.com/olekukonko/errors v1.2.0 // indirect
|
github.com/olekukonko/errors v1.2.0 // indirect
|
||||||
github.com/olekukonko/ll v0.1.8 // indirect
|
github.com/olekukonko/ll v0.1.8 // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
|
github.com/seeruk/go-wordwrap v0.0.0-20191208221741-14ec4aac9550 // indirect
|
||||||
github.com/tidwall/match v1.1.1 // indirect
|
github.com/tidwall/match v1.1.1 // indirect
|
||||||
github.com/tidwall/pretty v1.2.0 // indirect
|
github.com/tidwall/pretty v1.2.0 // indirect
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
|
|||||||
2
go.sum
2
go.sum
@@ -171,6 +171,8 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
|||||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||||
|
github.com/seeruk/go-wordwrap v0.0.0-20191208221741-14ec4aac9550 h1:C3CfUXH/qmWuQFRqnPm3Sx8PFxa+pqACjhV5CaNO8pw=
|
||||||
|
github.com/seeruk/go-wordwrap v0.0.0-20191208221741-14ec4aac9550/go.mod h1:Sl541M2Em6rRG3V9WObycR7MYFZiERVkd/TJg0Gt0U4=
|
||||||
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
|
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
|
||||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
|||||||
38
pkg/cfg/term.go
Normal file
38
pkg/cfg/term.go
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
/*
|
||||||
|
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 cfg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"golang.org/x/term"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DefaultMargin = 4
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetTermWidth() int {
|
||||||
|
if term.IsTerminal(int(os.Stdout.Fd())) {
|
||||||
|
width, _, err := term.GetSize(int(os.Stdout.Fd()))
|
||||||
|
if err == nil {
|
||||||
|
return width - DefaultMargin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 80
|
||||||
|
}
|
||||||
@@ -41,11 +41,9 @@ import (
|
|||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
"github.com/chzyer/readline"
|
"github.com/chzyer/readline"
|
||||||
"github.com/go-openapi/spec"
|
"github.com/go-openapi/spec"
|
||||||
"golang.org/x/term"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
DefaultMargin = 4
|
|
||||||
intro = `Input format: verb path [data]"
|
intro = `Input format: verb path [data]"
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
@@ -328,19 +326,19 @@ func ApiShow(conf *cfg.Config, showpath, verb string) error {
|
|||||||
|
|
||||||
cleanMarkup := regexp.MustCompile(`<[^<>]+>`)
|
cleanMarkup := regexp.MustCompile(`<[^<>]+>`)
|
||||||
|
|
||||||
width := getTermWidth()
|
width := cfg.GetTermWidth()
|
||||||
params := getApiParameters(op, showpath, width)
|
params := getApiParameters(op, showpath, width)
|
||||||
sample := getApiExample(op)
|
sample := getApiExample(op)
|
||||||
description := markdown.Render(cleanMarkup.ReplaceAllString(op.Op.Description, ""), width, DefaultMargin)
|
description := markdown.Render(cleanMarkup.ReplaceAllString(op.Op.Description, ""), width, cfg.DefaultMargin)
|
||||||
|
|
||||||
var bold = lipgloss.NewStyle().
|
var bold = lipgloss.NewStyle().
|
||||||
Bold(true)
|
Bold(true)
|
||||||
var paragraph = lipgloss.NewStyle().
|
var paragraph = lipgloss.NewStyle().
|
||||||
MarginBottom(1).
|
MarginBottom(1).
|
||||||
MarginLeft(DefaultMargin)
|
MarginLeft(cfg.DefaultMargin)
|
||||||
var boldparagraph = lipgloss.NewStyle().
|
var boldparagraph = lipgloss.NewStyle().
|
||||||
MarginBottom(1).
|
MarginBottom(1).
|
||||||
MarginLeft(DefaultMargin).
|
MarginLeft(cfg.DefaultMargin).
|
||||||
Bold(true)
|
Bold(true)
|
||||||
var indentparagraph = lipgloss.NewStyle().
|
var indentparagraph = lipgloss.NewStyle().
|
||||||
MarginBottom(1).
|
MarginBottom(1).
|
||||||
@@ -423,7 +421,7 @@ func getApiParameters(op *Op, path string, width int) Params {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
par := Param{
|
par := Param{
|
||||||
Description: string(markdown.Render(param.Description, width, DefaultMargin)),
|
Description: string(markdown.Render(param.Description, width, cfg.DefaultMargin)),
|
||||||
Param: param.Name,
|
Param: param.Name,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -527,14 +525,3 @@ func findOperation(item spec.PathItemProps) []*Op {
|
|||||||
|
|
||||||
return ops
|
return ops
|
||||||
}
|
}
|
||||||
|
|
||||||
func getTermWidth() int {
|
|
||||||
if term.IsTerminal(int(os.Stdout.Fd())) {
|
|
||||||
width, _, err := term.GetSize(int(os.Stdout.Fd()))
|
|
||||||
if err == nil {
|
|
||||||
return width - DefaultMargin
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return 80
|
|
||||||
}
|
|
||||||
|
|||||||
59
pkg/es/cluser_health_report.go
Normal file
59
pkg/es/cluser_health_report.go
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
/*
|
||||||
|
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 (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"codeberg.org/scip/esctl/pkg/cfg"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HealthReport struct {
|
||||||
|
Indicators map[string]HealthReportIndicator
|
||||||
|
}
|
||||||
|
|
||||||
|
type HealthReportIndicator struct {
|
||||||
|
Status, Symptom string
|
||||||
|
Diagnosis []HealthReportDiagnosis
|
||||||
|
}
|
||||||
|
|
||||||
|
type HealthReportDiagnosis struct {
|
||||||
|
Id, Cause, Action string
|
||||||
|
AffectedResources map[string][]string `json:"affected_resources"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// we do not use the go-elasticsearch client API here but call the ES
|
||||||
|
// API directly, because the returned structure (a
|
||||||
|
// healthreport.Response) is not iterable, you'd have to explicitly
|
||||||
|
// call every indicator type and every cause etc which also have
|
||||||
|
// different types each. To check which is !green would result in a
|
||||||
|
// gigantic function.
|
||||||
|
func getHealthReport(conf *cfg.Config) (*HealthReport, error) {
|
||||||
|
raw, err := CallAPI(conf, "GET", "/_health_report", "")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
report := HealthReport{}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(raw, &report); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal healthreport response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &report, nil
|
||||||
|
}
|
||||||
@@ -25,12 +25,7 @@ import (
|
|||||||
"codeberg.org/scip/esctl/pkg/cfg"
|
"codeberg.org/scip/esctl/pkg/cfg"
|
||||||
"codeberg.org/scip/esctl/pkg/printer"
|
"codeberg.org/scip/esctl/pkg/printer"
|
||||||
"github.com/dustin/go-humanize"
|
"github.com/dustin/go-humanize"
|
||||||
"github.com/elastic/go-elasticsearch/v9/typedapi/cat/indices"
|
|
||||||
"github.com/elastic/go-elasticsearch/v9/typedapi/cat/tasks"
|
|
||||||
"github.com/elastic/go-elasticsearch/v9/typedapi/ccr/stats"
|
|
||||||
"github.com/elastic/go-elasticsearch/v9/typedapi/cluster/health"
|
|
||||||
clusterstats "github.com/elastic/go-elasticsearch/v9/typedapi/cluster/stats"
|
clusterstats "github.com/elastic/go-elasticsearch/v9/typedapi/cluster/stats"
|
||||||
"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"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -78,8 +73,8 @@ func ClusterList(conf *cfg.Config) error {
|
|||||||
|
|
||||||
// We're using goroutines here to parallelize API requests, since we
|
// We're using goroutines here to parallelize API requests, since we
|
||||||
// have to do 3 of'em for each cluster. This speeds things up.
|
// have to do 3 of'em for each cluster. This speeds things up.
|
||||||
func ClusterStatus(conf *cfg.Config) error {
|
func getClusterStatus(conf *cfg.Config) (*apiResponse, error) {
|
||||||
gocount := 5
|
gocount := 6
|
||||||
if conf.Verbose {
|
if conf.Verbose {
|
||||||
gocount++
|
gocount++
|
||||||
}
|
}
|
||||||
@@ -90,65 +85,72 @@ func ClusterStatus(conf *cfg.Config) error {
|
|||||||
wg := &sync.WaitGroup{}
|
wg := &sync.WaitGroup{}
|
||||||
|
|
||||||
wg.Add(gocount)
|
wg.Add(gocount)
|
||||||
go getApiData(es, wg, responses, "health")
|
go getApiData(conf, es, wg, responses, "health")
|
||||||
go getApiData(es, wg, responses, "info")
|
go getApiData(conf, es, wg, responses, "healthreport")
|
||||||
go getApiData(es, wg, responses, "ccrstats")
|
go getApiData(conf, es, wg, responses, "info")
|
||||||
go getApiData(es, wg, responses, "indices")
|
go getApiData(conf, es, wg, responses, "ccr")
|
||||||
go getApiData(es, wg, responses, "tasks")
|
go getApiData(conf, es, wg, responses, "indices")
|
||||||
|
go getApiData(conf, es, wg, responses, "tasks")
|
||||||
|
|
||||||
if conf.Verbose {
|
if conf.Verbose {
|
||||||
go getApiData(es, wg, responses, "stats")
|
go getApiData(conf, es, wg, responses, "stats")
|
||||||
}
|
}
|
||||||
|
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
|
||||||
var clusterhealth *health.Response
|
all := apiResponse{}
|
||||||
var info *info.Response
|
|
||||||
var ccrstats *stats.Response
|
|
||||||
var clusterstats *clusterstats.Response
|
|
||||||
var indexstats *indices.Response
|
|
||||||
var taskstatus *tasks.Response
|
|
||||||
|
|
||||||
for i := 0; i < gocount; i++ {
|
for i := 0; i < gocount; i++ {
|
||||||
r := <-responses
|
r := <-responses
|
||||||
|
|
||||||
if r.error != nil {
|
if r.error != nil {
|
||||||
return r.error
|
return nil, r.error
|
||||||
}
|
}
|
||||||
|
|
||||||
switch r.which {
|
switch r.which {
|
||||||
case ResponseHealth:
|
case ResponseHealth:
|
||||||
clusterhealth = r.health
|
all.health = r.health
|
||||||
case ResponseCcr:
|
case ResponseCcr:
|
||||||
ccrstats = r.ccr
|
all.ccr = r.ccr
|
||||||
case ResponseInfo:
|
case ResponseInfo:
|
||||||
info = r.info
|
all.info = r.info
|
||||||
case ResponseStats:
|
case ResponseStats:
|
||||||
clusterstats = r.stats
|
all.stats = r.stats
|
||||||
case ResponseIndices:
|
case ResponseIndices:
|
||||||
indexstats = r.indices
|
all.indices = r.indices
|
||||||
case ResponseTasks:
|
case ResponseTasks:
|
||||||
taskstatus = r.tasks
|
all.tasks = r.tasks
|
||||||
|
case ResponseHealthReport:
|
||||||
|
all.healthreport = r.healthreport
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
slog.Debug("ES result", "cluster health", clusterhealth)
|
return &all, nil
|
||||||
|
}
|
||||||
|
|
||||||
isleader := len(ccrstats.AutoFollowStats.AutoFollowedClusters) == 0
|
func ClusterStatus(conf *cfg.Config) error {
|
||||||
|
res, err := getClusterStatus(conf)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Debug("ES result", "cluster health", res.health)
|
||||||
|
|
||||||
|
isleader := len(res.ccr.AutoFollowStats.AutoFollowedClusters) == 0
|
||||||
|
|
||||||
ccrfollowing := ""
|
ccrfollowing := ""
|
||||||
if len(ccrstats.AutoFollowStats.AutoFollowedClusters) > 0 {
|
if len(res.ccr.AutoFollowStats.AutoFollowedClusters) > 0 {
|
||||||
// is following another cluster
|
// is following another cluster
|
||||||
ccrfollowing = fmt.Sprintf("%s (%d/%d)",
|
ccrfollowing = fmt.Sprintf("%s (%d/%d)",
|
||||||
ccrstats.AutoFollowStats.AutoFollowedClusters[0].ClusterName,
|
res.ccr.AutoFollowStats.AutoFollowedClusters[0].ClusterName,
|
||||||
ccrstats.AutoFollowStats.NumberOfSuccessfulFollowIndices,
|
res.ccr.AutoFollowStats.NumberOfSuccessfulFollowIndices,
|
||||||
ccrstats.AutoFollowStats.NumberOfFailedFollowIndices,
|
res.ccr.AutoFollowStats.NumberOfFailedFollowIndices,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// look for red indices, if any
|
// look for red indices, if any
|
||||||
redindices := 0
|
redindices := 0
|
||||||
for _, index := range *indexstats {
|
for _, index := range *res.indices {
|
||||||
if *index.Health == "red" {
|
if *index.Health == "red" {
|
||||||
redindices++
|
redindices++
|
||||||
}
|
}
|
||||||
@@ -156,7 +158,7 @@ func ClusterStatus(conf *cfg.Config) error {
|
|||||||
|
|
||||||
// look for long running tasks
|
// look for long running tasks
|
||||||
longtasks := 0
|
longtasks := 0
|
||||||
for _, task := range *taskstatus {
|
for _, task := range *res.tasks {
|
||||||
if strings.Contains(*task.RunningTime, "d") {
|
if strings.Contains(*task.RunningTime, "d") {
|
||||||
longtasks++
|
longtasks++
|
||||||
}
|
}
|
||||||
@@ -166,16 +168,16 @@ func ClusterStatus(conf *cfg.Config) error {
|
|||||||
table.Addheaders(conf.DefaultCluster.Name, "status")
|
table.Addheaders(conf.DefaultCluster.Name, "status")
|
||||||
|
|
||||||
table.Entries = [][]string{
|
table.Entries = [][]string{
|
||||||
{"Cluster Name", clusterhealth.ClusterName},
|
{"Cluster Name", res.health.ClusterName},
|
||||||
{"ES Status", printer.Colorize(conf, clusterhealth.Status.Name, clusterhealth.Status.Name)},
|
{"ES Status", printer.Colorize(conf, res.health.Status.Name, res.health.Status.Name)},
|
||||||
{"ES Version", info.Version.Int},
|
{"ES Version", res.info.Version.Int},
|
||||||
{"Is Leader", fmt.Sprintf("%t", isleader)},
|
{"Is Leader", fmt.Sprintf("%t", isleader)},
|
||||||
{"Active Shards", fmt.Sprintf("%d", clusterhealth.ActiveShards)},
|
{"Active Shards", fmt.Sprintf("%d", res.health.ActiveShards)},
|
||||||
{"Active Primary Shards", fmt.Sprintf("%d", clusterhealth.ActivePrimaryShards)},
|
{"Active Primary Shards", fmt.Sprintf("%d", res.health.ActivePrimaryShards)},
|
||||||
{"Unassigned Shards", fmt.Sprintf("%d", clusterhealth.UnassignedShards)},
|
{"Unassigned Shards", fmt.Sprintf("%d", res.health.UnassignedShards)},
|
||||||
{"Unassigned Primary Shards", fmt.Sprintf("%d", clusterhealth.UnassignedPrimaryShards)},
|
{"Unassigned Primary Shards", fmt.Sprintf("%d", res.health.UnassignedPrimaryShards)},
|
||||||
{"Pending Tasks", fmt.Sprintf("%d", clusterhealth.NumberOfPendingTasks)},
|
{"Pending Tasks", fmt.Sprintf("%d", res.health.NumberOfPendingTasks)},
|
||||||
{"Nodes", fmt.Sprintf("%d", clusterhealth.NumberOfNodes)},
|
{"Nodes", fmt.Sprintf("%d", res.health.NumberOfNodes)},
|
||||||
{"Red Indices", fmt.Sprintf("%d", redindices)},
|
{"Red Indices", fmt.Sprintf("%d", redindices)},
|
||||||
{"Long Running Tasks", fmt.Sprintf("%d", longtasks)},
|
{"Long Running Tasks", fmt.Sprintf("%d", longtasks)},
|
||||||
}
|
}
|
||||||
@@ -183,12 +185,30 @@ func ClusterStatus(conf *cfg.Config) error {
|
|||||||
if !isleader {
|
if !isleader {
|
||||||
table.Entries = append(table.Entries, [][]string{
|
table.Entries = append(table.Entries, [][]string{
|
||||||
{"AutoFollow (success/failed indices)", ccrfollowing},
|
{"AutoFollow (success/failed indices)", ccrfollowing},
|
||||||
{"Followed Indices", fmt.Sprintf("%d", len(ccrstats.FollowStats.Indices))},
|
{"Followed Indices", fmt.Sprintf("%d", len(res.ccr.FollowStats.Indices))},
|
||||||
}...)
|
}...)
|
||||||
}
|
}
|
||||||
|
|
||||||
if conf.Verbose {
|
if conf.Verbose {
|
||||||
table = gatherClusterStats(conf, clusterstats, table)
|
table = gatherClusterStats(conf, res.stats, table)
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.health.Status.Name != "green" {
|
||||||
|
for name, indicator := range res.healthreport.Indicators {
|
||||||
|
if indicator.Status != "green" {
|
||||||
|
table.Entries = append(table.Entries, []string{
|
||||||
|
printer.Colorize(conf, indicator.Status, "Bad health "+name), indicator.Symptom,
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, diag := range indicator.Diagnosis {
|
||||||
|
table.Entries = append(table.Entries, []string{" -> cause", diag.Cause})
|
||||||
|
|
||||||
|
for resource, items := range diag.AffectedResources {
|
||||||
|
table.Entries = append(table.Entries, []string{" -> affected " + resource, strings.Join(items, ",")})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := table.Print(); err != nil {
|
if err := table.Print(); err != nil {
|
||||||
|
|||||||
48
pkg/es/debug.go
Normal file
48
pkg/es/debug.go
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
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 (
|
||||||
|
"codeberg.org/scip/esctl/pkg/cfg"
|
||||||
|
"github.com/alecthomas/repr"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Debug(conf *cfg.Config) error {
|
||||||
|
report, err := getHealthReport(conf)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
repr.Println(report)
|
||||||
|
/*
|
||||||
|
res, err := conf.DefaultCluster.ES().Search().
|
||||||
|
Index(conf.Index).
|
||||||
|
Size(0).
|
||||||
|
Aggregations(map[string]types.Aggregations{
|
||||||
|
"min_ts": *esdsl.NewMinAggregation().Field("@timestamp").AggregationsCaster(),
|
||||||
|
"max_ts": *esdsl.NewMaxAggregation().Field("@timestamp").AggregationsCaster(),
|
||||||
|
}).
|
||||||
|
Do(context.Background())
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
repr.Println(res)
|
||||||
|
*/
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -190,9 +190,9 @@ func getIlmPhaseData(conf *cfg.Config) ([]PhaseData, error) {
|
|||||||
wg := &sync.WaitGroup{}
|
wg := &sync.WaitGroup{}
|
||||||
wg.Add(3)
|
wg.Add(3)
|
||||||
|
|
||||||
go getApiData(conf.DefaultCluster.ES(), wg, responses, "indicesbytes")
|
go getApiData(conf, conf.DefaultCluster.ES(), wg, responses, "indicesbytes")
|
||||||
go getApiData(conf.DefaultCluster.ES(), wg, responses, "explain")
|
go getApiData(conf, conf.DefaultCluster.ES(), wg, responses, "explain")
|
||||||
go getApiData(conf.DefaultCluster.ES(), wg, responses, "policies")
|
go getApiData(conf, conf.DefaultCluster.ES(), wg, responses, "policies")
|
||||||
|
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
|
||||||
|
|||||||
@@ -18,10 +18,10 @@ package es
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"codeberg.org/scip/esctl/pkg/cfg"
|
||||||
"github.com/elastic/go-elasticsearch/v9"
|
"github.com/elastic/go-elasticsearch/v9"
|
||||||
"github.com/elastic/go-elasticsearch/v9/typedapi/cat/indices"
|
"github.com/elastic/go-elasticsearch/v9/typedapi/cat/indices"
|
||||||
"github.com/elastic/go-elasticsearch/v9/typedapi/cat/tasks"
|
"github.com/elastic/go-elasticsearch/v9/typedapi/cat/tasks"
|
||||||
@@ -43,12 +43,14 @@ const (
|
|||||||
ResponseTasks
|
ResponseTasks
|
||||||
ResponseExplain
|
ResponseExplain
|
||||||
ResponseLifecycle
|
ResponseLifecycle
|
||||||
|
ResponseHealthReport
|
||||||
)
|
)
|
||||||
|
|
||||||
type apiResponse struct {
|
type apiResponse struct {
|
||||||
error error
|
error error
|
||||||
info *info.Response
|
info *info.Response
|
||||||
health *health.Response
|
health *health.Response
|
||||||
|
healthreport *HealthReport
|
||||||
ccr *stats.Response
|
ccr *stats.Response
|
||||||
stats *clusterstats.Response
|
stats *clusterstats.Response
|
||||||
indices *indices.Response
|
indices *indices.Response
|
||||||
@@ -59,12 +61,16 @@ type apiResponse struct {
|
|||||||
which int
|
which int
|
||||||
}
|
}
|
||||||
|
|
||||||
func getApiData(es *elasticsearch.TypedClient, wg *sync.WaitGroup,
|
func getApiData(
|
||||||
reschan chan apiResponse, which string) {
|
conf *cfg.Config,
|
||||||
|
es *elasticsearch.TypedClient,
|
||||||
|
wg *sync.WaitGroup,
|
||||||
|
reschan chan apiResponse,
|
||||||
|
which string) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
|
|
||||||
ar := apiResponse{}
|
ar := apiResponse{}
|
||||||
arerr := errors.New("")
|
var arerr error
|
||||||
|
|
||||||
switch which {
|
switch which {
|
||||||
case "health":
|
case "health":
|
||||||
@@ -75,6 +81,13 @@ func getApiData(es *elasticsearch.TypedClient, wg *sync.WaitGroup,
|
|||||||
ar.which = ResponseHealth
|
ar.which = ResponseHealth
|
||||||
arerr = err
|
arerr = err
|
||||||
|
|
||||||
|
case "healthreport":
|
||||||
|
report, err := getHealthReport(conf)
|
||||||
|
|
||||||
|
ar.healthreport = report
|
||||||
|
ar.which = ResponseHealthReport
|
||||||
|
arerr = err
|
||||||
|
|
||||||
case "info":
|
case "info":
|
||||||
res, err := es.Info().
|
res, err := es.Info().
|
||||||
Do(context.Background())
|
Do(context.Background())
|
||||||
@@ -83,7 +96,7 @@ func getApiData(es *elasticsearch.TypedClient, wg *sync.WaitGroup,
|
|||||||
ar.which = ResponseInfo
|
ar.which = ResponseInfo
|
||||||
arerr = err
|
arerr = err
|
||||||
|
|
||||||
case "ccrstats":
|
case "ccr":
|
||||||
res, err := es.Ccr.Stats().
|
res, err := es.Ccr.Stats().
|
||||||
Do(context.Background())
|
Do(context.Background())
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ import (
|
|||||||
|
|
||||||
"codeberg.org/scip/esctl/pkg/cfg"
|
"codeberg.org/scip/esctl/pkg/cfg"
|
||||||
"codeberg.org/scip/esctl/pkg/printer"
|
"codeberg.org/scip/esctl/pkg/printer"
|
||||||
"github.com/alecthomas/repr"
|
|
||||||
"github.com/elastic/go-elasticsearch/v9/typedapi/core/search"
|
"github.com/elastic/go-elasticsearch/v9/typedapi/core/search"
|
||||||
"github.com/elastic/go-elasticsearch/v9/typedapi/esdsl"
|
"github.com/elastic/go-elasticsearch/v9/typedapi/esdsl"
|
||||||
"github.com/elastic/go-elasticsearch/v9/typedapi/indices/validatequery"
|
"github.com/elastic/go-elasticsearch/v9/typedapi/indices/validatequery"
|
||||||
@@ -149,25 +148,6 @@ func validateSearch(conf *cfg.Config, queries []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func Debug(conf *cfg.Config) error {
|
|
||||||
res, err := conf.DefaultCluster.ES().Search().
|
|
||||||
Index(conf.Index).
|
|
||||||
Size(0).
|
|
||||||
Aggregations(map[string]types.Aggregations{
|
|
||||||
"min_ts": *esdsl.NewMinAggregation().Field("@timestamp").AggregationsCaster(),
|
|
||||||
"max_ts": *esdsl.NewMaxAggregation().Field("@timestamp").AggregationsCaster(),
|
|
||||||
}).
|
|
||||||
Do(context.Background())
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
repr.Println(res)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func searchOnce(conf *cfg.Config, search *search.Search) error {
|
func searchOnce(conf *cfg.Config, search *search.Search) error {
|
||||||
res, err := search.
|
res, err := search.
|
||||||
From(conf.From).
|
From(conf.From).
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import (
|
|||||||
"github.com/olekukonko/tablewriter"
|
"github.com/olekukonko/tablewriter"
|
||||||
"github.com/olekukonko/tablewriter/renderer"
|
"github.com/olekukonko/tablewriter/renderer"
|
||||||
"github.com/olekukonko/tablewriter/tw"
|
"github.com/olekukonko/tablewriter/tw"
|
||||||
|
"github.com/seeruk/go-wordwrap"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -38,10 +39,11 @@ type Table struct {
|
|||||||
|
|
||||||
lenHeaders []int
|
lenHeaders []int
|
||||||
alignInts bool
|
alignInts bool
|
||||||
|
maxwidth int
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTable(conf *cfg.Config, columns, rows int) *Table {
|
func NewTable(conf *cfg.Config, columns, rows int) *Table {
|
||||||
table := Table{Mode: conf.Output}
|
table := Table{Mode: conf.Output, maxwidth: cfg.GetTermWidth()}
|
||||||
|
|
||||||
table.Headers = make([]string, columns)
|
table.Headers = make([]string, columns)
|
||||||
table.Entries = make([][]string, rows)
|
table.Entries = make([][]string, rows)
|
||||||
@@ -118,17 +120,31 @@ func (data *Table) PrintTSV() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, entries := range data.Entries {
|
for _, entries := range data.Entries {
|
||||||
|
currentWidth := 0
|
||||||
for idx, entry := range entries {
|
for idx, entry := range entries {
|
||||||
length := visibleLen(entry)
|
length := visibleLen(entry)
|
||||||
|
|
||||||
if data.lenHeaders[idx] < length {
|
if data.lenHeaders[idx] < length {
|
||||||
|
if length > currentWidth+data.maxwidth {
|
||||||
|
data.lenHeaders[idx] = data.maxwidth - currentWidth
|
||||||
|
} else {
|
||||||
data.lenHeaders[idx] = length
|
data.lenHeaders[idx] = length
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
currentWidth += data.lenHeaders[idx]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// output
|
// output headers
|
||||||
for idx, header := range data.Headers {
|
for idx, header := range data.Headers {
|
||||||
|
if idx+1 != len(data.Headers) {
|
||||||
fmt.Print(header, strings.Repeat(" ", data.lenHeaders[idx]-visibleLen(header)))
|
fmt.Print(header, strings.Repeat(" ", data.lenHeaders[idx]-visibleLen(header)))
|
||||||
|
} else {
|
||||||
|
// no padding for last header
|
||||||
|
fmt.Print(header)
|
||||||
|
}
|
||||||
|
|
||||||
if idx < len(data.Headers)-1 {
|
if idx < len(data.Headers)-1 {
|
||||||
fmt.Print(" ")
|
fmt.Print(" ")
|
||||||
}
|
}
|
||||||
@@ -136,14 +152,37 @@ func (data *Table) PrintTSV() error {
|
|||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|
||||||
for _, entries := range data.Entries {
|
for _, entries := range data.Entries {
|
||||||
|
currentWidth := 0
|
||||||
|
|
||||||
for idx, entry := range entries {
|
for idx, entry := range entries {
|
||||||
length := visibleLen(entry)
|
length := visibleLen(entry)
|
||||||
|
|
||||||
|
if length+currentWidth > data.maxwidth {
|
||||||
|
// text is too wide to be put into one line, wrap it
|
||||||
|
wrapper := wordwrap.Wrapper(data.maxwidth-currentWidth, false)
|
||||||
|
wrapped := wrapper(entry)
|
||||||
|
|
||||||
|
// and indent it
|
||||||
|
for idx, line := range strings.Split(wrapped, "\n") {
|
||||||
|
if idx == 0 {
|
||||||
|
entry = line
|
||||||
|
} else {
|
||||||
|
entry += "\n " + strings.Repeat(" ", currentWidth) + line
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
currentWidth += data.lenHeaders[idx]
|
||||||
|
|
||||||
if isInt(entry) && data.alignInts {
|
if isInt(entry) && data.alignInts {
|
||||||
// align right
|
// align right
|
||||||
fmt.Print(strings.Repeat(" ", data.lenHeaders[idx]-length), entry)
|
fmt.Print(strings.Repeat(" ", data.lenHeaders[idx]-length), entry)
|
||||||
} else {
|
} else if length < data.lenHeaders[idx] && idx+1 != len(entries) {
|
||||||
|
// pad right, if required
|
||||||
fmt.Print(entry, strings.Repeat(" ", data.lenHeaders[idx]-length))
|
fmt.Print(entry, strings.Repeat(" ", data.lenHeaders[idx]-length))
|
||||||
|
} else {
|
||||||
|
// no padding for last entry
|
||||||
|
fmt.Print(entry)
|
||||||
}
|
}
|
||||||
|
|
||||||
if idx < len(data.Headers)-1 {
|
if idx < len(data.Headers)-1 {
|
||||||
|
|||||||
Reference in New Issue
Block a user