diff --git a/cmd/health.go b/cmd/health.go index a82dc7d..6d13025 100644 --- a/cmd/health.go +++ b/cmd/health.go @@ -26,10 +26,11 @@ import ( "github.com/urfave/cli/v3" ) -func Health(conf *cfg.Config) *cli.Command { +func Status(conf *cfg.Config) *cli.Command { return &cli.Command{ - Name: "health", - Usage: "show ES health", + Name: "status", + Usage: "show ES status", + Aliases: []string{"s"}, Action: func(ctx context.Context, cmd *cli.Command) error { if err := es.Health(conf); err != nil { return err diff --git a/cmd/index.go b/cmd/index.go new file mode 100644 index 0000000..e5a83ad --- /dev/null +++ b/cmd/index.go @@ -0,0 +1,63 @@ +/* +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 . +*/ +package cmd + +import ( + "context" + + "codeberg.org/scip/esctl/pkg/cfg" + "codeberg.org/scip/esctl/pkg/es" + + "github.com/urfave/cli/v3" +) + +func Index(conf *cfg.Config) *cli.Command { + return &cli.Command{ + Name: "index", + Aliases: []string{"i"}, + Usage: "manage indicies", + + Commands: []*cli.Command{ + IndexList(conf), + }, + } +} + +func IndexList(conf *cfg.Config) *cli.Command { + return &cli.Command{ + Name: "list", + Aliases: []string{"ls"}, + Usage: "list indicies", + + Flags: []cli.Flag{ + &cli.IntFlag{ + Name: "max", + Usage: "max number of items to process", + Destination: &conf.MaxItems, + Aliases: []string{"m"}, + }, + }, + + Action: func(ctx context.Context, cmd *cli.Command) error { + if err := es.IndexList(conf); err != nil { + return err + } + + return nil + }, + } +} diff --git a/cmd/root.go b/cmd/root.go index f66f6c9..04b9f52 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -60,8 +60,9 @@ func Main() int { }, Commands: []*cli.Command{ - Health(conf), + Status(conf), Search(conf), + Index(conf), }, Before: func(ctx context.Context, cmd *cli.Command) (context.Context, error) { diff --git a/cmd/search.go b/cmd/search.go index 0bfc3d2..76b8453 100644 --- a/cmd/search.go +++ b/cmd/search.go @@ -38,6 +38,7 @@ func Search(conf *cfg.Config) *cli.Command { Usage: "index to search within", Sources: cli.EnvVars("ES_INDEX"), Destination: &conf.Index, + Aliases: []string{"i"}, }, &cli.IntFlag{ Name: "from", diff --git a/go.mod b/go.mod index d1f0f75..55f7cbd 100644 --- a/go.mod +++ b/go.mod @@ -1,37 +1,45 @@ // 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 . module codeberg.org/scip/esctl -go 1.24 +go 1.25.0 require ( github.com/alecthomas/repr v0.5.2 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/clipperhouse/displaywidth v0.10.0 // indirect + github.com/clipperhouse/uax29/v2 v2.6.0 // indirect github.com/elastic/elastic-transport-go/v8 v8.11.0 // indirect github.com/elastic/go-elasticsearch/v9 v9.3.2 // indirect - github.com/fatih/color v1.16.0 // indirect + github.com/fatih/color v1.19.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/lmittmann/tint v1.1.3 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.21 // indirect + github.com/mattn/go-runewidth v0.0.19 // indirect + github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect + github.com/olekukonko/errors v1.2.0 // indirect + github.com/olekukonko/ll v0.1.6 // indirect + github.com/olekukonko/tablewriter v1.1.4 // indirect github.com/tlinden/yadu v0.1.3 // indirect github.com/urfave/cli/v3 v3.8.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/otel v1.35.0 // indirect go.opentelemetry.io/otel/metric v1.35.0 // indirect go.opentelemetry.io/otel/trace v1.35.0 // indirect - golang.org/x/sys v0.28.0 // indirect + golang.org/x/sys v0.42.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 2306ef2..d4e1dd4 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,11 @@ github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs= github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/clipperhouse/displaywidth v0.10.0 h1:GhBG8WuerxjFQQYeuZAeVTuyxuX+UraiZGD4HJQ3Y8g= +github.com/clipperhouse/displaywidth v0.10.0/go.mod h1:XqJajYsaiEwkxOj4bowCTMcT1SgvHo9flfF3jQasdbs= +github.com/clipperhouse/uax29/v2 v2.6.0 h1:z0cDbUV+aPASdFb2/ndFnS9ts/WNXgTNNGFoKXuhpos= +github.com/clipperhouse/uax29/v2 v2.6.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= github.com/elastic/elastic-transport-go/v8 v8.9.0 h1:KeT/2P54F0xS0S8Y3Pf+tFDg4HmBgReQMB+BMz8dDAs= github.com/elastic/elastic-transport-go/v8 v8.9.0/go.mod h1:ssMTvNS2hwf7CaiGsRRsx4gQHFZ/jS/DkLcISxekWzc= github.com/elastic/elastic-transport-go/v8 v8.11.0 h1:taYmqC2M6+fZt/+W+ENYh/W5L9+KrlJGOSbEJs8egWc= @@ -8,6 +14,10 @@ github.com/elastic/go-elasticsearch/v9 v9.3.2 h1:nvtvfN/Gsp/rzUPz/9yILwDAsYJ3s5L github.com/elastic/go-elasticsearch/v9 v9.3.2/go.mod h1:ubKUMJCJbX5V/gW5MIn2NQZyaEZ61ubXwJmD5UMNrM8= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= +github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -17,9 +27,21 @@ github.com/lmittmann/tint v1.1.3 h1:Hv4EaHWXQr+GTFnOU4VKf8UvAtZgn0VuKT+G0wFlO3I= github.com/lmittmann/tint v1.1.3/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs= github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= +github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= +github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc= +github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0= +github.com/olekukonko/errors v1.2.0 h1:10Zcn4GeV59t/EGqJc8fUjtFT/FuUh5bTMzZ1XwmCRo= +github.com/olekukonko/errors v1.2.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= +github.com/olekukonko/ll v0.1.6 h1:lGVTHO+Qc4Qm+fce/2h2m5y9LvqaW+DCN7xW9hsU3uA= +github.com/olekukonko/ll v0.1.6/go.mod h1:NVUmjBb/aCtUpjKk75BhWrOlARz3dqsM+OtszpY4o88= +github.com/olekukonko/tablewriter v1.1.4 h1:ORUMI3dXbMnRlRggJX3+q7OzQFDdvgbN9nVWj1drm6I= +github.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY= github.com/tlinden/yadu v0.1.3 h1:5cRCUmj+l5yvlM2irtpFBIJwVV2DPEgYSaWvF19FtcY= github.com/tlinden/yadu v0.1.3/go.mod h1:l3bRmHKL9zGAR6pnBHY2HRPxBecf7L74BoBgOOpTcUA= github.com/urfave/cli/v3 v3.8.0 h1:XqKPrm0q4P0q5JpoclYoCAv0/MIvH/jZ2umzuf8pNTI= @@ -35,6 +57,10 @@ go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/cfg/config.go b/pkg/cfg/config.go index e21a6ba..19fac7a 100644 --- a/pkg/cfg/config.go +++ b/pkg/cfg/config.go @@ -31,12 +31,12 @@ const ( ) type Config struct { - Uri, User, Pass string - ES *elasticsearch.TypedClient - Debug bool - From, To int - Index string - Filter []string + Uri, User, Pass string + ES *elasticsearch.TypedClient + Debug bool + From, To, MaxItems int + Index string + Filter []string } func Init() (*Config, error) { diff --git a/pkg/es/aux.go b/pkg/es/aux.go index 05fe721..cb4e16c 100644 --- a/pkg/es/aux.go +++ b/pkg/es/aux.go @@ -18,6 +18,7 @@ package es import ( "context" + "fmt" "log" "log/slog" @@ -30,7 +31,20 @@ func Health(conf *cfg.Config) error { log.Fatalf("Error getting health: %s", err) } - slog.Info("ES result", "cluster health", res) + slog.Debug("ES result", "cluster health", res) + table := NewTable(5) + + table.headers = []string{bold("SETTING"), bold("STATUS")} + + table.entries = [][]string{ + {"Cluster Name", Colorize(*&res.Status.Name, res.ClusterName)}, + {"Active Shards", fmt.Sprintf("%d", res.ActiveShards)}, + {"Active Primary Shards", fmt.Sprintf("%d", res.ActivePrimaryShards)}, + {"Indicies", fmt.Sprintf("%d", len(res.Indices))}, + {"Nodes", fmt.Sprintf("%d", res.NumberOfNodes)}, + } + + table.PrintMarkdown() return nil } diff --git a/pkg/es/color.go b/pkg/es/color.go new file mode 100644 index 0000000..930b208 --- /dev/null +++ b/pkg/es/color.go @@ -0,0 +1,40 @@ +/* +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 . +*/ + +package es + +import "github.com/fatih/color" + +var ( + green = color.New(color.BgGreen, color.FgHiWhite).SprintFunc() + orange = color.BgRGB(255, 128, 0).AddRGB(0, 0, 0).SprintFunc() + red = color.New(color.BgRed, color.FgHiWhite).SprintFunc() + bold = color.New(color.Bold).SprintFunc() +) + +func Colorize(col, what string) string { + switch col { + case "green": + what = green(what) + case "orange": + what = orange(what) + case "red": + what = red(what) + } + + return what +} diff --git a/pkg/es/index.go b/pkg/es/index.go new file mode 100644 index 0000000..4f64b22 --- /dev/null +++ b/pkg/es/index.go @@ -0,0 +1,61 @@ +/* +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 . +*/ +package es + +import ( + "context" + "log" + "log/slog" + + "codeberg.org/scip/esctl/pkg/cfg" +) + +func IndexList(conf *cfg.Config) error { + res, err := conf.ES.Cat.Indices().Do(context.Background()) + if err != nil { + log.Fatalf("Error getting indicies: %s", err) + } + + slog.Debug("ES result", "indicies", res) + + size := len(res) + + if conf.MaxItems > 0 { + if size > conf.MaxItems { + size = conf.MaxItems + } + } + + table := NewTable(size) + + table.headers = []string{bold("NAME"), bold("SIZE"), bold("DOCSCOUNT")} + + for idx, index := range res { + name := Colorize(*index.Health, *index.Index) + + table.entries[idx] = []string{name, *index.DatasetSize, *index.DocsCount} + + if idx == size-1 { + break + } + } + + table.Sort() + table.PrintMarkdown() + + return nil +} diff --git a/pkg/es/search.go b/pkg/es/search.go index fe7b76b..e085a92 100644 --- a/pkg/es/search.go +++ b/pkg/es/search.go @@ -19,6 +19,7 @@ package es import ( "context" "fmt" + "log/slog" "strings" "codeberg.org/scip/esctl/pkg/cfg" @@ -64,6 +65,8 @@ func Search(conf *cfg.Config, q string) error { return fmt.Errorf("Error running search (esdsl): %s", err) } + slog.Debug("ES result", "search", res) + for _, hit := range res.Hits.Hits { fmt.Printf("%s\n", hit.Source_) } diff --git a/pkg/es/table.go b/pkg/es/table.go new file mode 100644 index 0000000..28fe3a5 --- /dev/null +++ b/pkg/es/table.go @@ -0,0 +1,108 @@ +/* +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 . +*/ +package es + +import ( + "fmt" + "sort" + "strings" + + "github.com/olekukonko/tablewriter" + "github.com/olekukonko/tablewriter/renderer" + "github.com/olekukonko/tablewriter/tw" +) + +type Table struct { + headers []string + entries [][]string +} + +func NewTable(size int) *Table { + table := Table{} + + table.headers = make([]string, size) + table.entries = make([][]string, size) + + return &table +} + +func (data *Table) PrintMarkdown() error { + tableString := &strings.Builder{} + + table := tablewriter.NewTable(tableString, + tablewriter.WithRenderer( + renderer.NewBlueprint( + tw.Rendition{ + Borders: tw.Border{ + Left: tw.On, + Right: tw.On, + Top: tw.Off, + Bottom: tw.Off, + }, + Settings: tw.Settings{ + Separators: tw.Separators{ + ShowHeader: tw.On, + ShowFooter: tw.Off, + BetweenRows: tw.Off, + BetweenColumns: 0, + }, + }, + Symbols: tw.NewSymbols(tw.StyleMarkdown), + })), + + tablewriter.WithConfig( + tablewriter.Config{ + Header: tw.CellConfig{ + Formatting: tw.CellFormatting{ + Alignment: tw.AlignLeft, + AutoFormat: tw.Off, + }, + }, + Row: tw.CellConfig{ + Formatting: tw.CellFormatting{ + Alignment: tw.AlignLeft, + }, + }, + }, + ), + ) + + table.Header(data.headers) + + if err := table.Bulk(data.entries); err != nil { + return fmt.Errorf("Failed to add data to table renderer: %s", err) + } + + if err := table.Render(); err != nil { + fmt.Errorf("Failed to render table: %s", err) + } + + fmt.Println(tableString.String()) + + return nil +} + +func (data *Table) Sort() { + // sanity checks + if len(data.entries) == 0 { + return + } + + sort.Slice(data.entries, func(i, j int) bool { + return data.entries[i][0] < data.entries[j][0] + }) +}