diff --git a/README.md b/README.md index 1b812011..4281a438 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ See [docs/COMMANDS.md](docs/COMMANDS.md) for detailed command reference. |------------|--------|--------------|-------| | Metrics | ✅ | `metrics search`, `metrics query`, `metrics list`, `metrics get` | V1 and V2 APIs supported | | Logs | ✅ | `logs search`, `logs list`, `logs aggregate` | V1 and V2 APIs supported | -| Traces | ✅ | `traces search`, `traces list`, `traces aggregate` | APM traces support | +| Traces | ❌ | - | Not yet implemented (use APM commands for trace data) | | Events | ✅ | `events list`, `events search`, `events get` | Infrastructure event management | | RUM | ✅ | `rum apps`, `rum sessions`, `rum metrics list/get`, `rum retention-filters list/get` | Apps, sessions, metrics, retention filters (create/update pending) | | APM Services | ✅ | `apm services`, `apm entities`, `apm dependencies`, `apm flow-map` | Services stats, operations, resources; entity queries; dependencies; flow visualization | diff --git a/cmd/apm.go b/cmd/apm.go index c316f807..8bd46b6b 100644 --- a/cmd/apm.go +++ b/cmd/apm.go @@ -428,7 +428,10 @@ var ( func init() { // Services list flags - apmServicesListCmd.Flags().StringVar(&envFilter, "env", "", "Environment filter") + apmServicesListCmd.Flags().StringVar(&envFilter, "env", "", "Environment filter (required)") + if err := apmServicesListCmd.MarkFlagRequired("env"); err != nil { + panic(fmt.Errorf("failed to mark flag as required: %w", err)) + } apmServicesListCmd.Flags().Int64Var(&startTime, "start", time.Now().Add(-1*time.Hour).Unix(), "Start time (Unix timestamp)") apmServicesListCmd.Flags().Int64Var(&endTime, "end", time.Now().Unix(), "End time (Unix timestamp)") @@ -544,24 +547,37 @@ func runAPMServicesList(cmd *cobra.Command, args []string) error { return err } + // Validate that env filter is provided + if envFilter == "" { + return fmt.Errorf("--env flag is required (e.g., --env prod)") + } + + // Set default time range if not provided (shared variables may be 0) + if startTime == 0 { + startTime = time.Now().Add(-1 * time.Hour).Unix() + } + if endTime == 0 { + endTime = time.Now().Unix() + } + // Build query parameters params := url.Values{} params.Add("start", strconv.FormatInt(startTime, 10)) params.Add("end", strconv.FormatInt(endTime, 10)) - if envFilter != "" { - params.Add("env", envFilter) - } + params.Add("filter[env]", envFilter) path := fmt.Sprintf("/api/v2/apm/services?%s", params.Encode()) resp, err := client.RawRequest("GET", path, nil) if err != nil { - return fmt.Errorf("failed to list APM services: %w", err) + return fmt.Errorf("failed to list APM services: %w\n\nRequest: GET %s\nParameters: start=%d, end=%d, filter[env]=%s", + err, path, startTime, endTime, envFilter) } defer func() { _ = resp.Body.Close() }() result, err := readRawResponse(resp) if err != nil { - return fmt.Errorf("failed to list APM services: %w", err) + return fmt.Errorf("failed to list APM services: %w\n\nRequest: GET %s\nParameters: start=%d, end=%d, filter[env]=%s", + err, path, startTime, endTime, envFilter) } output, err := formatter.FormatOutput(result, formatter.OutputFormat(outputFormat)) diff --git a/cmd/logs_simple.go b/cmd/logs_simple.go index e7d02f98..edddcec5 100644 --- a/cmd/logs_simple.go +++ b/cmd/logs_simple.go @@ -14,6 +14,7 @@ import ( "github.com/DataDog/datadog-api-client-go/v2/api/datadogV2" "github.com/DataDog/pup/pkg/formatter" + "github.com/DataDog/pup/pkg/util" "github.com/spf13/cobra" ) @@ -53,8 +54,12 @@ LOG QUERY SYNTAX: TIME RANGES: Supported time formats: - • Relative: 1h, 30m, 7d, 1w (hour, minute, day, week) + • Relative short: 1h, 30m, 7d, 5s, 1w + • Relative long: 5min, 5minutes, 2hr, 2hours, 3days, 1week + • With spaces: "5 minutes", "2 hours" + • With minus: -5m, -2h (treated same as 5m, 2h) • Absolute: Unix timestamp in milliseconds + • RFC3339: 2024-01-01T00:00:00Z • now: Current time EXAMPLES: @@ -556,8 +561,8 @@ var ( func init() { // Search command flags (v1) logsSearchCmd.Flags().StringVar(&logsQuery, "query", "", "Search query (required)") - logsSearchCmd.Flags().StringVar(&logsFrom, "from", "", "Start time: 1h, 30m, 7d, or timestamp (required)") - logsSearchCmd.Flags().StringVar(&logsTo, "to", "now", "End time: 1h, 30m, now, or timestamp") + logsSearchCmd.Flags().StringVar(&logsFrom, "from", "1h", "Start time: 1h, 5min, 2hours, '5 minutes', RFC3339, Unix timestamp, or 'now'") + logsSearchCmd.Flags().StringVar(&logsTo, "to", "now", "End time: 1h, 5min, 2hours, '5 minutes', RFC3339, Unix timestamp, or 'now'") logsSearchCmd.Flags().IntVar(&logsLimit, "limit", 50, "Maximum number of logs (1-1000)") logsSearchCmd.Flags().StringVar(&logsSort, "sort", "desc", "Sort order: asc or desc") logsSearchCmd.Flags().StringVar(&logsIndex, "index", "", "Comma-separated log indexes") @@ -565,24 +570,18 @@ func init() { if err := logsSearchCmd.MarkFlagRequired("query"); err != nil { panic(fmt.Errorf("failed to mark flag as required: %w", err)) } - if err := logsSearchCmd.MarkFlagRequired("from"); err != nil { - panic(fmt.Errorf("failed to mark flag as required: %w", err)) - } // List command flags (v2) logsListCmd.Flags().StringVar(&logsQuery, "query", "*", "Search query") - logsListCmd.Flags().StringVar(&logsFrom, "from", "", "Start time (required)") + logsListCmd.Flags().StringVar(&logsFrom, "from", "1h", "Start time: 1h, 5min, 2hours, '5 minutes', RFC3339, Unix timestamp, or 'now'") logsListCmd.Flags().StringVar(&logsTo, "to", "now", "End time") logsListCmd.Flags().IntVar(&logsLimit, "limit", 10, "Number of logs") logsListCmd.Flags().StringVar(&logsSort, "sort", "-timestamp", "Sort order") logsListCmd.Flags().StringVar(&logsStorage, "storage", "", "Storage tier: indexes, online-archives, or flex") - if err := logsListCmd.MarkFlagRequired("from"); err != nil { - panic(fmt.Errorf("failed to mark flag as required: %w", err)) - } // Query command flags (v2) logsQueryCmd.Flags().StringVar(&logsQuery, "query", "", "Log query (required)") - logsQueryCmd.Flags().StringVar(&logsFrom, "from", "", "Start time (required)") + logsQueryCmd.Flags().StringVar(&logsFrom, "from", "1h", "Start time: 1h, 5min, 2hours, '5 minutes', RFC3339, Unix timestamp, or 'now'") logsQueryCmd.Flags().StringVar(&logsTo, "to", "now", "End time") logsQueryCmd.Flags().IntVar(&logsLimit, "limit", 50, "Maximum results") logsQueryCmd.Flags().StringVar(&logsSort, "sort", "-timestamp", "Sort order") @@ -591,13 +590,10 @@ func init() { if err := logsQueryCmd.MarkFlagRequired("query"); err != nil { panic(fmt.Errorf("failed to mark flag as required: %w", err)) } - if err := logsQueryCmd.MarkFlagRequired("from"); err != nil { - panic(fmt.Errorf("failed to mark flag as required: %w", err)) - } // Aggregate command flags (v2) logsAggregateCmd.Flags().StringVar(&logsQuery, "query", "", "Log query (required)") - logsAggregateCmd.Flags().StringVar(&logsFrom, "from", "", "Start time (required)") + logsAggregateCmd.Flags().StringVar(&logsFrom, "from", "1h", "Start time: 1h, 5min, 2hours, '5 minutes', RFC3339, Unix timestamp, or 'now'") logsAggregateCmd.Flags().StringVar(&logsTo, "to", "now", "End time") logsAggregateCmd.Flags().StringVar(&logsCompute, "compute", "count", "Metric to compute") logsAggregateCmd.Flags().StringVar(&logsGroupBy, "group-by", "", "Field to group by") @@ -606,9 +602,6 @@ func init() { if err := logsAggregateCmd.MarkFlagRequired("query"); err != nil { panic(fmt.Errorf("failed to mark flag as required: %w", err)) } - if err := logsAggregateCmd.MarkFlagRequired("from"); err != nil { - panic(fmt.Errorf("failed to mark flag as required: %w", err)) - } // Add subcommands logsCmd.AddCommand(logsSearchCmd) @@ -641,47 +634,6 @@ func init() { // Helper functions -// parseTimeString converts relative or absolute time to Unix timestamp in milliseconds (UTC) -func parseTimeString(timeStr string) (int64, error) { - if timeStr == "now" { - return time.Now().UTC().UnixMilli(), nil - } - - // Try parsing as relative time (1h, 30m, 7d) - if len(timeStr) >= 2 { - unit := timeStr[len(timeStr)-1:] - valueStr := timeStr[:len(timeStr)-1] - - var value int64 - if _, err := fmt.Sscanf(valueStr, "%d", &value); err == nil { - var duration time.Duration - switch unit { - case "s": - duration = time.Duration(value) * time.Second - case "m": - duration = time.Duration(value) * time.Minute - case "h": - duration = time.Duration(value) * time.Hour - case "d": - duration = time.Duration(value) * 24 * time.Hour - case "w": - duration = time.Duration(value) * 7 * 24 * time.Hour - default: - return 0, fmt.Errorf("invalid time unit: %s (use s, m, h, d, or w)", unit) - } - return time.Now().UTC().Add(-duration).UnixMilli(), nil - } - } - - // Try parsing as Unix timestamp (milliseconds) - var timestamp int64 - if _, err := fmt.Sscanf(timeStr, "%d", ×tamp); err == nil { - return timestamp, nil - } - - return 0, fmt.Errorf("invalid time format: %s (use relative like '1h' or Unix timestamp)", timeStr) -} - // validateAndConvertStorageTier validates the storage tier string and converts it to LogsStorageTier // Returns nil if storage is empty (which means search all tiers) func validateAndConvertStorageTier(storage string) (*datadogV2.LogsStorageTier, error) { @@ -782,12 +734,12 @@ func runLogsSearch(cmd *cobra.Command, args []string) error { return err } - fromTime, err := parseTimeString(logsFrom) + fromTime, err := util.ParseTimeToUnixMilli(logsFrom) if err != nil { return fmt.Errorf("invalid --from time: %w", err) } - toTime, err := parseTimeString(logsTo) + toTime, err := util.ParseTimeToUnixMilli(logsTo) if err != nil { return fmt.Errorf("invalid --to time: %w", err) } @@ -930,12 +882,12 @@ func runLogsList(cmd *cobra.Command, args []string) error { return err } - fromTime, err := parseTimeString(logsFrom) + fromTime, err := util.ParseTimeToUnixMilli(logsFrom) if err != nil { return fmt.Errorf("invalid --from time: %w", err) } - toTime, err := parseTimeString(logsTo) + toTime, err := util.ParseTimeToUnixMilli(logsTo) if err != nil { return fmt.Errorf("invalid --to time: %w", err) } @@ -1000,12 +952,12 @@ func runLogsQuery(cmd *cobra.Command, args []string) error { return err } - fromTime, err := parseTimeString(logsFrom) + fromTime, err := util.ParseTimeToUnixMilli(logsFrom) if err != nil { return fmt.Errorf("invalid --from time: %w", err) } - toTime, err := parseTimeString(logsTo) + toTime, err := util.ParseTimeToUnixMilli(logsTo) if err != nil { return fmt.Errorf("invalid --to time: %w", err) } @@ -1072,12 +1024,12 @@ func runLogsAggregate(cmd *cobra.Command, args []string) error { return err } - fromTime, err := parseTimeString(logsFrom) + fromTime, err := util.ParseTimeToUnixMilli(logsFrom) if err != nil { return fmt.Errorf("invalid --from time: %w", err) } - toTime, err := parseTimeString(logsTo) + toTime, err := util.ParseTimeToUnixMilli(logsTo) if err != nil { return fmt.Errorf("invalid --to time: %w", err) } diff --git a/cmd/logs_simple_test.go b/cmd/logs_simple_test.go index e95fa3e2..a7afa597 100644 --- a/cmd/logs_simple_test.go +++ b/cmd/logs_simple_test.go @@ -14,6 +14,7 @@ import ( "github.com/DataDog/pup/pkg/client" "github.com/DataDog/pup/pkg/config" + "github.com/DataDog/pup/pkg/util" ) func TestLogsCmd(t *testing.T) { @@ -58,6 +59,15 @@ func TestParseTimeString(t *testing.T) { return ts > 1000000000000 && ts < 9999999999999 }, }, + { + name: "RFC3339 timestamp", + input: "2024-01-01T00:00:00Z", + check: func(ts int64) bool { + // Should be Jan 1, 2024 in milliseconds + // 2024-01-01T00:00:00Z = 1704067200000ms + return ts == 1704067200000 + }, + }, { name: "invalid format", input: "invalid", @@ -67,15 +77,15 @@ func TestParseTimeString(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := parseTimeString(tt.input) + got, err := util.ParseTimeToUnixMilli(tt.input) if (err != nil) != tt.wantErr { - t.Errorf("parseTimeString() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("util.ParseTimeToUnixMilli() error = %v, wantErr %v", err, tt.wantErr) return } if !tt.wantErr && tt.check != nil && !tt.check(got) { - t.Errorf("parseTimeString() = %d, validation failed", got) + t.Errorf("util.ParseTimeToUnixMilli() = %d, validation failed", got) } }) } diff --git a/cmd/metrics.go b/cmd/metrics.go index f7622223..3e5eeb8f 100644 --- a/cmd/metrics.go +++ b/cmd/metrics.go @@ -16,6 +16,7 @@ import ( "github.com/DataDog/datadog-api-client-go/v2/api/datadogV1" "github.com/DataDog/datadog-api-client-go/v2/api/datadogV2" "github.com/DataDog/pup/pkg/formatter" + "github.com/DataDog/pup/pkg/util" "github.com/spf13/cobra" ) @@ -523,12 +524,12 @@ func runMetricsQuery(cmd *cobra.Command, args []string) error { } // Parse time ranges - from, err := parseTimeParam(fromTime) + from, err := util.ParseTimeParam(fromTime) if err != nil { return fmt.Errorf("invalid --from time: %w", err) } - to, err := parseTimeParam(toTime) + to, err := util.ParseTimeParam(toTime) if err != nil { return fmt.Errorf("invalid --to time: %w", err) } @@ -589,12 +590,12 @@ func runMetricsSearch(cmd *cobra.Command, args []string) error { } // Parse time ranges - from, err := parseTimeParam(fromTime) + from, err := util.ParseTimeParam(fromTime) if err != nil { return fmt.Errorf("invalid --from time: %w", err) } - to, err := parseTimeParam(toTime) + to, err := util.ParseTimeParam(toTime) if err != nil { return fmt.Errorf("invalid --to time: %w", err) } @@ -859,48 +860,3 @@ func runMetricsTagsList(cmd *cobra.Command, args []string) error { return fmt.Errorf("listing tags by metric name is not supported in the current API client version") } -// parseTimeParam parses a time parameter (relative or absolute) -func parseTimeParam(timeStr string) (time.Time, error) { - // Handle "now" keyword - if strings.ToLower(timeStr) == "now" { - return time.Now(), nil - } - - // Try parsing as unix timestamp - if timestamp, err := strconv.ParseInt(timeStr, 10, 64); err == nil { - return time.Unix(timestamp, 0), nil - } - - // Parse relative time (e.g., 1h, 30m, 7d, 1w) - if len(timeStr) < 2 { - return time.Time{}, fmt.Errorf("invalid time format: %s", timeStr) - } - - valueStr := timeStr[:len(timeStr)-1] - unit := timeStr[len(timeStr)-1:] - - value, err := strconv.ParseInt(valueStr, 10, 64) - if err != nil { - return time.Time{}, fmt.Errorf("invalid time value: %s", timeStr) - } - - now := time.Now() - var duration time.Duration - - switch strings.ToLower(unit) { - case "s": - duration = time.Duration(value) * time.Second - case "m": - duration = time.Duration(value) * time.Minute - case "h": - duration = time.Duration(value) * time.Hour - case "d": - duration = time.Duration(value) * 24 * time.Hour - case "w": - duration = time.Duration(value) * 7 * 24 * time.Hour - default: - return time.Time{}, fmt.Errorf("invalid time unit: %s (use s, m, h, d, or w)", unit) - } - - return now.Add(-duration), nil -} diff --git a/cmd/metrics_test.go b/cmd/metrics_test.go index bfd002e6..0046a39b 100644 --- a/cmd/metrics_test.go +++ b/cmd/metrics_test.go @@ -15,6 +15,7 @@ import ( "github.com/DataDog/pup/pkg/client" "github.com/DataDog/pup/pkg/config" + "github.com/DataDog/pup/pkg/util" ) func TestMetricsCmd(t *testing.T) { @@ -529,16 +530,16 @@ func TestParseTimeParam(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result, err := parseTimeParam(tt.timeStr) + result, err := util.ParseTimeParam(tt.timeStr) if (err != nil) != tt.wantErr { - t.Errorf("parseTimeParam(%q) error = %v, wantErr %v", tt.timeStr, err, tt.wantErr) + t.Errorf("util.ParseTimeParam(%q) error = %v, wantErr %v", tt.timeStr, err, tt.wantErr) } // Validate result for successful cases if err == nil { if result.IsZero() { - t.Errorf("parseTimeParam(%q) returned zero time", tt.timeStr) + t.Errorf("util.ParseTimeParam(%q) returned zero time", tt.timeStr) } } }) @@ -574,23 +575,23 @@ func TestParseTimeParam_RelativeTime(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result, err := parseTimeParam(tt.timeStr) + result, err := util.ParseTimeParam(tt.timeStr) if err != nil { - t.Fatalf("parseTimeParam(%q) unexpected error: %v", tt.timeStr, err) + t.Fatalf("util.ParseTimeParam(%q) unexpected error: %v", tt.timeStr, err) } now := time.Now() if tt.expectPast && result.After(now) { - t.Errorf("parseTimeParam(%q) = %v, expected time in the past", tt.timeStr, result) + t.Errorf("util.ParseTimeParam(%q) = %v, expected time in the past", tt.timeStr, result) } }) } } func TestParseTimeParam_NowKeyword(t *testing.T) { - result, err := parseTimeParam("now") + result, err := util.ParseTimeParam("now") if err != nil { - t.Fatalf("parseTimeParam(\"now\") unexpected error: %v", err) + t.Fatalf("util.ParseTimeParam(\"now\") unexpected error: %v", err) } now := time.Now() @@ -598,6 +599,6 @@ func TestParseTimeParam_NowKeyword(t *testing.T) { // Should be very close to current time (within 1 second) if diff > time.Second || diff < -time.Second { - t.Errorf("parseTimeParam(\"now\") = %v, too far from current time %v (diff: %v)", result, now, diff) + t.Errorf("util.ParseTimeParam(\"now\") = %v, too far from current time %v (diff: %v)", result, now, diff) } } diff --git a/cmd/rum.go b/cmd/rum.go index 16c9fc55..74c2931b 100644 --- a/cmd/rum.go +++ b/cmd/rum.go @@ -12,6 +12,7 @@ import ( "github.com/DataDog/datadog-api-client-go/v2/api/datadog" "github.com/DataDog/datadog-api-client-go/v2/api/datadogV2" "github.com/DataDog/pup/pkg/formatter" + "github.com/DataDog/pup/pkg/util" "github.com/spf13/cobra" ) @@ -658,14 +659,14 @@ func runRumRetentionFiltersDelete(cmd *cobra.Command, args []string) error { // RUM Sessions Implementation func runRumSessionsList(cmd *cobra.Command, args []string) error { // Convert relative time strings to absolute timestamps (validate input first) - fromTime, err := parseTimeString(rumFrom) + fromTime, err := util.ParseTimeToUnixMilli(rumFrom) if err != nil { - return fmt.Errorf("invalid --from time: %w\n\nSupported formats:\n- Relative: 1h, 30m, 7d, 1w (hour, minute, day, week)\n- Absolute: Unix timestamp in milliseconds\n- now: Current time", err) + return fmt.Errorf("invalid --from time: %w\n\nSupported formats:\n- Relative: 1h, 30m, 7d, 1w (hour, minute, day, week)\n- Absolute: Unix timestamp in milliseconds\n- RFC3339: 2024-01-01T00:00:00Z\n- now: Current time", err) } - toTime, err := parseTimeString(rumTo) + toTime, err := util.ParseTimeToUnixMilli(rumTo) if err != nil { - return fmt.Errorf("invalid --to time: %w\n\nSupported formats:\n- Relative: 1h, 30m, 7d, 1w (hour, minute, day, week)\n- Absolute: Unix timestamp in milliseconds\n- now: Current time", err) + return fmt.Errorf("invalid --to time: %w\n\nSupported formats:\n- Relative: 1h, 30m, 7d, 1w (hour, minute, day, week)\n- Absolute: Unix timestamp in milliseconds\n- RFC3339: 2024-01-01T00:00:00Z\n- now: Current time", err) } // Convert timestamps to strings for RUM API @@ -706,14 +707,14 @@ func runRumSessionsList(cmd *cobra.Command, args []string) error { func runRumSessionsSearch(cmd *cobra.Command, args []string) error { // Convert relative time strings to absolute timestamps (validate input first) - fromTime, err := parseTimeString(rumFrom) + fromTime, err := util.ParseTimeToUnixMilli(rumFrom) if err != nil { - return fmt.Errorf("invalid --from time: %w\n\nSupported formats:\n- Relative: 1h, 30m, 7d, 1w (hour, minute, day, week)\n- Absolute: Unix timestamp in milliseconds\n- now: Current time", err) + return fmt.Errorf("invalid --from time: %w\n\nSupported formats:\n- Relative: 1h, 30m, 7d, 1w (hour, minute, day, week)\n- Absolute: Unix timestamp in milliseconds\n- RFC3339: 2024-01-01T00:00:00Z\n- now: Current time", err) } - toTime, err := parseTimeString(rumTo) + toTime, err := util.ParseTimeToUnixMilli(rumTo) if err != nil { - return fmt.Errorf("invalid --to time: %w\n\nSupported formats:\n- Relative: 1h, 30m, 7d, 1w (hour, minute, day, week)\n- Absolute: Unix timestamp in milliseconds\n- now: Current time", err) + return fmt.Errorf("invalid --to time: %w\n\nSupported formats:\n- Relative: 1h, 30m, 7d, 1w (hour, minute, day, week)\n- Absolute: Unix timestamp in milliseconds\n- RFC3339: 2024-01-01T00:00:00Z\n- now: Current time", err) } // Convert timestamps to strings for RUM API diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index 13680789..e01620f8 100644 --- a/docs/COMMANDS.md +++ b/docs/COMMANDS.md @@ -22,7 +22,7 @@ pup [options] # Nested commands | auth | login, logout, status, refresh | cmd/auth.go | ✅ | | metrics | query, list, get, search | cmd/metrics.go | ✅ | | logs | search, list, aggregate | cmd/logs.go | ✅ | -| traces | search, list, aggregate | cmd/traces.go | ✅ | +| traces | - | cmd/traces_simple.go | ❌ | | monitors | list, get, delete, search | cmd/monitors.go | ✅ | | dashboards | list, get, delete, url | cmd/dashboards.go | ✅ | | slos | list, get, create, update, delete, corrections | cmd/slos.go | ✅ | @@ -108,7 +108,7 @@ pup infrastructure hosts list ### Data & Observability - **metrics** - Time-series metrics (query, list, get, search) - **logs** - Log search and analysis (search, list, aggregate) -- **traces** - APM traces (search, list, aggregate) +- **traces** - APM traces (not yet implemented - use `apm` commands instead) - **rum** - Real User Monitoring (apps, metrics, retention-filters, sessions) - **events** - Infrastructure events (list, search, get) diff --git a/pkg/util/time.go b/pkg/util/time.go index e80f6c14..744522f0 100644 --- a/pkg/util/time.go +++ b/pkg/util/time.go @@ -9,17 +9,25 @@ import ( "fmt" "regexp" "strconv" + "strings" "time" ) // ParseTimeParam parses time parameters supporting multiple formats: // - "now" for current time // - Unix timestamps (e.g., "1704067200") -// - Relative time (e.g., "1h", "30m", "7d") +// - Relative time with flexible formats: +// - Short: "1h", "30m", "7d", "5s", "1w" +// - Long: "5min", "5mins", "5minute", "5minutes" +// - Long: "2hr", "2hrs", "2hour", "2hours" +// - Long: "3day", "3days" +// - Long: "1week", "1weeks" +// - With spaces: "5 minutes", "2 hours" +// - With minus prefix: "-5m", "-2h" (treated same as "5m", "2h") // - ISO date strings (e.g., "2024-01-01T00:00:00Z") func ParseTimeParam(timeStr string) (time.Time, error) { - // Handle "now" - if timeStr == "now" { + // Handle "now" (case-insensitive) + if strings.ToLower(timeStr) == "now" { return time.Now(), nil } @@ -31,8 +39,10 @@ func ParseTimeParam(timeStr string) (time.Time, error) { } } - // Try parsing relative time (e.g., "1h", "30m", "2d") - re := regexp.MustCompile(`^(\d+)([smhd])$`) + // Try parsing relative time with flexible formats + // Supports: 5m, 5min, 5mins, 5minute, 5minutes, 5 minutes, -5m, etc. + // Case-insensitive to handle MIN, Hour, HOURS, etc. + re := regexp.MustCompile(`(?i)^-?(\d+)\s*(s|sec|secs|second|seconds|m|min|mins|minute|minutes|h|hr|hrs|hour|hours|d|day|days|w|week|weeks)$`) matches := re.FindStringSubmatch(timeStr) if len(matches) == 3 { value, err := strconv.Atoi(matches[1]) @@ -40,18 +50,21 @@ func ParseTimeParam(timeStr string) (time.Time, error) { return time.Time{}, fmt.Errorf("invalid time value: %w", err) } - unit := matches[2] + unit := strings.ToLower(matches[2]) var duration time.Duration + // Map all variations to their base duration switch unit { - case "s": + case "s", "sec", "secs", "second", "seconds": duration = time.Duration(value) * time.Second - case "m": + case "m", "min", "mins", "minute", "minutes": duration = time.Duration(value) * time.Minute - case "h": + case "h", "hr", "hrs", "hour", "hours": duration = time.Duration(value) * time.Hour - case "d": + case "d", "day", "days": duration = time.Duration(value) * 24 * time.Hour + case "w", "week", "weeks": + duration = time.Duration(value) * 7 * 24 * time.Hour } return time.Now().Add(-duration), nil diff --git a/pkg/util/time_test.go b/pkg/util/time_test.go index 498eb20a..37ef2b01 100644 --- a/pkg/util/time_test.go +++ b/pkg/util/time_test.go @@ -96,9 +96,114 @@ func TestParseTimeParam(t *testing.T) { wantError: true, }, { - name: "negative value", + name: "negative value (minus prefix)", input: "-5h", - wantError: true, + wantError: false, + checkFunc: func(t time.Time) bool { + expected := now.Add(-5 * time.Hour) + diff := expected.Sub(t).Abs() + return diff < time.Second + }, + }, + { + name: "long form: minutes", + input: "5minutes", + wantError: false, + checkFunc: func(t time.Time) bool { + expected := now.Add(-5 * time.Minute) + diff := expected.Sub(t).Abs() + return diff < time.Second + }, + }, + { + name: "long form: min", + input: "10min", + wantError: false, + checkFunc: func(t time.Time) bool { + expected := now.Add(-10 * time.Minute) + diff := expected.Sub(t).Abs() + return diff < time.Second + }, + }, + { + name: "long form: hours", + input: "2hours", + wantError: false, + checkFunc: func(t time.Time) bool { + expected := now.Add(-2 * time.Hour) + diff := expected.Sub(t).Abs() + return diff < time.Second + }, + }, + { + name: "long form: hr", + input: "3hr", + wantError: false, + checkFunc: func(t time.Time) bool { + expected := now.Add(-3 * time.Hour) + diff := expected.Sub(t).Abs() + return diff < time.Second + }, + }, + { + name: "long form: hrs", + input: "4hrs", + wantError: false, + checkFunc: func(t time.Time) bool { + expected := now.Add(-4 * time.Hour) + diff := expected.Sub(t).Abs() + return diff < time.Second + }, + }, + { + name: "long form: days", + input: "14days", + wantError: false, + checkFunc: func(t time.Time) bool { + expected := now.Add(-14 * 24 * time.Hour) + diff := expected.Sub(t).Abs() + return diff < time.Second + }, + }, + { + name: "long form: weeks", + input: "2weeks", + wantError: false, + checkFunc: func(t time.Time) bool { + expected := now.Add(-2 * 7 * 24 * time.Hour) + diff := expected.Sub(t).Abs() + return diff < time.Second + }, + }, + { + name: "with space: minutes", + input: "5 minutes", + wantError: false, + checkFunc: func(t time.Time) bool { + expected := now.Add(-5 * time.Minute) + diff := expected.Sub(t).Abs() + return diff < time.Second + }, + }, + { + name: "with space: hours", + input: "2 hours", + wantError: false, + checkFunc: func(t time.Time) bool { + expected := now.Add(-2 * time.Hour) + diff := expected.Sub(t).Abs() + return diff < time.Second + }, + }, + { + name: "with minus prefix and long form", + input: "-10minutes", + wantError: false, + checkFunc: func(t time.Time) bool { + expected := now.Add(-10 * time.Minute) + diff := expected.Sub(t).Abs() + return diff < time.Second + }, }, { name: "empty string",