diff --git a/cmd/ilm_forecast.go b/cmd/ilm_forecast.go index 1a89a8d..a944d04 100644 --- a/cmd/ilm_forecast.go +++ b/cmd/ilm_forecast.go @@ -39,11 +39,28 @@ func IlmForecast(conf *cfg.Config) *cli.Command { } } +const IlmForeCastListReference = ` must be a string which consists one or more of these elements: +d - days +h - hours +m - minutes +s - seconds + +Examples: + +58d12h - 58 days and 12 hours +122h30m - 122 hours and 30 minutes + +Specs may be mixed: + +8h12d - 12 days and 8 hours` + func IlmForecastList(conf *cfg.Config) *cli.Command { return &cli.Command{ - Name: "list", - Aliases: []string{"ls"}, - Usage: "list index rollover config", + Name: "list", + Aliases: []string{"ls"}, + Usage: "list index rollover config", + UsageText: "list [options] []", + CustomHelpTemplate: addReference(IlmForeCastListReference), Flags: []cli.Flag{ &cli.StringFlag{ @@ -54,10 +71,22 @@ func IlmForecastList(conf *cfg.Config) *cli.Command { }, &cli.StringFlag{ Name: "within", - Usage: "duration withing which to forecast", + Usage: " withing which to forecast", Destination: &conf.Ilm.Within, Aliases: []string{"w"}, }, + &cli.StringFlag{ + Name: "min-age", + Usage: "show only indices older than ", + Destination: &conf.Ilm.MinAge, + Aliases: []string{"m"}, + }, + &cli.BoolFlag{ + Name: "verbose", + Usage: "show ilm policy as well", + Destination: &conf.Verbose, + Aliases: []string{"v"}, + }, }, Action: func(ctx context.Context, cmd *cli.Command) error { @@ -66,7 +95,7 @@ func IlmForecastList(conf *cfg.Config) *cli.Command { return errors.New("invalid from phase, allowed: hot, warm, cold, frozen") } - return es.IlmForecastList(conf) + return es.IlmForecastList(conf, cmd.Args().Get(0)) }, } } diff --git a/pkg/cfg/ilm.go b/pkg/cfg/ilm.go index 1bdc079..3353d9a 100644 --- a/pkg/cfg/ilm.go +++ b/pkg/cfg/ilm.go @@ -40,7 +40,7 @@ type Ilm struct { DeleteMinAge string DeleteSearchableSnapshots bool - FromPhase, Within string // forecast + MinAge, FromPhase, Within string // forecast } func (cfg *Ilm) HaveHot() bool { diff --git a/pkg/es/duration.go b/pkg/es/duration.go new file mode 100644 index 0000000..5922fb6 --- /dev/null +++ b/pkg/es/duration.go @@ -0,0 +1,126 @@ +/* +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" + "math" + "regexp" + "strconv" + "time" +) + +var ( + matchDuration = regexp.MustCompile(`(\d+)([dhms])`) +) + +type esDuration struct { + days, hours, minutes, seconds int +} + +/* +We could use time.ParseDuration(), but this doesn't support days. + +We could also use github.com/xhit/go-str2duration/v2, which does +the job, but it's just another dependency, just for this little +gem. And we don't need a time.Time value. And int is good enough +for duration comparison. + +Convert a duration into an integer. Valid time units are "s", "m", "h" and "d". + + via https://codeberg.org/scip/tablizer/src/branch/main/lib/sort.go#L113 +*/ +func parseDuration(duration string) time.Duration { + + seconds := 0 + + for _, match := range matchDuration.FindAllStringSubmatch(duration, -1) { + if len(match) == 3 { + durationvalue, _ := strconv.Atoi(match[1]) + + switch match[2][0] { + case 'd': + seconds += durationvalue * 86400 + case 'h': + seconds += durationvalue * 3600 + case 'm': + seconds += durationvalue * 60 + case 's': + seconds += durationvalue + } + } + } + + return time.Duration(seconds) * time.Second +} + +// pretty much the same as above, but return a struct filled with int duration values +func extractDuration(duration string) esDuration { + esd := esDuration{} + + for _, match := range matchDuration.FindAllStringSubmatch(duration, -1) { + if len(match) == 3 { + durationvalue, _ := strconv.Atoi(match[1]) + + switch match[2][0] { + case 'd': + esd.days = durationvalue + case 'h': + esd.hours = durationvalue + case 'm': + esd.minutes = durationvalue + case 's': + esd.seconds = durationvalue + } + } + } + + return esd +} + +// convert a duration like 156h6m15.095s-6d/12h/6m => 6d:12h:6m +func formatDuration(val time.Duration) string { + esd := extractDuration(val.String()) + + daysF := float64(esd.hours) / 24.0 + days := math.Floor(daysF) + hoursLeft := (daysF - days) * 24 + + switch { + case val.Minutes() < 1: + return fmt.Sprintf("%ds", int64(esd.seconds)) + + case val.Hours() < 1: + return fmt.Sprintf("%dm", int64(esd.minutes)) + + case val.Hours() < 24: + if esd.minutes == 0 { + return fmt.Sprintf("%dh", esd.hours) + } + return fmt.Sprintf("%dh:%dm", esd.hours, int64(esd.minutes)) + + default: + if esd.minutes == 0 && hoursLeft == 0 { + return fmt.Sprintf("%dd", int64(days)) + } + return fmt.Sprintf("%dd:%dh:%dm", + int64(days), + int64(hoursLeft), + esd.minutes, + ) + } +} diff --git a/pkg/es/ilm_forecast.go b/pkg/es/ilm_forecast.go index 7d73686..a284e39 100644 --- a/pkg/es/ilm_forecast.go +++ b/pkg/es/ilm_forecast.go @@ -34,7 +34,6 @@ import ( ) var ( - matchDuration = regexp.MustCompile(`(\d+)([dhms])`) IlmPhaseOrder = []string{"hot", "warm", "cold", "frozen", "delete"} ) @@ -47,6 +46,7 @@ type NextPhase struct { type PhaseData struct { index string + policy string size int64 age time.Duration minage time.Duration @@ -86,30 +86,55 @@ types.Lifecycle{ hot for 7 days or 25Gig, then Rollover => warm 14 days => frozen 60 days => delete */ -func IlmForecastList(conf *cfg.Config) error { +func IlmForecastList(conf *cfg.Config, filter string) error { phaseData, err := getIlmPhaseData(conf) if err != nil { return err } - table := printer.NewTable(conf, 8, 0) - table.Addheaders("index", "current size", "current age", "virtual age", "min age", "min size", "current phase", "next phase") + flt := regexp.MustCompile(filter) + + minage := time.Duration(0) + if conf.Ilm.MinAge != "" { + minage = parseDuration(conf.Ilm.MinAge) + } + + headers := []string{"index", "current size", "current age", + "virtual age", "min age", "min size", "current phase", "next phase"} + if conf.Verbose { + headers = append(headers, "ilm policy") + } + + table := printer.NewTable(conf, len(headers), 0) + table.Addheaders(headers...) for _, phase := range phaseData { + if filter != "" && !flt.MatchString(phase.index) { + continue + } + + if conf.Ilm.MinAge != "" && phase.age < minage { + continue + } + virtualAge := virtualAge(&phase) - table.AddRow( + row := []string{ phase.index, humanize.Bytes(uint64(phase.size)), - phase.age.String(), - virtualAge.String(), - - phase.minage.String(), + formatDuration(phase.age), + formatDuration(virtualAge), + formatDuration(phase.minage), humanize.Bytes(uint64(phase.minsize)), - phase.currentPhase, phase.nextPhase, - ) + } + + if conf.Verbose { + row = append(row, phase.policy) + } + + table.AddRow(row...) } table.Sort() @@ -241,6 +266,7 @@ func getIlmPhaseData(conf *cfg.Config) ([]PhaseData, error) { list = append(list, PhaseData{ index: indexName, + policy: *explain.Policy, size: size, age: age, minage: nextPhase.minage, @@ -365,39 +391,3 @@ func findNextPhase(policy types.IlmPolicy, currentPhase string) *NextPhase { return nil } - -/* -We could use time.ParseDuration(), but this doesn't support days. - -We could also use github.com/xhit/go-str2duration/v2, which does -the job, but it's just another dependency, just for this little -gem. And we don't need a time.Time value. And int is good enough -for duration comparison. - -Convert a duration into an integer. Valid time units are "s", "m", "h" and "d". - - via https://codeberg.org/scip/tablizer/src/branch/main/lib/sort.go#L113 -*/ -func parseDuration(duration string) time.Duration { - - seconds := 0 - - for _, match := range matchDuration.FindAllStringSubmatch(duration, -1) { - if len(match) == 3 { - durationvalue, _ := strconv.Atoi(match[1]) - - switch match[2][0] { - case 'd': - seconds += durationvalue * 86400 - case 'h': - seconds += durationvalue * 3600 - case 'm': - seconds += durationvalue * 60 - case 's': - seconds += durationvalue - } - } - } - - return time.Duration(seconds) * time.Second -}