diff --git a/cmd/root.go b/cmd/root.go index db7ec05..295ee0a 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -21,6 +21,7 @@ import ( "fmt" golog "log" "os" + "runtime/debug" "runtime/pprof" "strings" @@ -118,6 +119,12 @@ func Main() int { Usage: "enable HTTP debugging", Destination: &conf.DebugHTTP, }, + &cli.BoolFlag{ + Name: "debug-goroutines", + Value: false, + Usage: "enable goroutine debugging", + Destination: &conf.DebugGoRoutines, + }, &cli.BoolFlag{ Name: "align-ints", Aliases: []string{"I"}, @@ -253,6 +260,12 @@ func Version(conf *cfg.Config) *cli.Command { Usage: "show esctl version information", Action: func(ctx context.Context, cmd *cli.Command) error { + info, _ := debug.ReadBuildInfo() + + if strings.Contains(info.Main.Version, "+dirty") { + cfg.COMMIT += "+dirty" + } + _, err := fmt.Printf(versionFmt, cfg.Version, cfg.BUILD, cfg.BRANCH, cfg.COMMIT, cfg.GOVERSION, cfg.APIVERSION) diff --git a/pkg/cfg/cluster.go b/pkg/cfg/cluster.go index 65b86f9..f2ef359 100644 --- a/pkg/cfg/cluster.go +++ b/pkg/cfg/cluster.go @@ -194,18 +194,18 @@ func (cluster *Cluster) getDefaultOptions() []elasticsearch.Option { } func (cluster *Cluster) getTransport() elastictransport.Option { - transport := &http.Transport{ + transport := new(http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - } + }) if cluster.DebugHTTP { return elastictransport.WithTransport( - &DebugTransport{Transport: transport}, + new(DebugTransport{Transport: transport}), ) } return elastictransport.WithTransport( - &CompatibilityTransport{Transport: transport}, + new(CompatibilityTransport{Transport: transport}), ) } diff --git a/pkg/cfg/config.go b/pkg/cfg/config.go index 3ee5605..47626d3 100644 --- a/pkg/cfg/config.go +++ b/pkg/cfg/config.go @@ -28,7 +28,7 @@ import ( ) const ( - Version string = `v0.0.26` + Version string = `v0.0.27` ) var ( @@ -89,12 +89,13 @@ type Config struct { Force bool // ccr follower renew: -f - DebugHTTP bool // root: --debug-http - Separator string // role diff: -s - NotDeployed bool // role diff: -n - Undefined bool // role diff: -u - Diff bool // role diff: -D - Hidden bool // ds ls: -H + DebugHTTP bool // root: --debug-http + DebugGoRoutines bool // root: --debug-goroutines + Separator string // role diff: -s + NotDeployed bool // role diff: -n + Undefined bool // role diff: -u + Diff bool // role diff: -D + Hidden bool // ds ls: -H // rollover MaxAge string @@ -113,7 +114,7 @@ type Config struct { } func NewConfig() *Config { - return &Config{Clusters: map[string]*Cluster{}} + return new(Config{Clusters: map[string]*Cluster{}}) } func getDefaultPath() string { @@ -216,7 +217,7 @@ func (conf *Config) LoadConfig() error { return fmt.Errorf("failed to read config file: %w", err) } - newconf := &Config{} + newconf := new(Config{}) err = yaml.Unmarshal(data, newconf) if err != nil { diff --git a/pkg/es/api.go b/pkg/es/api.go index f02b733..63a6346 100644 --- a/pkg/es/api.go +++ b/pkg/es/api.go @@ -28,6 +28,7 @@ import ( "io" "log" "log/slog" + "maps" "net/http" "os" "os/exec" @@ -158,7 +159,7 @@ func ApiRepl(conf *cfg.Config) error { } func pageJsonOutput(conf *cfg.Config, raw []byte) { - tmpconf := &cfg.Config{HaveJQ: conf.HaveJQ} + tmpconf := new(cfg.Config{HaveJQ: conf.HaveJQ}) if conf.Pager != "" { tmpconf.HaveJQ = false @@ -206,16 +207,16 @@ func CallAPI(conf *cfg.Config, verb, path, data string) ([]byte, error) { verb = strings.ToUpper(verb) // we're using port-forwards anyway - noVerifyTransport := &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - } + noVerifyTransport := new(http.Transport{ + TLSClientConfig: new(tls.Config{InsecureSkipVerify: true}), + }) - client := &http.Client{Transport: noVerifyTransport} + client := new(http.Client{Transport: noVerifyTransport}) if conf.DebugHTTP { - client = &http.Client{ - Transport: &cfg.DebugTransport{ - Transport: noVerifyTransport}} + client = new(http.Client{ + Transport: new(cfg.DebugTransport{ + Transport: noVerifyTransport})}) } req, err := http.NewRequest(verb, conf.DefaultCluster.Uri+path, bytes.NewBuffer([]byte(data))) @@ -366,16 +367,7 @@ func ApiList(conf *cfg.Config, pattern string) error { func ApiPathNames() []string { assets.LoadAssetOpenApi() - paths := make([]string, len(assets.OpenAPI.Spec().Paths.Paths)) - - idx := 0 - - for path := range assets.OpenAPI.Spec().Paths.Paths { - paths[idx] = path - idx++ - } - - return paths + return slices.Collect(maps.Keys(assets.OpenAPI.Spec().Paths.Paths)) } func ApiShow(conf *cfg.Config, showpath, verb string) error { @@ -536,7 +528,7 @@ func getApiExample(op *Op) string { // otherwise showpath+verb have to match precisely. func matchOperation(showpath, verb string) (*Op, error) { ops := []*Op{} - op := &Op{} + op := new(Op{}) var found bool diff --git a/pkg/es/cluser_health_report.go b/pkg/es/cluser_health_report.go index e751e51..8a64938 100644 --- a/pkg/es/cluser_health_report.go +++ b/pkg/es/cluser_health_report.go @@ -17,6 +17,10 @@ along with this program. If not, see . package es import ( + + // "encoding/json/jsontext" + // "encoding/json/v2" + "encoding/json" "fmt" @@ -49,11 +53,22 @@ func getHealthReport(conf *cfg.Config) (*HealthReport, error) { return nil, err } - report := HealthReport{} + report := new(HealthReport{}) + + // FIXME: use this once jsonv2 is no more experimental it already + // builds and works like intended, but golangci-lint doesn't + // recognize it with: go: unknown GOEXPERIMENT jsonv2 + // + // if err := json.UnmarshalDecode( + // jsontext.NewDecoder( + // bytes.NewBuffer(raw)), + // &report); err != nil { + // return nil, fmt.Errorf("failed to unmarshal healthreport response: %w", err) + // } if err := json.Unmarshal(raw, &report); err != nil { return nil, fmt.Errorf("failed to unmarshal healthreport response: %w", err) } - return &report, nil + return report, nil } diff --git a/pkg/es/cluster.go b/pkg/es/cluster.go index a7e2988..e13bec8 100644 --- a/pkg/es/cluster.go +++ b/pkg/es/cluster.go @@ -101,23 +101,39 @@ func getClusterStatus(conf *cfg.Config) (*apiResponse, error) { es := conf.DefaultCluster.ES() responses := make(chan apiResponse, gocount) - wg := &sync.WaitGroup{} + wg := new(sync.WaitGroup{}) - wg.Add(gocount) - go getApiData(conf, es, wg, responses, "health") - go getApiData(conf, es, wg, responses, "healthreport") - go getApiData(conf, es, wg, responses, "info") - go getApiData(conf, es, wg, responses, "ccr") - go getApiData(conf, es, wg, responses, "indices") - go getApiData(conf, es, wg, responses, "tasks") + wg.Go(func() { + getApiData(conf, es, responses, "health") + }) + + wg.Go(func() { + getApiData(conf, es, responses, "healthreport") + }) + + wg.Go(func() { + getApiData(conf, es, responses, "info") + }) + + wg.Go(func() { + getApiData(conf, es, responses, "ccr") + }) + + wg.Go(func() { + getApiData(conf, es, responses, "indices") + }) + + wg.Go(func() { + getApiData(conf, es, responses, "tasks") + }) if conf.Verbose { - go getApiData(conf, es, wg, responses, "stats") + getApiData(conf, es, responses, "stats") } wg.Wait() - all := apiResponse{} + all := new(apiResponse{}) var err error @@ -144,7 +160,7 @@ func getClusterStatus(conf *cfg.Config) (*apiResponse, error) { } } - return &all, err + return all, err } func ClusterStatus(conf *cfg.Config) error { diff --git a/pkg/es/cluster_reroute.go b/pkg/es/cluster_reroute.go index 47582ce..4f957a1 100644 --- a/pkg/es/cluster_reroute.go +++ b/pkg/es/cluster_reroute.go @@ -29,12 +29,12 @@ func ClusterRerouteMove(conf *cfg.Config, index string) error { move := conf.DefaultCluster.ES().Cluster.Reroute() commands := esdsl.NewCommand() - moveCommand := &types.CommandMoveAction{ + moveCommand := new(types.CommandMoveAction{ Shard: conf.Shards, FromNode: conf.FromNode, ToNode: conf.ToNode, Index: index, - } + }) commands.CommandCaster().Move = moveCommand @@ -52,11 +52,11 @@ func ClusterRerouteAllocateReplica(conf *cfg.Config, index string) error { move := conf.DefaultCluster.ES().Cluster.Reroute() commands := esdsl.NewCommand() - allocCommand := &types.CommandAllocateReplicaAction{ + allocCommand := new(types.CommandAllocateReplicaAction{ Shard: conf.Shards, Node: conf.ToNode, Index: index, - } + }) commands.CommandCaster().AllocateReplica = allocCommand @@ -74,12 +74,12 @@ func ClusterRerouteCancel(conf *cfg.Config, index string) error { move := conf.DefaultCluster.ES().Cluster.Reroute() commands := esdsl.NewCommand() - cancelCommand := &types.CommandCancelAction{ + cancelCommand := new(types.CommandCancelAction{ Shard: conf.Shards, Node: conf.ToNode, Index: index, AllowPrimary: &conf.AllowPrimary, - } + }) commands.CommandCaster().Cancel = cancelCommand @@ -97,12 +97,12 @@ func ClusterRerouteAllocatePrimary(conf *cfg.Config, index string, stale bool) e move := conf.DefaultCluster.ES().Cluster.Reroute() commands := esdsl.NewCommand() - allocCommand := &types.CommandAllocatePrimaryAction{ + allocCommand := new(types.CommandAllocatePrimaryAction{ Shard: conf.Shards, Node: conf.ToNode, Index: index, AcceptDataLoss: conf.AcceptDataLoss, - } + }) if stale { commands.CommandCaster().AllocateStalePrimary = allocCommand diff --git a/pkg/es/doc.go b/pkg/es/doc.go index 30e9123..796ab13 100644 --- a/pkg/es/doc.go +++ b/pkg/es/doc.go @@ -101,7 +101,7 @@ func DocDelete(conf *cfg.Config, queries []string) error { return nil } - req := &deletebyquery.Request{} + req := new(deletebyquery.Request{}) if len(queries) == 0 && conf.All { req.Query = esdsl.NewMatchAllQuery().QueryCaster() diff --git a/pkg/es/ilm.go b/pkg/es/ilm.go index bac1a53..46562b8 100644 --- a/pkg/es/ilm.go +++ b/pkg/es/ilm.go @@ -22,6 +22,8 @@ import ( "errors" "fmt" "log/slog" + "maps" + "slices" "strings" "codeberg.org/scip/esctl/pkg/cfg" @@ -62,13 +64,7 @@ func IlmNames(conf *cfg.Config) ([]string, error) { return nil, fmt.Errorf("failed to get ilm policies: %w", esErrorString(err)) } - names := make([]string, len(res)) - idx := 0 - - for name := range res { - names[idx] = name - idx++ - } + names := slices.Collect(maps.Keys(res)) return names, nil } @@ -351,7 +347,7 @@ func IlmCreate(conf *cfg.Config, policyname string) error { var actions types.IlmActionsVariant = esdsl.NewIlmActions() - rollover := &types.RolloverAction{} + rollover := new(types.RolloverAction{}) haveroll := false if policy != nil { @@ -513,8 +509,8 @@ func IlmCreate(conf *cfg.Config, policyname string) error { phases.PhasesCaster().Delete = policy.Phases.Delete } - put := &putlifecycle.Request{} - newpolicy := &types.IlmPolicy{} + put := new(putlifecycle.Request{}) + newpolicy := new(types.IlmPolicy{}) newpolicy.IlmPolicyCaster().Phases = *phases.PhasesCaster() put.Policy = newpolicy diff --git a/pkg/es/ilm_forecast.go b/pkg/es/ilm_forecast.go index ee659e1..be7c19f 100644 --- a/pkg/es/ilm_forecast.go +++ b/pkg/es/ilm_forecast.go @@ -185,12 +185,19 @@ func virtualAge(phase *PhaseData) time.Duration { // Retrieve all index, ilm-explain and ilm-policies in parallel func getIlmPhaseData(conf *cfg.Config) ([]PhaseData, error) { responses := make(chan apiResponse, 3) - wg := &sync.WaitGroup{} - wg.Add(3) + wg := new(sync.WaitGroup{}) - go getApiData(conf, conf.DefaultCluster.ES(), wg, responses, "indicesbytes") - go getApiData(conf, conf.DefaultCluster.ES(), wg, responses, "explain") - go getApiData(conf, conf.DefaultCluster.ES(), wg, responses, "policies") + wg.Go(func() { + getApiData(conf, conf.DefaultCluster.ES(), responses, "indicesbytes") + }) + + wg.Go(func() { + getApiData(conf, conf.DefaultCluster.ES(), responses, "explain") + }) + + wg.Go(func() { + getApiData(conf, conf.DefaultCluster.ES(), responses, "policies") + }) wg.Wait() @@ -331,7 +338,7 @@ func findNextPhase(policy types.IlmPolicy, currentPhase string) *NextPhase { // phase list to determine which comes next phases, start := registerPhases(policy, currentPhase) - nextPhase := &NextPhase{} + nextPhase := new(NextPhase{}) // finally determine which phase comes next // exception: hot, where we look for rollover rules diff --git a/pkg/es/parallel.go b/pkg/es/parallel.go index 7d41046..b32550c 100644 --- a/pkg/es/parallel.go +++ b/pkg/es/parallel.go @@ -19,7 +19,6 @@ package es import ( "context" "fmt" - "sync" "codeberg.org/scip/esctl/pkg/cfg" "github.com/elastic/go-elasticsearch/v9" @@ -61,14 +60,7 @@ type apiResponse struct { which int } -func getApiData( - conf *cfg.Config, - es *elasticsearch.TypedClient, - wg *sync.WaitGroup, - reschan chan apiResponse, - which string) { - defer wg.Done() - +func getApiData(conf *cfg.Config, es *elasticsearch.TypedClient, reschan chan apiResponse, which string) { apiRes := apiResponse{} var arerr error diff --git a/pkg/es/role.go b/pkg/es/role.go index 08341e8..3665940 100644 --- a/pkg/es/role.go +++ b/pkg/es/role.go @@ -20,6 +20,8 @@ import ( "context" "fmt" "log/slog" + "maps" + "slices" "codeberg.org/scip/esctl/pkg/cfg" "codeberg.org/scip/esctl/pkg/printer" @@ -33,15 +35,7 @@ func RoleNames(conf *cfg.Config) ([]string, error) { return nil, fmt.Errorf("failed to get roles: %w", esErrorString(err)) } - roles := make([]string, len(res)) - - idx := 0 - for name := range res { - roles[idx] = name - idx++ - } - - return roles, nil + return slices.Collect(maps.Keys(res)), nil } func RoleList(conf *cfg.Config) error { diff --git a/pkg/es/role_diff.go b/pkg/es/role_diff.go index 4bdd416..1f16588 100644 --- a/pkg/es/role_diff.go +++ b/pkg/es/role_diff.go @@ -127,7 +127,7 @@ func getCsvRecord(conf *cfg.Config, csvfile, rolename string) (*Record, error) { }() scanner := bufio.NewScanner(fd) - record := Record{role: rolename} + record := new(Record{role: rolename}) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) @@ -150,7 +150,7 @@ func getCsvRecord(conf *cfg.Config, csvfile, rolename string) (*Record, error) { } } - return &record, nil + return record, nil } func diffRoles(conf *cfg.Config, records map[string]Record, res getrole.Response) []Register { diff --git a/pkg/es/search.go b/pkg/es/search.go index 44a6bda..4cd3bc4 100644 --- a/pkg/es/search.go +++ b/pkg/es/search.go @@ -56,7 +56,7 @@ func Search(conf *cfg.Config, queries []string) error { return err } - req := &search.Request{Query: queryCaster} + req := new(search.Request{Query: queryCaster}) searchEs.Request(req) @@ -128,7 +128,7 @@ func validateSearch(conf *cfg.Config, queries []string) error { return err } - req := &validatequery.Request{Query: queryCaster} + req := new(validatequery.Request{Query: queryCaster}) validate.Request(req) diff --git a/pkg/es/search_filter.go b/pkg/es/search_filter.go index 5385ab2..cf5ffaf 100644 --- a/pkg/es/search_filter.go +++ b/pkg/es/search_filter.go @@ -80,7 +80,7 @@ func NewFilter(query string) (*filter, error) { return nil, errors.New("search queries must be in the form fieldpattern where must be one of: = or !=") } - flt := &filter{term: part[0], filter: part[1], criteria: criteria} + flt := new(filter{term: part[0], filter: part[1], criteria: criteria}) if strings.Contains(part[0], ",") { // a MultiMatchQuery, match across multiple fields at once diff --git a/pkg/es/snapshot.go b/pkg/es/snapshot.go index f9b2449..aa2fb6d 100644 --- a/pkg/es/snapshot.go +++ b/pkg/es/snapshot.go @@ -66,13 +66,13 @@ func SnapshotList(conf *cfg.Config) error { snapshots := []*Snapshot{} // original snapshot names for _, snapshot := range sres { - snap := &Snapshot{ + snap := new(Snapshot{ Name: *snapshot.Id, Status: *snapshot.Status, Start: fmt.Sprintf("%s", snapshot.StartTime), Forindex: indexFromSnapshot(*snapshot.Id), Orphaned: "no", - } + }) _, exists := indicies[snap.Forindex] if !exists { diff --git a/pkg/log/logger.go b/pkg/log/logger.go index c65791f..e199019 100644 --- a/pkg/log/logger.go +++ b/pkg/log/logger.go @@ -29,13 +29,13 @@ import ( const LevelNotice = slog.Level(2) func Init(conf *cfg.Config) { - logLevel := &slog.LevelVar{} + logLevel := new(slog.LevelVar{}) - opts := &yadu.Options{ + opts := new(yadu.Options{ Level: logLevel, AddSource: true, NoColor: !isatty.IsTerminal(os.Stdout.Fd()), - } + }) buildInfo, _ := debug.ReadBuildInfo() diff --git a/pkg/printer/bytes.go b/pkg/printer/bytes.go index 39cfcb5..ecbeaa7 100644 --- a/pkg/printer/bytes.go +++ b/pkg/printer/bytes.go @@ -27,7 +27,7 @@ func (b *ByteSize) String() string { } func Bytes(size int64) *ByteSize { - return &ByteSize{size: uint64(size)} + return new(ByteSize{size: uint64(size)}) } func ByteString(size int64) string { diff --git a/pkg/printer/table.go b/pkg/printer/table.go index 55b62d0..bcb5826 100644 --- a/pkg/printer/table.go +++ b/pkg/printer/table.go @@ -35,15 +35,20 @@ type Table struct { RawHeaders []string Entries [][]any - rows [][]string // representation used for printing - processed bool - lenHeaders []int - alignInts bool - maxwidth int + rows [][]string // representation used for printing + processed bool + lenHeaders []int + alignInts bool + maxwidth int + debugGoRoutines bool } func NewTable(conf *cfg.Config, columns, rows int) *Table { - table := Table{Mode: conf.Output, maxwidth: cfg.GetTermWidth()} + table := new(Table{ + Mode: conf.Output, + maxwidth: cfg.GetTermWidth(), + debugGoRoutines: conf.DebugGoRoutines, + }) table.Headers = make([]string, columns) table.RawHeaders = make([]string, columns) @@ -51,14 +56,18 @@ func NewTable(conf *cfg.Config, columns, rows int) *Table { table.lenHeaders = make([]int, columns) table.alignInts = conf.AlignInts - return &table + return table } func NewTableEmpty(conf *cfg.Config) *Table { - table := Table{Mode: conf.Output, maxwidth: cfg.GetTermWidth()} + table := new(Table{ + Mode: conf.Output, + maxwidth: cfg.GetTermWidth(), + debugGoRoutines: conf.DebugGoRoutines, + }) table.alignInts = conf.AlignInts - return &table + return table } func (table *Table) WithHeaders(headers ...string) *Table { @@ -75,16 +84,24 @@ func (table *Table) WithHeaders(headers ...string) *Table { } func (table *Table) Print() error { + var err error + switch table.Mode { case "json": - return table.PrintJSON() + err = table.PrintJSON() case "yaml": - return table.PrintYAML() + err = table.PrintYAML() case "csv": - return table.PrintCSV() + err = table.PrintCSV() default: - return table.PrintTSV() + err = table.PrintTSV() } + + if table.debugGoRoutines { + printGoRoutineMetrics() + } + + return err } var ( @@ -149,9 +166,11 @@ func (table *Table) PrintTSV() error { wrapped := wrapper(entry) // and indent it - for idx, line := range strings.Split(wrapped, "\n") { - if idx == 0 { + first := true + for line := range strings.Lines(wrapped) { + if first { entry = line + first = false } else { entry += "\n " + strings.Repeat(" ", currentWidth) + line }