fix(observability-proxy): bound only the logs times sent to vlselect

The 1970..2262 check refused times on parsing, including a start that
the window clamps to the maximum range before anything is sent and a
since that only becomes a capped start_offset, so an old start or a
tail resume from before 1970 answered 400 where the documented answer
is to clamp. It had also detached the parseTime doc comment.

The check now applies to the times actually sent, after clamping, and
uses the range vlselect can hold, int64 nanoseconds (1677..2262); 1970
is not a limit of vlselect.

Assisted-by: LLM
Signed-off-by: ZverGuy <maximbel2003@gmail.com>
This commit is contained in:
ZverGuy committed 2026-11-10 00:45:07 +07:00
1 parent 3722808050
commit f8ff307f71
2 files changed
+41 -19

No files matched your search

+21 -15
View File
@@ -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
+20 -4
View File
@@ -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)
}
}