package cli import ( "fmt" "strings" "time" "git.unkin.net/unkin/logarchiver/internal/index" "github.com/spf13/cobra" ) // selectFlags are the shared object-selection flags for search and fetch. type selectFlags struct { subject string host string from string to string limit int } func (s *selectFlags) bind(cmd *cobra.Command) { f := cmd.Flags() f.StringVar(&s.subject, "subject", "", "NATS-style subject glob (e.g. 'logs.vm.*' or 'logs.k8s.vault.>')") f.StringVar(&s.host, "host", "", "source host to match (exact, or a glob with '*')") f.StringVar(&s.from, "from", "", "start of time window (RFC3339, 'YYYY-MM-DD', or relative like '-24h')") f.StringVar(&s.to, "to", "", "end of time window (RFC3339, 'YYYY-MM-DD', or relative like '-1h')") f.IntVar(&s.limit, "limit", 100, "max objects to return (0 = no limit)") } // query builds an index.SearchQuery from the flags. func (s *selectFlags) query(now time.Time) (index.SearchQuery, error) { q := index.SearchQuery{Subject: s.subject, Host: s.host, Limit: s.limit} if s.from != "" { t, err := parseTimeArg(s.from, now) if err != nil { return q, fmt.Errorf("--from: %w", err) } q.From = t } if s.to != "" { t, err := parseTimeArg(s.to, now) if err != nil { return q, fmt.Errorf("--to: %w", err) } q.To = t } if !q.From.IsZero() && !q.To.IsZero() && q.To.Before(q.From) { return q, fmt.Errorf("--to (%s) is before --from (%s)", q.To, q.From) } return q, nil } // parseTimeArg accepts RFC3339[/Nano], "YYYY-MM-DD", "YYYY-MM-DDTHH:MM:SS", or a // signed Go duration relative to now (e.g. "-24h", "30m"). func parseTimeArg(s string, now time.Time) (time.Time, error) { s = strings.TrimSpace(s) for _, layout := range []string{time.RFC3339Nano, time.RFC3339, "2006-01-02T15:04:05", "2006-01-02"} { if t, err := time.Parse(layout, s); err == nil { return t.UTC(), nil } } if d, err := time.ParseDuration(s); err == nil { return now.Add(d).UTC(), nil } return time.Time{}, fmt.Errorf("unrecognized time %q (use RFC3339, YYYY-MM-DD, or a duration like -24h)", s) } func newIndexClient(cmd *cobra.Command) (index.Index, error) { cfg, err := loadConfig() if err != nil { return nil, err } if !cfg.Index.Enabled { return nil, fmt.Errorf("index is disabled in config; search/fetch-by-query require the ClickHouse index") } return index.NewClickHouse(cmd.Context(), indexConfig(cfg.Index)) } func humanBytes(n uint64) string { const unit = 1024 if n < unit { return fmt.Sprintf("%dB", n) } div, exp := int64(unit), 0 for x := n / unit; x >= unit; x /= unit { div *= unit exp++ } return fmt.Sprintf("%.1f%ciB", float64(n)/float64(div), "KMGTPE"[exp]) }