diff --git a/app/vmselect/prometheus/prometheus.go b/app/vmselect/prometheus/prometheus.go index a244f13b9..67a2a955c 100644 --- a/app/vmselect/prometheus/prometheus.go +++ b/app/vmselect/prometheus/prometheus.go @@ -549,6 +549,14 @@ func getTime(r *http.Request, argKey string, defaultValue int64) (int64, error) // Try parsing string format t, err := time.Parse(time.RFC3339, argValue) if err != nil { + // Handle Prometheus'-provided minTime and maxTime. + // See https://github.com/prometheus/client_golang/issues/614 + switch argValue { + case prometheusMinTimeFormatted: + return minTimeMsecs, nil + case prometheusMaxTimeFormatted: + return maxTimeMsecs, nil + } return 0, fmt.Errorf("cannot parse %q=%q: %s", argKey, argValue, err) } secs = float64(t.UnixNano()) / 1e9 @@ -560,6 +568,13 @@ func getTime(r *http.Request, argKey string, defaultValue int64) (int64, error) return msecs, nil } +var ( + // These constants were obtained from https://github.com/prometheus/prometheus/blob/91d7175eaac18b00e370965f3a8186cc40bf9f55/web/api/v1/api.go#L442 + // See https://github.com/prometheus/client_golang/issues/614 for details. + prometheusMinTimeFormatted = time.Unix(math.MinInt64/1000+62135596801, 0).UTC().Format(time.RFC3339Nano) + prometheusMaxTimeFormatted = time.Unix(math.MaxInt64/1000-62135596801, 999999999).UTC().Format(time.RFC3339Nano) +) + const ( // These values prevent from overflow when storing msec-precision time in int64. minTimeMsecs = int64(-1<<63) / 1e6 diff --git a/app/vmselect/prometheus/prometheus_test.go b/app/vmselect/prometheus/prometheus_test.go new file mode 100644 index 000000000..a4a5cb517 --- /dev/null +++ b/app/vmselect/prometheus/prometheus_test.go @@ -0,0 +1,78 @@ +package prometheus + +import ( + "fmt" + "testing" + "net/http" + "net/url" +) + +func TestGetTimeSuccess(t *testing.T) { + f := func(s string, timestampExpected int64) { + t.Helper() + urlStr := fmt.Sprintf("http://foo.bar/baz?s=%s", url.QueryEscape(s)) + r, err := http.NewRequest("GET", urlStr, nil) + if err != nil { + t.Fatalf("unexpected error in NewRequest: %s", err) + } + + // Verify defaultValue + ts, err := getTime(r, "foo", 123) + if err != nil { + t.Fatalf("unexpected error when obtaining default time from getTime(%q): %s", s, err) + } + if ts != 123 { + t.Fatalf("unexpected default value for getTime(%q); got %d; want %d", s, ts, 123) + } + + // Verify timestampExpected + ts, err = getTime(r, "s", 123) + if err != nil { + t.Fatalf("unexpected error in getTime(%q): %s", s, err) + } + if ts != timestampExpected { + t.Fatalf("unexpected timestamp for getTime(%q); got %d; want %d", s, ts, timestampExpected) + } + } + + f("2019-07-07T20:01:02Z", 1562529662000) + f("2019-07-07T20:47:40+03:00", 1562521660000) + f("-292273086-05-16T16:47:06Z", -9223372036854) + f("292277025-08-18T07:12:54.999999999Z", 9223372036854) + f("1562529662.324", 1562529662324) + f("-9223372036.854", -9223372036854) +} + +func TestGetTimeError(t *testing.T) { + f := func(s string) { + t.Helper() + urlStr := fmt.Sprintf("http://foo.bar/baz?s=%s", url.QueryEscape(s)) + r, err := http.NewRequest("GET", urlStr, nil) + if err != nil { + t.Fatalf("unexpected error in NewRequest: %s", err) + } + + // Verify defaultValue + ts, err := getTime(r, "foo", 123) + if err != nil { + t.Fatalf("unexpected error when obtaining default time from getTime(%q): %s", s, err) + } + if ts != 123 { + t.Fatalf("unexpected default value for getTime(%q); got %d; want %d", s, ts, 123) + } + + // Verify timestampExpected + ts, err = getTime(r, "s", 123) + if err == nil { + t.Fatalf("expecting non-nil error in getTime(%q)", s) + } + } + + f("foo") + f("2019-07-07T20:01:02Zisdf") + f("2019-07-07T20:47:40+03:00123") + f("-292273086-05-16T16:47:07Z") + f("292277025-08-18T07:12:54.999999998Z") + f("-9223372036.855") + f("9223372036.855") +}