diff --git a/cmd/metrics.go b/cmd/metrics.go index f7622223..586a3712 100644 --- a/cmd/metrics.go +++ b/cmd/metrics.go @@ -549,8 +549,8 @@ func runMetricsQuery(cmd *cobra.Command, args []string) error { Name: datadog.PtrString("a"), }, }}, - From: from.UTC().UnixMilli(), - To: to.UTC().UnixMilli(), + From: from.UTC().Unix() * 1000, + To: to.UTC().Unix() * 1000, }, Type: datadogV2.TIMESERIESFORMULAREQUESTTYPE_TIMESERIES_REQUEST, }, diff --git a/cmd/metrics_test.go b/cmd/metrics_test.go index bfd002e6..31814b45 100644 --- a/cmd/metrics_test.go +++ b/cmd/metrics_test.go @@ -601,3 +601,110 @@ func TestParseTimeParam_NowKeyword(t *testing.T) { t.Errorf("parseTimeParam(\"now\") = %v, too far from current time %v (diff: %v)", result, now, diff) } } + +// TestV2TimeseriesTimestampConversion verifies that the timestamp conversion +// used for the Datadog V2 timeseries API produces correct millisecond values. +// +// The V2 API expects timestamps in milliseconds. Previously, UnixMilli() was +// used directly, but this could produce incorrect values due to sub-second +// precision in the time.Time value propagating unexpected digits. Using +// Unix() * 1000 truncates sub-second precision and produces clean millisecond +// boundaries that the API accepts reliably. +func TestV2TimeseriesTimestampConversion(t *testing.T) { + tests := []struct { + name string + timeStr string + }{ + { + name: "now keyword", + timeStr: "now", + }, + { + name: "relative 1 hour", + timeStr: "1h", + }, + { + name: "relative 30 minutes", + timeStr: "30m", + }, + { + name: "relative 7 days", + timeStr: "7d", + }, + { + name: "unix timestamp", + timeStr: "1700000000", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + parsed, err := parseTimeParam(tt.timeStr) + if err != nil { + t.Fatalf("parseTimeParam(%q) unexpected error: %v", tt.timeStr, err) + } + + // This is the conversion used in runMetricsQuery for the V2 timeseries API. + // Using Unix() * 1000 ensures we get clean millisecond-boundary timestamps + // without sub-millisecond noise that UnixMilli() can include from + // time.Time's internal nanosecond precision. + msTimestamp := parsed.UTC().Unix() * 1000 + + // The millisecond timestamp must be positive + if msTimestamp <= 0 { + t.Errorf("expected positive millisecond timestamp, got %d", msTimestamp) + } + + // It must be evenly divisible by 1000 (i.e., on a second boundary) + // This is the key property: Unix() * 1000 always produces second-aligned + // millisecond timestamps, whereas UnixMilli() can include sub-second + // components that cause API issues. + if msTimestamp%1000 != 0 { + t.Errorf("timestamp %d is not on a second boundary (remainder: %d)", msTimestamp, msTimestamp%1000) + } + + // Verify round-trip: converting back should match the original second + roundTripped := time.Unix(msTimestamp/1000, 0) + if roundTripped.Unix() != parsed.UTC().Unix() { + t.Errorf("round-trip failed: original Unix=%d, round-tripped Unix=%d", + parsed.UTC().Unix(), roundTripped.Unix()) + } + }) + } +} + +// TestV2TimestampUnixMilliVsUnixTimes1000 demonstrates the difference between +// UnixMilli() and Unix()*1000 when a time.Time has sub-second precision. +// parseTimeParam("now") calls time.Now() which includes nanosecond precision, +// and relative times are computed via time.Duration arithmetic that also +// preserves nanosecond precision. +func TestV2TimestampUnixMilliVsUnixTimes1000(t *testing.T) { + // Construct a time with known sub-second precision + // 2024-01-15 12:00:00.123456789 UTC + ts := time.Date(2024, 1, 15, 12, 0, 0, 123456789, time.UTC) + + unixMilli := ts.UnixMilli() // Includes millisecond component: ...123 + unixTimes1000 := ts.Unix() * 1000 // Truncated to second boundary: ...000 + + // UnixMilli includes the sub-second milliseconds + if unixMilli%1000 == 0 { + t.Error("expected UnixMilli() to have sub-second component for a time with nanoseconds") + } + + // Unix()*1000 is always on a second boundary + if unixTimes1000%1000 != 0 { + t.Errorf("expected Unix()*1000 to be on second boundary, got remainder %d", unixTimes1000%1000) + } + + // The difference should be exactly the millisecond component (123ms) + diff := unixMilli - unixTimes1000 + if diff != 123 { + t.Errorf("expected difference of 123ms, got %d", diff) + } + + // Both should represent approximately the same point in time + // (within 1 second) + if diff < 0 || diff >= 1000 { + t.Errorf("timestamps diverged by more than 1 second: diff=%dms", diff) + } +}