From 601d798607f993cbfbe86530e2db746b6cd2b6c4 Mon Sep 17 00:00:00 2001 From: Corey Ballou Date: Thu, 12 Feb 2026 12:34:12 -0500 Subject: [PATCH] fix(metrics): use Unix()*1000 instead of UnixMilli() for V2 timeseries API timestamps The Datadog V2 timeseries query API expects millisecond timestamps, but using time.Time.UnixMilli() directly caused query failures. The root cause is that parseTimeParam returns time values with nanosecond precision (from time.Now() and time.Duration arithmetic), and UnixMilli() faithfully includes the sub-second millisecond component. This produces timestamps that are not on clean second boundaries, which the API rejects or misinterprets. Switching to Unix() * 1000 truncates sub-second precision and produces second-aligned millisecond timestamps that the API accepts reliably. --- cmd/metrics.go | 4 +- cmd/metrics_test.go | 107 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+), 2 deletions(-) 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) + } +}