fix(observability-proxy): round the step up to whole milliseconds

The step is sent to vlselect in milliseconds and was truncated, so a
window that the bucket cap does not divide evenly got one bucket more
than --logs-max-buckets allows. Round it up before the window is
shrunk to fit the step, so both caps still hold.

Bare-seconds steps and start offsets are now tested on the accepting
side too: only the refused forms were, and disabling the digit branch
passed the suite.

Assisted-by: LLM
Signed-off-by: ZverGuy <maximbel2003@gmail.com>
This commit is contained in:
ZverGuy committed 2026-11-09 15:48:57 +07:00
1 parent a56ff5050f
commit 28b391ff17
2 files changed
+28

No files matched your search

@@ -179,6 +179,10 @@ func setStep(out, in url.Values, start, end time.Time, lim LogsLimits) *HTTPErro
}
step = max(min(d, span, lim.MaxRange/3), minStep)
}
// vlselect gets whole milliseconds; rounding down would add a bucket.
if r := step % time.Millisecond; r != 0 {
step += time.Millisecond - r
}
if span+2*step > lim.MaxRange {
start = end.Add(-max(lim.MaxRange-2*step, 0))
}
@@ -227,6 +227,12 @@ func TestBuildLogsParamsStepAndLimit(t *testing.T) {
if got := build(t, "stats_query_range", with(day, "step", "1w")).Get("step"); got != "86400000ms" {
t.Errorf("1w step on a day window = %s, want capped at the window", got)
}
if got := build(t, "hits", with(day, "step", "3600")).Get("step"); got != "3600000ms" {
t.Errorf("bare-seconds step = %s, want 3600000ms", got)
}
if got := build(t, "hits", with(day, "step", "5400.5")).Get("step"); got != "5400500ms" {
t.Errorf("fractional bare-seconds step = %s, want 5400500ms", got)
}
if got := build(t, "hits", day).Get("step"); got != "86400ms" {
t.Errorf("omitted step = %s, want the bucket-bounded minimum", got)
}
@@ -271,6 +277,7 @@ func TestBuildLogsParamsTail(t *testing.T) {
{"since in the future", url.Values{"since": {"2026-11-04T13:00:00Z"}}, "0ms"},
{"start_offset within cap", url.Values{"start_offset": {"2m"}}, "120000ms"},
{"start_offset over cap", url.Values{"start_offset": {"30d"}}, "900000ms"},
{"start_offset in bare seconds", url.Values{"start_offset": {"120"}}, "120000ms"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -328,3 +335,20 @@ func TestBuildLogsParamsStepCannotWidenWindow(t *testing.T) {
}
}
}
func TestBuildLogsParamsStepKeepsBucketCap(t *testing.T) {
lim := testLimits()
// Spans that MaxBuckets does not divide into whole milliseconds.
for _, start := range []string{"2026-11-03T12:00:00.0007Z", "2026-11-04T11:59:58.3333Z", "6d23h59m59s"} {
out := build(t, "hits", url.Values{"start": {start}})
step, err := parseDuration(out.Get("step"))
if err != nil {
t.Fatal(err)
}
s, _ := time.Parse(time.RFC3339Nano, out.Get("start"))
e, _ := time.Parse(time.RFC3339Nano, out.Get("end"))
if buckets := (e.Sub(s) + step - 1) / step; int(buckets) > lim.MaxBuckets {
t.Errorf("start %s: step %s gives %d buckets, more than %d", start, out.Get("step"), buckets, lim.MaxBuckets)
}
}
}