diff --git a/internal/observabilityproxy/logsparams.go b/internal/observabilityproxy/logsparams.go index 2952209c4..220a06762 100644 --- a/internal/observabilityproxy/logsparams.go +++ b/internal/observabilityproxy/logsparams.go @@ -160,6 +160,11 @@ func window(in url.Values, now time.Time, lim LogsLimits) (time.Time, time.Time, if end.Sub(start) > lim.MaxRange { start = end.Add(-lim.MaxRange) } + for k, t := range map[string]time.Time{"start": start, "end": end} { + if herr := sendableTime(k, t); herr != nil { + return time.Time{}, time.Time{}, herr + } + } return start.UTC(), end.UTC(), nil } @@ -228,6 +233,9 @@ func tailParams(in, out url.Values, now time.Time, lim LogsLimits) *HTTPError { if err != nil { return errorf(http.StatusBadRequest, "invalid %s: %v", k, err) } + if herr := sendableTime(k, t); herr != nil { + return herr + } out.Set(k, t.UTC().Format(time.RFC3339Nano)) if k == "start" { start = t @@ -323,23 +331,21 @@ func scaleDuration(f float64, unit time.Duration) (time.Duration, bool) { return time.Duration(v), true } +// sendableTime refuses a time vlselect cannot hold: it reads the RFC3339 +// the proxy sends through time.UnixNano, which is undefined outside the +// int64 range (1677..2262) and would move the window anywhere. Only times +// actually sent are checked; an older start is clamped first. +func sendableTime(key string, t time.Time) *HTTPError { + if t.Before(time.Unix(0, math.MinInt64)) || t.After(time.Unix(0, math.MaxInt64)) { + return errorf(http.StatusBadRequest, "invalid %s: %s is outside 1677..2262", key, t.Format(time.RFC3339)) + } + return nil +} + // parseTime accepts "now", unix seconds with an optional fraction, RFC3339, // and a duration meaning that long before now (VictoriaLogs' "start=3h"). // Anything else is refused rather than guessed, since a misread timestamp // silently moves the window. -// maxLogsTime is the last instant int64 nanoseconds hold. -var maxLogsTime = time.Unix(0, math.MaxInt64) - -// absoluteLogsTime refuses a time vlselect cannot hold: it keeps times as -// int64 nanoseconds (time.UnixNano), which is undefined outside 1678..2262 -// and would move the window anywhere; nothing was logged before 1970. -func absoluteLogsTime(s string, t time.Time) (time.Time, error) { - if t.Before(time.Unix(0, 0)) || !t.Before(maxLogsTime) { - return time.Time{}, fmt.Errorf("time %q is outside 1970..2262", s) - } - return t, nil -} - func parseTime(s string, now time.Time) (time.Time, error) { if s == "now" { return now, nil @@ -359,10 +365,10 @@ func parseTime(s string, now time.Time) (time.Time, error) { frac = (frac + "000000000")[:9] nanos, _ = strconv.ParseInt(frac, 10, 64) } - return absoluteLogsTime(s, time.Unix(secs, nanos)) + return time.Unix(secs, nanos), nil } if t, err := time.Parse(time.RFC3339Nano, s); err == nil { - return absoluteLogsTime(s, t) + return t, nil } if d, err := parseDuration(s); err == nil { return now.Add(-d), nil diff --git a/internal/observabilityproxy/logsparams_test.go b/internal/observabilityproxy/logsparams_test.go index 3884428f0..f7d21753a 100644 --- a/internal/observabilityproxy/logsparams_test.go +++ b/internal/observabilityproxy/logsparams_test.go @@ -138,11 +138,11 @@ func TestBuildLogsParamsRejects(t *testing.T) { {"trailing dot", "query", url.Values{"start": {"300."}}, http.StatusBadRequest}, {"infinite time", "query", url.Values{"end": {"Inf"}}, http.StatusBadRequest}, // vlselect keeps times as int64 nanoseconds (time.UnixNano), which - // overflows outside 1678..2262; before 1970 is no log time either. - {"time before 1970", "query", url.Values{"start": {"0001-01-01T00:00:00Z"}, "end": {"0001-01-02T00:00:00Z"}}, http.StatusBadRequest}, - {"time past int64 nanoseconds", "query", url.Values{"start": {"9999-01-01T00:00:00Z"}, "end": {"9999-01-02T00:00:00Z"}}, http.StatusBadRequest}, - {"tail since before 1970", "tail", url.Values{"since": {"1969-12-31T23:59:59Z"}}, http.StatusBadRequest}, + // is undefined outside 1677..2262; a time sent there must fit. + {"end before int64 nanoseconds", "query", url.Values{"start": {"0001-01-01T00:00:00Z"}, "end": {"0001-01-02T00:00:00Z"}}, http.StatusBadRequest}, + {"end past int64 nanoseconds", "query", url.Values{"end": {"9999-01-02T00:00:00Z"}}, http.StatusBadRequest}, {"unix seconds past int64 nanoseconds", "query", url.Values{"start": {"9999999990"}, "end": {"9999999999"}}, http.StatusBadRequest}, + {"tail end past int64 nanoseconds", "tail", url.Values{"end": {"9999-01-02T00:00:00Z"}}, http.StatusBadRequest}, {"NaN time", "query", url.Values{"start": {"NaN"}}, http.StatusBadRequest}, {"NaN step", "hits", url.Values{"step": {"NaN"}}, http.StatusBadRequest}, {"NaN start_offset", "tail", url.Values{"start_offset": {"NaN"}}, http.StatusBadRequest}, @@ -392,3 +392,19 @@ func TestParseDurationBounds(t *testing.T) { } } } + +// TestBuildLogsParamsOldTimesClamped: only times that reach vlselect must +// fit its int64 nanoseconds. An old start is clamped to MaxRange before it +// is sent, and an old since becomes a start_offset capped at the resume +// limit, so both are served. +func TestBuildLogsParamsOldTimesClamped(t *testing.T) { + lim := testLimits() + out := build(t, "query", url.Values{"start": {"1900-01-01T00:00:00Z"}}) + if want := testNow.Add(-lim.MaxRange).Format(time.RFC3339Nano); out.Get("start") != want { + t.Errorf("start = %s, want clamped to %s", out.Get("start"), want) + } + out = build(t, "tail", url.Values{"since": {"1969-12-31T23:59:59Z"}}) + if got := out.Get("start_offset"); got != formatDuration(lim.MaxTailResume) { + t.Errorf("start_offset = %s, want the %s cap", got, lim.MaxTailResume) + } +}