Skip to content

Commit 677f3c0

Browse files
Merge pull request #61 from DataDog/fix/second-aligned-millisecond-timestamps
fix(util): second-aligned millisecond timestamps for all Datadog APIs
2 parents 61dc4e7 + 710c533 commit 677f3c0

3 files changed

Lines changed: 89 additions & 10 deletions

File tree

cmd/metrics.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -533,13 +533,13 @@ func runMetricsQuery(cmd *cobra.Command, args []string) error {
533533
return err
534534
}
535535

536-
// Parse time ranges
537-
from, err := util.ParseTimeParam(fromTime)
536+
// Parse time ranges as second-aligned millisecond timestamps
537+
fromMs, err := util.ParseTimeToUnixMilli(fromTime)
538538
if err != nil {
539539
return fmt.Errorf("invalid --from time: %w", err)
540540
}
541541

542-
to, err := util.ParseTimeParam(toTime)
542+
toMs, err := util.ParseTimeToUnixMilli(toTime)
543543
if err != nil {
544544
return fmt.Errorf("invalid --to time: %w", err)
545545
}
@@ -560,8 +560,8 @@ func runMetricsQuery(cmd *cobra.Command, args []string) error {
560560
Name: datadog.PtrString("a"),
561561
},
562562
}},
563-
From: from.UTC().UnixMilli(),
564-
To: to.UTC().UnixMilli(),
563+
From: fromMs,
564+
To: toMs,
565565
},
566566
Type: datadogV2.TIMESERIESFORMULAREQUESTTYPE_TIMESERIES_REQUEST,
567567
},
@@ -572,11 +572,11 @@ func runMetricsQuery(cmd *cobra.Command, args []string) error {
572572
if r != nil {
573573
apiBody := extractAPIErrorBody(err)
574574
if apiBody != "" {
575-
return fmt.Errorf("failed to query metrics: %w\nStatus: %d\nAPI Response: %s\n\nRequest Details:\n- Query: %s\n- From: %s (Unix: %d)\n- To: %s (Unix: %d)\n\nTroubleshooting:\n- Verify your query syntax is correct (e.g., avg:metric.name{filter})\n- Check that the time range is valid\n- Ensure the metric exists and has data in the specified time range\n- Confirm you have proper permissions to access the metric",
575+
return fmt.Errorf("failed to query metrics: %w\nStatus: %d\nAPI Response: %s\n\nRequest Details:\n- Query: %s\n- From: %d\n- To: %d\n\nTroubleshooting:\n- Verify your query syntax is correct (e.g., avg:metric.name{filter})\n- Check that the time range is valid\n- Ensure the metric exists and has data in the specified time range\n- Confirm you have proper permissions to access the metric",
576576
err, r.StatusCode, apiBody,
577577
queryString,
578-
from.Format(time.RFC3339), from.Unix(),
579-
to.Format(time.RFC3339), to.Unix())
578+
fromMs,
579+
toMs)
580580
}
581581
return fmt.Errorf("failed to query metrics: %w (status: %d)", err, r.StatusCode)
582582
}

cmd/metrics_test.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -824,3 +824,79 @@ func TestParseTimeParam_NowKeyword(t *testing.T) {
824824
t.Errorf("util.ParseTimeParam(\"now\") = %v, too far from current time %v (diff: %v)", result, now, diff)
825825
}
826826
}
827+
828+
// TestV2TimeseriesTimestampConversion verifies that ParseTimeToUnixMilli
829+
// produces second-aligned millisecond timestamps suitable for Datadog APIs.
830+
func TestV2TimeseriesTimestampConversion(t *testing.T) {
831+
tests := []struct {
832+
name string
833+
timeStr string
834+
}{
835+
{"now keyword", "now"},
836+
{"relative 1 hour", "1h"},
837+
{"relative 30 minutes", "30m"},
838+
{"relative 7 days", "7d"},
839+
{"unix timestamp", "1700000000"},
840+
}
841+
842+
for _, tt := range tests {
843+
t.Run(tt.name, func(t *testing.T) {
844+
msTimestamp, err := util.ParseTimeToUnixMilli(tt.timeStr)
845+
if err != nil {
846+
t.Fatalf("ParseTimeToUnixMilli(%q) unexpected error: %v", tt.timeStr, err)
847+
}
848+
849+
if msTimestamp <= 0 {
850+
t.Errorf("expected positive millisecond timestamp, got %d", msTimestamp)
851+
}
852+
853+
// Must be on a second boundary (divisible by 1000)
854+
if msTimestamp%1000 != 0 {
855+
t.Errorf("timestamp %d is not on a second boundary (remainder: %d)", msTimestamp, msTimestamp%1000)
856+
}
857+
858+
// Verify round-trip: converting back should produce a valid second
859+
roundTripped := time.Unix(msTimestamp/1000, 0)
860+
if roundTripped.Unix() != msTimestamp/1000 {
861+
t.Errorf("round-trip failed: expected Unix=%d, got Unix=%d",
862+
msTimestamp/1000, roundTripped.Unix())
863+
}
864+
})
865+
}
866+
}
867+
868+
// TestV2TimestampUnixMilliVsUnixTimes1000 demonstrates the difference between
869+
// UnixMilli() and Unix()*1000 when a time.Time has sub-second precision.
870+
// parseTimeParam("now") calls time.Now() which includes nanosecond precision,
871+
// and relative times are computed via time.Duration arithmetic that also
872+
// preserves nanosecond precision.
873+
func TestV2TimestampUnixMilliVsUnixTimes1000(t *testing.T) {
874+
// Construct a time with known sub-second precision
875+
// 2024-01-15 12:00:00.123456789 UTC
876+
ts := time.Date(2024, 1, 15, 12, 0, 0, 123456789, time.UTC)
877+
878+
unixMilli := ts.UnixMilli() // Includes millisecond component: ...123
879+
unixTimes1000 := ts.Unix() * 1000 // Truncated to second boundary: ...000
880+
881+
// UnixMilli includes the sub-second milliseconds
882+
if unixMilli%1000 == 0 {
883+
t.Error("expected UnixMilli() to have sub-second component for a time with nanoseconds")
884+
}
885+
886+
// Unix()*1000 is always on a second boundary
887+
if unixTimes1000%1000 != 0 {
888+
t.Errorf("expected Unix()*1000 to be on second boundary, got remainder %d", unixTimes1000%1000)
889+
}
890+
891+
// The difference should be exactly the millisecond component (123ms)
892+
diff := unixMilli - unixTimes1000
893+
if diff != 123 {
894+
t.Errorf("expected difference of 123ms, got %d", diff)
895+
}
896+
897+
// Both should represent approximately the same point in time
898+
// (within 1 second)
899+
if diff < 0 || diff >= 1000 {
900+
t.Errorf("timestamps diverged by more than 1 second: diff=%dms", diff)
901+
}
902+
}

pkg/util/time.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,11 +89,14 @@ func ParseTimeToUnix(timeStr string) (int64, error) {
8989
return t.Unix(), nil
9090
}
9191

92-
// ParseTimeToUnixMilli parses time string and returns Unix timestamp in milliseconds
92+
// ParseTimeToUnixMilli parses time string and returns Unix timestamp in milliseconds.
93+
// Uses Unix()*1000 instead of UnixMilli() to produce second-aligned timestamps.
94+
// time.Now() and duration arithmetic produce nanosecond precision, and UnixMilli()
95+
// preserves the sub-second component which some Datadog APIs reject or misinterpret.
9396
func ParseTimeToUnixMilli(timeStr string) (int64, error) {
9497
t, err := ParseTimeParam(timeStr)
9598
if err != nil {
9699
return 0, err
97100
}
98-
return t.UnixMilli(), nil
101+
return t.Unix() * 1000, nil
99102
}

0 commit comments

Comments
 (0)