From 51bfcd1b73de893fc09ed4ce7ade74a0d72d0128 Mon Sep 17 00:00:00 2001 From: szibis Date: Wed, 13 May 2026 12:24:58 +0200 Subject: [PATCH 1/2] docs(changelog): add entry for documentation accuracy audit --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a3ed9d4..d25bea53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Documentation + +- docs: comprehensive accuracy audit across 9 doc files — fix wrong flag defaults (`-cb-open-duration` 2s→10s, `goMemLimitPercent` 70→85, `GOGC` 100→200, Go requirement 1.26.2→1.26.3, Loki benchmark stack 3.4.x→3.6.x), correct "not exposed as flags" claim for `-rate-limit-per-second`/`-rate-limit-burst`/`-max-concurrent` in configuration.md + operations.md + scaling.md, add missing Cold Storage Backend section to configuration.md (`-cold-enabled`, `-cold-backend`, `-cold-boundary`, `-cold-overlap`, `-cold-manifest-refresh`, `-cold-timeout`; shipped in 1.28.0 with zero doc coverage), add Go Runtime Tuning flag section, extend compatibility-loki.md release notes from v1.17.1 through v1.31.2, refresh roadmap.md Completed section through v1.31.2, clarify buildinfo `2.9.0` is intentional. + ## [1.31.3] - 2026-05-13 ### CI From ec0496df12f55b1c1f756b260ffcf9b9980eec4c Mon Sep 17 00:00:00 2001 From: szibis Date: Wed, 13 May 2026 13:48:35 +0200 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20address=20code=20review=20issues=20?= =?UTF-8?q?=E2=80=94=20correctness,=20perf,=20memory,=20and=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix 1 (correctness): Restore __error__ guard to parser-stage metric queries. VL native stats_query_range counts parse-failed lines while Loki excludes them. Queries with parser stages (| json, | logfmt, etc.) now route to the slow log-fetch path unless the caller explicitly opts in via " | drop __error__". The opt-in check uses origSpec.BaseQuery (inner LogQL pipeline without outer aggregation or range window brackets) so hasDropErrorOnlyPostParserStage correctly identifies the drop-error clause. The | math guard for grouped rate queries and a new pre-check for non-| math queries (count_over_time etc.) both apply this fast-path bypass before shouldUseManualRangeMetricCompat is called. Fix 2 (performance): Eliminate O(N²) copy amplification in coldBackwardChunkedFetch. Replaced prepend-into-growing-buffer (accumulated = append(chunkBody, accumulated...)) with a slice of chunks that is reversed once at the end and joined. Fix 3 (memory): Cap unbounded io.ReadAll in collectRangeMetricHits at 64 MB via the existing readBodyLimited helper to bound RSS on oversized VL error bodies. Fix 4 (documentation): Document optional tenant header behaviour in KNOWN_ISSUES.md. Fix 5 (documentation + code): Document multi-tenant serial fanout in KNOWN_ISSUES.md and add a TODO comment in multitenant.go referencing the tracking entry. --- docs/KNOWN_ISSUES.md | 2 + internal/proxy/cold_dispatch.go | 26 +++-- .../proxy/compat_coverage_helpers_test.go | 18 ++-- internal/proxy/multitenant.go | 3 + internal/proxy/range_metric_compat.go | 45 ++++++++- internal/proxy/range_metric_compat_test.go | 96 ++++++++++++++----- 6 files changed, 150 insertions(+), 40 deletions(-) diff --git a/docs/KNOWN_ISSUES.md b/docs/KNOWN_ISSUES.md index 7ec0c639..a89d3878 100644 --- a/docs/KNOWN_ISSUES.md +++ b/docs/KNOWN_ISSUES.md @@ -63,6 +63,8 @@ equivalents. | Startup warm readiness | When patterns or label-values startup warm is configured, readiness can remain `503` until disk restore or peer warm completes. | | Older VictoriaLogs metadata paths | Newer VictoriaLogs versions let the proxy prefer stream-only metadata APIs. Older versions may fall back to broader field APIs, which can change how strictly stream-shaped some browse endpoints feel. | | Large body fields | Very large body fields can still be dropped on the VictoriaLogs side. Track the upstream issue: [VictoriaLogs issue #91](https://github.com/VictoriaMetrics/victorialogs-datasource/issues/91). | +| Optional tenant header | The proxy accepts requests without an `X-Scope-OrgID` header and routes them to the default tenant. In deployments where tenant isolation is required, callers must supply the header; the proxy does not enforce a hard rejection. Add `X-Scope-OrgID` enforcement at the ingress/gateway layer if you need a strict tenant boundary. | +| Multi-tenant serial fanout | When `X-Scope-OrgID` contains multiple tenants (e.g. `tenant1\|tenant2\|tenant3`), the proxy issues sub-requests to each tenant sequentially. Latency scales linearly with the number of tenants. For high fan-out counts (>4 tenants), consider running per-tenant Grafana datasources instead of relying on the multi-tenant merge path. | ## What Is No Longer an Open Gap diff --git a/internal/proxy/cold_dispatch.go b/internal/proxy/cold_dispatch.go index 13015643..69d73be4 100644 --- a/internal/proxy/cold_dispatch.go +++ b/internal/proxy/cold_dispatch.go @@ -126,7 +126,9 @@ func countNDJSONLines(body []byte) int { // // Returns the accumulated NDJSON rows in ascending time order. func (p *Proxy) coldBackwardChunkedFetch(ctx context.Context, baseParams url.Values, startNs, endNs int64, limit int) ([]byte, error) { - var accumulated []byte + // Chunks are collected newest-to-oldest in a slice and joined at the end to avoid + // O(N²) copy amplification from prepend-into-growing-buffer on each iteration. + var chunks [][]byte accCount := 0 chunkEnd := endNs chunkDurNs := coldBackwardChunkDuration.Nanoseconds() @@ -137,10 +139,19 @@ func (p *Proxy) coldBackwardChunkedFetch(ctx context.Context, baseParams url.Val chunkStart = startNs } + // Cap per-chunk limit to the remaining rows needed; never overfetch. + chunkLimit := limit - accCount + if chunkLimit <= 0 { + break + } + if chunkLimit > maxLimitValue { + chunkLimit = maxLimitValue + } + chunkParams := cloneURLValues(baseParams) chunkParams.Set("start", strconv.FormatInt(chunkStart, 10)) chunkParams.Set("end", strconv.FormatInt(chunkEnd, 10)) - chunkParams.Set("limit", strconv.Itoa(maxLimitValue)) + chunkParams.Set("limit", strconv.Itoa(chunkLimit)) resp, err := p.coldRouter.ColdPost(ctx, "/select/logsql/query", chunkParams) if err != nil { @@ -158,9 +169,7 @@ func (p *Proxy) coldBackwardChunkedFetch(ctx context.Context, baseParams url.Val } chunkCount := countNDJSONLines(chunkBody) - // Prepend this chunk so the accumulation buffer stays in ascending order - // (oldest chunk first, newest chunk last). - accumulated = append(chunkBody, accumulated...) + chunks = append(chunks, chunkBody) accCount += chunkCount if accCount >= limit { @@ -168,7 +177,12 @@ func (p *Proxy) coldBackwardChunkedFetch(ctx context.Context, baseParams url.Val } chunkEnd = chunkStart } - return accumulated, nil + + // chunks is newest-to-oldest; reverse so the joined result is ascending (oldest first). + for i, j := 0, len(chunks)-1; i < j; i, j = i+1, j-1 { + chunks[i], chunks[j] = chunks[j], chunks[i] + } + return bytes.Join(chunks, nil), nil } func (p *Proxy) proxyLogQueryCold(w http.ResponseWriter, r *http.Request, logsqlQuery string) { diff --git a/internal/proxy/compat_coverage_helpers_test.go b/internal/proxy/compat_coverage_helpers_test.go index 88dcd8b3..e45b7677 100644 --- a/internal/proxy/compat_coverage_helpers_test.go +++ b/internal/proxy/compat_coverage_helpers_test.go @@ -59,13 +59,15 @@ func TestCompatHelpers_ParseQuantileAndUnwrapErrorName(t *testing.T) { } // rate / bytes_rate: sliding window (range != step) always requires manual path. - // When range == step (tumbling window), VL native stats is semantically equivalent, - // including for queries with parser stages. + // Tumbling window (range == step) also requires manual path for parser-stage queries: + // Loki excludes parse-failed lines from metric aggregation; VL native stats counts all. + // Callers that explicitly add "| drop __error__" opt in to VL's count-all semantics; + // that opt-in is checked at the call site (handleStatsCompatRange) before this function. if !shouldUseManualRangeMetricCompat(`{app="api"} | unpack_json`, "rate", false) { t.Fatal("expected parser-stage rate to use manual fallback when range != step") } - if shouldUseManualRangeMetricCompat(`{app="api"} | unpack_json`, "rate", true) { - t.Fatal("expected parser-stage rate to use VL native stats when range == step") + if !shouldUseManualRangeMetricCompat(`{app="api"} | unpack_json`, "rate", true) { + t.Fatal("expected parser-stage rate to use manual fallback even when range == step (no drop-error opt-in)") } if !shouldUseManualRangeMetricCompat(`{app="api"}`, "rate", false) { t.Fatal("expected non-parser rate to use manual fallback when range != step (sliding window)") @@ -99,14 +101,14 @@ func TestCompatHelpers_ParseQuantileAndUnwrapErrorName(t *testing.T) { if !shouldUseManualRangeMetricCompat(`{app="api"} | unpack_json`, "count_over_time", false) { t.Fatal("expected parser-stage count_over_time to use manual fallback when range != step") } - if shouldUseManualRangeMetricCompat(`{app="api"} | unpack_json`, "count_over_time", true) { - t.Fatal("expected parser-stage count_over_time to use VL native stats when range == step") + if !shouldUseManualRangeMetricCompat(`{app="api"} | unpack_json`, "count_over_time", true) { + t.Fatal("expected parser-stage count_over_time to use manual fallback even when range == step (no drop-error opt-in)") } if !shouldUseManualRangeMetricCompat(`{app="api"} | unpack_json`, "bytes_over_time", false) { t.Fatal("expected parser-stage bytes_over_time to use manual fallback when range != step") } - if shouldUseManualRangeMetricCompat(`{app="api"} | unpack_json`, "bytes_over_time", true) { - t.Fatal("expected parser-stage bytes_over_time to use VL native stats when range == step") + if !shouldUseManualRangeMetricCompat(`{app="api"} | unpack_json`, "bytes_over_time", true) { + t.Fatal("expected parser-stage bytes_over_time to use manual fallback even when range == step (no drop-error opt-in)") } } diff --git a/internal/proxy/multitenant.go b/internal/proxy/multitenant.go index 4ee7edf9..66fdc532 100644 --- a/internal/proxy/multitenant.go +++ b/internal/proxy/multitenant.go @@ -69,6 +69,9 @@ func (p *Proxy) handleMultiTenantFanout(w http.ResponseWriter, r *http.Request, return true } + // TODO: fanout is serial — each tenant sub-request blocks the next. For high + // fan-out counts (>4 tenants) parallel dispatch would reduce latency. Tracked + // in docs/KNOWN_ISSUES.md under "Multi-tenant serial fanout". recorders := make([]*httptest.ResponseRecorder, 0, len(filteredTenants)) for _, tenantID := range filteredTenants { subReq := filteredReq.Clone(filteredReq.Context()) diff --git a/internal/proxy/range_metric_compat.go b/internal/proxy/range_metric_compat.go index 2a5cab46..85cf1f6c 100644 --- a/internal/proxy/range_metric_compat.go +++ b/internal/proxy/range_metric_compat.go @@ -33,6 +33,7 @@ type originalRangeMetricSpec struct { UnwrapField string UnwrapConv string HasUnwrap bool + BaseQuery string // inner stream selector + pipeline, without range window [T] } type rangeMetricSample struct { @@ -129,6 +130,7 @@ func parseOriginalRangeMetricSpec(logql string) (originalRangeMetricSpec, bool) if spec.Window < 0 { return originalRangeMetricSpec{}, false } + spec.BaseQuery = strings.TrimSpace(body[:bracketOpen]) unwrap := rangeMetricUnwrapRE.FindStringSubmatch(body) if len(unwrap) == 2 { @@ -290,11 +292,25 @@ func (p *Proxy) handleStatsCompatRange(w http.ResponseWriter, r *http.Request, o // (e.g. sum(rate({...} | json [w]))). For non-sliding windows (range == step), VL can // execute them natively — no manual decomposition needed. For sliding windows (range > // step), fall through to the manual path which implements correct per-step accumulation. + // + // Exception: queries with parser stages (| json, | logfmt, etc.) without an explicit + // "| drop __error__" opt-in must NOT use VL native stats for tumbling windows. Loki + // excludes parse-failed lines from metric aggregation; VL counts all lines. The manual + // path (collectRangeMetricSamples) preserves Loki's error-exclusion semantics. if strings.Contains(logsqlQuery, "| math ") { step, stepOk := parsePositiveStepDuration(r.FormValue("step")) origSpec, hasOrigSpec := parseOriginalRangeMetricSpec(originalLogql) if stepOk && hasOrigSpec && origSpec.Window > 0 && origSpec.Window <= step { - return false + spec, specOk := parseStatsCompatSpec(logsqlQuery) + // Parser stages without an explicit drop-error opt-in require the manual path + // to preserve Loki's error-exclusion semantics. Use origSpec.BaseQuery (the inner + // LogQL stream selector + pipeline without the range window) for the drop-error + // check — hasDropErrorOnlyPostParserStage requires the pipeline without outer + // aggregation or range window brackets. + if !specOk || !queryUsesParserStages(spec.BaseQuery) || hasDropErrorOnlyPostParserStage(origSpec.BaseQuery) { + return false + } + // Parser stage without drop-error — fall through to the manual path below. } } spec, ok := parseStatsCompatSpec(logsqlQuery) @@ -319,6 +335,13 @@ func (p *Proxy) handleStatsCompatRange(w http.ResponseWriter, r *http.Request, o // semantically equivalent for VL native stats — only range > step produces // overlapping sliding windows where VL tumbling-bucket stats diverges from LogQL. noSlidingOverlap := step > 0 && origSpec.Window > 0 && origSpec.Window <= step + // For tumbling windows, an explicit "| drop __error__" in the original LogQL opts in to + // VL's count-all semantics (parse failures counted). Use origSpec.BaseQuery — the inner + // pipeline without outer aggregation or range brackets — so hasDropErrorOnlyPostParserStage + // can correctly identify the drop-error clause. + if noSlidingOverlap && queryUsesParserStages(spec.BaseQuery) && hasOrigSpec && hasDropErrorOnlyPostParserStage(origSpec.BaseQuery) { + return false + } if !shouldUseManualRangeMetricCompat(spec.BaseQuery, manualFunc, noSlidingOverlap) { return false } @@ -388,6 +411,16 @@ func shouldUseManualRangeMetricCompat(baseQuery, manualFunc string, rangeEqualsS // natively supports inline filter pipelines and parser stages. switch manualFunc { case "rate", "bytes_rate", "count_over_time", "bytes_over_time": + // Parser stages require the slow log-fetch path: Loki excludes parse-failed lines + // from metric aggregation while VL native stats counts all lines. The drop-error + // opt-in check (hasDropErrorOnlyPostParserStage) only works on LogQL syntax; the + // baseQuery here is VL-translated syntax (| unpack_json etc.), so we conservatively + // route all parser-stage queries to the manual path. Bare parser metric queries + // (proxyBareParserMetricQueryRange) handle the drop-error fast path for the + // ungrouped case where the original LogQL is available. + if queryUsesParserStages(baseQuery) { + return true + } return !rangeEqualsStep } @@ -444,7 +477,10 @@ func (p *Proxy) proxyManualRangeMetricRange(w http.ResponseWriter, r *http.Reque case "__bytes__": statsAggFunc = "sum_len(_msg) as c" } - if statsAggFunc != "" { + // Mirror the parser-stage guard from shouldUseManualRangeMetricCompat: if the query + // uses parser stages, skip the stats_query_range fast path to preserve Loki's + // parse-error exclusion semantics (Loki excludes parse-failed lines; VL counts all). + if statsAggFunc != "" && !queryUsesParserStages(spec.BaseQuery) { hasStreamSentinel := false for _, g := range spec.GroupBy { if g == "_stream" { @@ -576,11 +612,12 @@ func (p *Proxy) collectRangeMetricHits( defer resp.Body.Close() if resp.StatusCode >= 400 { - body, _ := io.ReadAll(resp.Body) + body, _ := readBodyLimited(resp.Body, maxUpstreamErrorBodyBytes) return nil, fmt.Errorf("stats_query_range backend %d: %s", resp.StatusCode, strings.TrimSpace(string(body))) } - body, err := io.ReadAll(resp.Body) + const maxStatsResponseBytes = 64 << 20 // 64 MB + body, err := readBodyLimited(resp.Body, maxStatsResponseBytes) if err != nil { return nil, err } diff --git a/internal/proxy/range_metric_compat_test.go b/internal/proxy/range_metric_compat_test.go index e781161a..80a71902 100644 --- a/internal/proxy/range_metric_compat_test.go +++ b/internal/proxy/range_metric_compat_test.go @@ -106,11 +106,11 @@ func TestParseOriginalRangeMetricSpecRate(t *testing.T) { } func TestShouldUseManualRangeMetricCompat_ParserStageRate(t *testing.T) { - // Written for the 3-param signature introduced by the parser-stage guard removal. - // This test will fail to compile until shouldUseManualRangeMetricCompat drops - // the originalLogql parameter (Task 2). That is intentional — red-phase TDD. - // - // After Task 2: parser-stage rate queries obey only !rangeEqualsStep. + // shouldUseManualRangeMetricCompat receives the VL-translated base query (| unpack_json + // etc.), not the original LogQL. hasDropErrorOnlyPostParserStage requires LogQL syntax, + // so the drop-error fast-path cannot be checked here. All parser-stage queries go to + // the slow path at this level; the drop-error fast path for bare (ungrouped) parser + // metrics is handled separately in proxyBareParserMetricQueryRange. // Base query uses VL-translated syntax (| unpack_json, not | json). parserBaseQuery := `{app="api-gateway"} | unpack_json | status >= 400` plainBaseQuery := `{app="api-gateway"}` @@ -122,34 +122,34 @@ func TestShouldUseManualRangeMetricCompat_ParserStageRate(t *testing.T) { rangeEqualsStep bool wantManual bool // true = slow path, false = fast path }{ - // Parser stages + tumbling window (range==step): FAST PATH expected after change. + // Parser stages + tumbling window: SLOW PATH (preserve __error__ semantics). { - name: "rate_parser_tumbling_fast", + name: "rate_parser_tumbling_slow", baseQuery: parserBaseQuery, manualFunc: "rate", rangeEqualsStep: true, - wantManual: false, // fast path + wantManual: true, }, { - name: "count_over_time_parser_tumbling_fast", + name: "count_over_time_parser_tumbling_slow", baseQuery: parserBaseQuery, manualFunc: "count_over_time", rangeEqualsStep: true, - wantManual: false, + wantManual: true, }, { - name: "bytes_rate_parser_tumbling_fast", + name: "bytes_rate_parser_tumbling_slow", baseQuery: parserBaseQuery, manualFunc: "bytes_rate", rangeEqualsStep: true, - wantManual: false, + wantManual: true, }, { - name: "bytes_over_time_parser_tumbling_fast", + name: "bytes_over_time_parser_tumbling_slow", baseQuery: parserBaseQuery, manualFunc: "bytes_over_time", rangeEqualsStep: true, - wantManual: false, + wantManual: true, }, // Parser stages + sliding window: SLOW PATH preserved. { @@ -203,10 +203,63 @@ func TestShouldUseManualRangeMetricCompat_ParserStageRate(t *testing.T) { } } -func TestQueryRange_RateParserStageTumblingUsesStatsQueryRange(t *testing.T) { +func TestQueryRange_RateParserStageTumblingUsesSlowPath(t *testing.T) { // sum by (app) (rate({app="api-gateway"} | json | status >= 400 [5m])) with step=5m - // (range == step, tumbling window) must route to VL stats_query_range after the - // parser-stage guard is removed from shouldUseManualRangeMetricCompat. + // (range == step, tumbling window) WITHOUT "| drop __error__" must NOT route to VL + // stats_query_range: Loki excludes parse-failed lines; VL stats counts all lines. + // The slow log-fetch path preserves Loki's error-exclusion semantics. + base := time.Unix(1700000000, 0).UTC() + var slowPathCalled bool + + vlBackend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if err := r.ParseForm(); err != nil { + t.Fatalf("parse form: %v", err) + } + switch r.URL.Path { + case "/select/logsql/query": + slowPathCalled = true + w.Header().Set("Content-Type", "application/x-ndjson") + _, _ = fmt.Fprintf(w, + `{"_time":%q,"_msg":"ok","_stream":"{app=\"api-gateway\"}","app":"api-gateway","status":"404"}`+"\n", + base.Format(time.RFC3339Nano), + ) + case "/select/logsql/stats_query_range": + // stats_query_range MUST NOT be called for parser-stage query without drop-error. + t.Error("unexpected fast-path /select/logsql/stats_query_range call for parser-stage rate without | drop __error__") + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"data":{"resultType":"matrix","result":[]}}`)) + default: + if r.URL.Path != "/metrics" { + t.Logf("unhandled path: %s", r.URL.Path) + } + http.NotFound(w, r) + } + })) + defer vlBackend.Close() + + p := newGapTestProxy(t, vlBackend.URL) + params := url.Values{} + // step=300 == range=[5m]=300 → tumbling window, but parser stage without drop-error → slow path. + params.Set("query", `sum by (app) (rate({app="api-gateway"} | json | status >= 400 [5m]))`) + params.Set("start", strconv.FormatInt(base.Unix(), 10)) + params.Set("end", strconv.FormatInt(base.Add(30*time.Minute).Unix(), 10)) + params.Set("step", "300") + req := httptest.NewRequest(http.MethodGet, "/loki/api/v1/query_range?"+params.Encode(), nil) + rec := httptest.NewRecorder() + p.handleQueryRange(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String()) + } + if !slowPathCalled { + t.Error("expected slow-path /select/logsql/query to be called for parser-stage tumbling rate without | drop __error__") + } +} + +func TestQueryRange_RateParserStageDropErrTumblingUsesStatsQueryRange(t *testing.T) { + // sum by (app) (rate({app="api-gateway"} | json | drop __error__ [5m])) with step=5m + // (range == step, tumbling window) WITH "| drop __error__" opts in to VL count-all + // semantics and MUST route to VL stats_query_range fast path. base := time.Unix(1700000000, 0).UTC() var statsCalled bool @@ -220,8 +273,7 @@ func TestQueryRange_RateParserStageTumblingUsesStatsQueryRange(t *testing.T) { w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"data":{"resultType":"matrix","result":[{"metric":{"app":"api-gateway"},"values":[[1700000300,"0.5"]]}]}}`)) case "/select/logsql/query": - // Manual raw-log-fetch MUST NOT be called for tumbling window. - t.Error("unexpected slow-path /select/logsql/query call for tumbling-window parser-stage rate") + t.Error("unexpected slow-path /select/logsql/query call for drop-error tumbling-window parser-stage rate") w.Header().Set("Content-Type", "application/x-ndjson") default: if r.URL.Path != "/metrics" { @@ -234,8 +286,8 @@ func TestQueryRange_RateParserStageTumblingUsesStatsQueryRange(t *testing.T) { p := newGapTestProxy(t, vlBackend.URL) params := url.Values{} - // step=300 == range=[5m]=300 → rangeEqualsStep=true → tumbling window → fast path. - params.Set("query", `sum by (app) (rate({app="api-gateway"} | json | status >= 400 [5m]))`) + // step=300 == range=[5m]=300, "| drop __error__" opt-in → fast path. + params.Set("query", `sum by (app) (rate({app="api-gateway"} | json | drop __error__ [5m]))`) params.Set("start", strconv.FormatInt(base.Unix(), 10)) params.Set("end", strconv.FormatInt(base.Add(30*time.Minute).Unix(), 10)) params.Set("step", "300") @@ -247,7 +299,7 @@ func TestQueryRange_RateParserStageTumblingUsesStatsQueryRange(t *testing.T) { t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String()) } if !statsCalled { - t.Error("expected stats_query_range to be called for tumbling-window parser-stage rate query") + t.Error("expected stats_query_range to be called for drop-error tumbling-window parser-stage rate query") } var resp struct { Status string `json:"status"`