Skip to content

fix: sum without over an unwrap range function keeps a series per remaining label set, like Loki - #739

Merged
szibis merged 32 commits into
mainfrom
fix/unwrap-sum-without
Oct 18, 2026
Merged

szibis merged 32 commits into
mainfrom
fix/unwrap-sum-without

Conversation

@szibis

@szibis szibis commented Oct 12, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Gap semantics/unwrap-sum-without-collapses. Loki 3.7.7 keeps every label except pod for sum without (pod) (max_over_time({app="payment-service"} | logfmt amount | unwrap amount [5m])). The proxy answered a single series on query_range and query.

Cause. For <op> without (...) over an unwrap range function, the translator's last stats pipe had no by. VictoriaLogs returned one row, so the proxy's without step had no labels left to group by (Loki: pkg/logql/evaluator.go:451-466). The same applied to the stdvar branch and to tumbling rate windows.

Fix.

  • VictoriaLogs computes the range function per stream with native stats. The last stats pipe now keeps the stream.
  • The proxy then drops the excluded labels and folds the rows with the outer operator. A LogsQL by can't express "every label except X".
  • max_query_series is applied to the folded answer, which is what Loki counts (pkg/logql/engine.go:536-548, :575-590). Before this, any without query failed once the streams outnumbered the limit, even when the answer fit.
  • Over the limit, plain clients get Loki's 400 and Logs Drilldown gets the busiest series with Loki's warning. Multi-tenant answers keep __tenant_id__ and the merged answer is what gets counted.
  • The per-stream rows read before the fold are bounded by -manual-range-metric-row-limit when it is not below the tenant's effective max_query_series (-max-stats-query-series unless the tenant has its own override); past it the client reads the row-limit message naming the flag as a 400 (a rejection of the query), where the raw-row reads of the other routes answer 502; the 400 is a client error and does not count toward the LokiVLProxyHighErrorRate 5xx alert. The limits reference, the operations notes and the API reference state both. Which error a request hits is recorded on the request, not inferred from the two limits having the same value.
  • The operator of the without aggregation travels in the marker (__without__:<op>:pod), so a max, min, count or avg without folds with its own operator and not with a sum, also when a topk, bottomk, sort, stddev or stdvar wraps it (the wrapper is applied to the folded series), and a comparison around it. A label_replace around it is applied on query_range; on an instant query it adds no label, as without without (registered below). A binary expression folds each operand on its own.
  • A parser that names the keys it extracts (| logfmt amount, | logfmt amount, currency) makes a Loki series the stream plus those labels, less the unwrapped one. The inner stats of the without the proxy folds, and of that fold only, over such a pipeline now groups by exactly that (_stream, plus the named labels), where it grouped by _msg, which stands for the labels of a parser that extracts every key and split the series per line: sum without (pod) (max_over_time(... | logfmt amount | unwrap amount [5m])) answered a sum of per-line maxima (870,792.27 against Loki's 252,870.11 on generator data). A bare | logfmt or | json, label_format, drop, keep, unpack, an alias of a logfmt list (r="route", which creates no field) and the names the proxy reads specially (level, service_name) keep the previous grouping, and so does a key that is also a label of the stream selector ({app="x", pod=~".+"} | logfmt amount, pod): Loki names the parsed one pod_extracted, which the grouping would not, so that query keeps the fold's earlier grouping by the stream. A stream label the selector does not name cannot be seen by the translation (open row semantics/unwrap-sum-without-extracted-label-collision). The labels named by the list are carried on the rate route and on the sliding-window route of the fold too. Every other query is unchanged by this: the grouping is read only when the translator marks the query for the fold (the marker with an operator), and the sliding route adds the parsed labels to a series only for a request that folds. Sum, avg, count, max, min and topk of per-series functions, rate and count_over_time, a without that is not folded (topk, bottomk, stddev and stdvar, or under another aggregation), after an extraction list, on an instant query, a tumbling window and a sliding one, send VictoriaLogs the same LogsQL and answer the same as origin/main (TestExtractionListOutsideWithoutKeepsItsGrouping, whose golden table was produced on cd174a7d and passes there too). On generator data, with Loki 3.7.7 and VictoriaLogs v1.52.0, sum without (pod) over max_over_time and over rate with | logfmt amount and with | logfmt amount, currency (2 and 10 series), count without over max_over_time (118 and 150) and max without over max_over_time answer Loki's series and values at an instant, and rate on query_range answers the same series and last values. The over-time functions on query_range still read a step-sized bucket where Loki reads the sliding window (max without over max_over_time: 9,722 against Loki's 9,988 at the last step), the open row semantics/unwrap-over-time-range-windows.
  • The fold is limited to sum, min, max, avg and count that are the outermost aggregation of the query. topk, bottomk, stddev and stdvar with a without clause, and a without under another aggregation (max(sum without ...)), carry a marker without an operator: their translation, fold and series limit are what they were before this change, and the tests pin those answers (TestWithoutFoldIsLimitedToTheOperatorsItProves, and after an extraction list TestExtractionListOutsideWithoutKeepsItsGrouping: max(sum without (pod) (max_over_time(... | logfmt amount | unwrap amount [5m]))) answers main's 1691 on the fixture; the build that read the grouping there answered 358, and 45 of the 69 cases of the test then differ from main).

Why the proof uses | logfmt amount. With a bare | logfmt every payment line carries a unique tx_id, user_id, latency_ms and msg, so on Loki each line is a series of its own: about 6,300 series over 15 minutes and 19,600 over an hour for a rate, and Loki's Grafana capture never settles and its A/B timings reach the harness timeout. The proxy does not reproduce a parsed-label identity of that kind (open row semantics/unwrap-per-series-identity-raw-rows), and the over-time functions on query_range read a step-sized bucket where Loki reads the sliding window (open row semantics/unwrap-over-time-range-windows). With | logfmt amount the parser extracts only the unwrapped label, which Loki drops from the series identity, so the series is the stream: Loki answers two series, bounded, and the proxy answers the same two with identical timestamps and values at 15 minutes and at 1 hour. The fix-proof page, the other sum without pages and the A/B shapes W1 to W7 use that form.

Remaining differences. Shapes around without that are still wrong on origin/main and on this branch are registered, each with the proxy's answer and Loki's on generator data (measured against Loki 3.7.7, VictoriaLogs v1.52.0; the values of the fixture quoted in a row's text come from reading the v3.7.7 evaluator, not from a Loki run) and, where one exists, a unit test that reproduces it: sum without () over an over-time function on query_range (without-empty-grouping-range-collapses), a without over an inner sum by or count_over_time (without-over-inner-aggregation-collapses), a without under max or sum by (without-nested-under-aggregation), a comparison inside the aggregation (without-comparison-inside-ignored), topk/bottomk without (topk-bottomk-without-strips-label), label_replace around a vector at an instant (label-replace-instant-vector) and around a binary expression on query_range (label-replace-range-binary-marker).

Outside a without, the labels an extraction list names are not read yet, so sum(max_over_time(... | logfmt amount | unwrap amount [5m])), avg by, count by and topk of per-series functions read other series than Loki (open row semantics/unwrap-extraction-list-series-identity; on a 4-stream fixture with three routes, from an emulator run with a reference evaluator that follows the Loki sources and not against a real Loki: sum(max_over_time) 1691 against 358, count by (ns) over max_over_time 2 and 2 against 6 and 6, count by (ns) over count_over_time 2 and 2 against 6 and 6, avg by (ns) over avg_over_time 55.94 and 42.75 against 55.94 and 42.06; TestExtractionListSeriesIdentityStillDiffers pins both). Applying the grouping to those queries multiplies the series VictoriaLogs returns and is left to its own change.

After a bare parser, Loki's series also carry every extracted label, so it answers thousands of series. The proxy groups by stream; projected onto the stream labels, it matches Loki on instant queries and for rate on query_range. Range values of sum_over_time and max_over_time follow the open row semantics/unwrap-over-time-range-windows, and parser series identity is semantics/unwrap-per-series-identity-raw-rows. Two more shapes found with the extraction list are registered: sum by (level) over max_over_time takes the maximum of the group, not the sum of the per-series maxima (proxy 9,972 and 9,998 against Loki's 733,939 and 1,389,474; semantics/unwrap-sum-by-per-series-function-extraction-list), and a range function with unwrap and no outer aggregation exposes every parsed label although the parser names one key (2,035 series against Loki's 268 at an instant; semantics/unwrap-bare-range-exposes-parsed-labels).

Instant without answers also lack the detected_level=unknown label that Loki, and the proxy on query_range, add to a stream with no level label (semantics/without-instant-detected-level-unknown, open); the new tests drop that label before comparing instant answers.

e2e vs Loki 3.7.7

Isolated stack (own compose project), image built from head ee10d7dd, a fresh stack per VictoriaLogs version and per run, VictoriaLogs RestartCount 0 before and after every run. Every URL of the test environment points at that stack. Patterns are those of ci.yaml; the first row is the set the change touches.

run v1.52.0 v1.47.0 v1.40.0
TestSetup_IngestLogs, TestCompat_Unwrap.*, TestRangeMetric.*, TestGrafanaClickout_.* (34 tests, 789 passes, 3 skips that are the open cases of TestCompat_UnwrapConversionErrorParity) pass pass 8 tests, 57 subtests fail (65 failures)
same run on main (cd174a7d) not run (nothing failed) not run the same 65 fail; the 3 new tests (TestCompat_UnwrapSumWithoutKeepsSeries, ...ExtractionList, ...UnderAnotherAggregation) also fail, as they must
semantics group (45 tests, 897 passes) pass
core group (149 tests, 1539 passes) 1 test fails: TestProxyCompatibilitySurface/vmauth_proxy_drilldown_limits_backend_detection
same test on main fails the same way (the vmauth-fronted proxy reports v1.138.0 where v1.52. is expected)

The new tests pass on all three versions: TestCompat_UnwrapSumWithoutKeepsSeries (120 subtests: fails 94 of 120 on main), TestCompat_UnwrapSumWithoutExtractionList (every step of the rate range queries, exact values and series against Loki), TestCompat_UnwrapSumWithoutUnderAnotherAggregation and TestCompat_UnwrapSumWithoutUnderMaxStillDiffers (pins max over a folded without as the known difference).

On v1.40.0 the 8 failing tests (DrilldownFieldBreakdown, JSONDetectedLevel, JSONDetectedLevelSeriesLimit, JSONFilterPushdown, JSONLogsVolume, BareParserWindows, SlidingBucketEdges, TumblingAndShortWindows) fail with the same 57 subtests on main (compared by name with the run timestamps removed): the branch adds no failure on any version.

A/B (main cd174a7d vs branch ee10d7dd, VictoriaLogs v1.52.0 capped at 2 CPUs)

One fresh stack per run, targets interleaved per request, 5 runs (the first is cold), a shape the first pass moves is measured again with more runs, noise 30% and 100 ms. VictoriaLogs restarts 0 to 0 on every run. Saved results: bench/ab/results/2026-10-17-unwrapwithout-{1h,long,control-1h,control-long}.json.

python3 bench/ab/pr_smoke.py --base origin/main --head HEAD --ranges 1h --runs 5 --confirm-runs 7 \
  --selection <sel.json> --loki-mem 5g --vl-mem 4g --timeout 40 --out <dir>      # 1h, with Loki
python3 bench/ab/pr_smoke.py --base origin/main --head HEAD --ranges 6h,24h,7d --runs 5 --confirm-runs 5 \
  --selection <sel.json> --loki-mem 2g --vl-mem 5g --timeout 120 --out <dir>     # VictoriaLogs only, base capped at 3 runs

No degradation. The control set (20 shapes) is unchanged: 20/20 within noise at 1h with Loki, 37/37 at 6h and 24h, no new difference from Loki (topk rate by app differs from Loki on main too; json rate by level answers 400 on main, the branch and Loki).

Affected shapes. Nothing is slower beyond noise. At 1h all 9 are within noise. At 24h and 7d the branch is faster on the range max_over_time, sum_over_time and topk shapes (the inner stats now group by the stream and the named keys; main grouped by _msg):

shape 1h 6h 24h 7d
W1 sum without (pod) over max_over_time 15 / 15 ms 145 / 136 ms 690 / 534 ms 2.44 / 1.71 s (-30%)
W2 sum without (pod) over sum_over_time 15 / 17 ms 81 / 110 ms 736 / 543 ms (-26%) 2.44 / 1.70 s (-30%)
W3 sum without (pod) over rate 54 / 50 ms 297 / 276 ms 1.02 / 0.99 s 2.27 / 2.25 s
W4 max without (pod) over max_over_time 15 / 16 ms 90 / 106 ms 788 / 597 ms (-24%) 2.59 / 1.95 s (-25%)
W7 topk(5, sum without ...) 15 / 15 ms 85 / 110 ms 764 / 615 ms 2.55 / 1.80 s (-29%)
W8 sum without over count_over_time (production) 350 / 350 ms 8.38 / 7.38 s 502 / 502 9.31 / 8.62 s
W5, W6 instant sum without over max_over_time / rate 5 / 4 ms, 4 / 4 ms
W9 label_replace instant control 3 / 3 ms

Each cell is main / branch p50. The first pass flagged one cell as slower, W3 at 24h (1.01 against 1.39 s); measured again with more runs it reads 1.02 against 0.99 s, so it was noise. At 7d the windows of consecutive runs resolve to the same step-aligned buckets and the harness marks those warm timings as possibly cache-assisted; the cold timings agree in direction. The count_over_time production shape answers 502 at 24h on main and the branch alike (the response-size bound), unchanged.

Against Loki at 1h (same data, Loki answers within the timeout now that the shapes use | logfmt amount): W3 (rate), W5 and W6 (instant) and W9 match Loki's series and sums exactly. W1, W2, W4 and W7 (range max_over_time and sum_over_time) still differ, the same difference main has: the series set is now Loki's (two series, the error and info streams), but the values and the time axis follow the open row semantics/unwrap-over-time-range-windows. W8 is unanswered by Loki on this stack. The answers of W1, W2, W4, W5, W6 and W7 differ from main's by design (main answered one series).

benchstat

Test binaries of both trees run alternately, 10 rounds, GOMAXPROCS=4, GOMEMLIMIT=2GiB, benchstat main against branch ee10d7dd. The main tree is a detached cd174a7d worktree with internal/proxy/sum_without_bench_test.go copied in, uncommitted: the benchmark is new and uses only functions that exist on main.

go test -c -o <tree>.test ./internal/proxy      # and ./internal/translator, in each tree
<tree>.test -test.run '^$' -test.bench 'BenchmarkSumWithout_Query|BenchmarkProxy_QueryRange_CacheBypass|BenchmarkProxy_QueryRange_CacheHit|BenchmarkNoCache_StatsQuery|BenchmarkCollectRangeMetric_E2E_5000|BenchmarkAggregateManualWindow' -test.benchmem -test.count=1
<tree>.test -test.run '^$' -test.bench Benchmark -test.benchmem -test.count=1       # translator

The hot paths of every query (handleQueryRange with the cache hit and bypass, the stats query, the range-metric collector, the manual window aggregations) show no change in time, bytes or allocations (all ~, allocations identical). The translator (internal/translator, all benchmarks) shows no significant change in time, bytes or allocations (geomean time +1.7%, bytes and allocs +0.0%; one TranslateLogQL_MultiLabel B/op line reads -0.06% at p=0.013).

BenchmarkSumWithout_Query drives the handlers with the sum without shapes and controls against a stub VictoriaLogs. Both builds receive the same 500-stream stats answer, so it compares what the proxy does with an answer, not what each translation makes VictoriaLogs compute:

shape time (main / branch) B/op allocs/op
sum without over max_over_time, query_range 8.61 / 8.84 ms (~) 10.9 / 12.5 MiB (+14.6%) 182.7k / 188.3k (+3.1%)
sum without over rate, query_range 10.5 / 11.0 ms (~) 17.8 / 18.6 MiB (+4.5%) 204.9k / 209.4k (+2.2%)
instant sum without 1.84 / 1.96 ms (+6.4%, p=0.043) 2.97 / 3.64 MiB (+22.5%) 29.0k / 29.9k (+3.0%)
topk(5, sum without ...) 8.61 / 9.10 ms (~) 11.2 / 12.6 MiB (+12.6%) 184.4k / 190.0k (+3.1%)
controls: sum by, ungrouped rate, label_replace instant, binary all ~ (sum by +6.5% at p=0.035) all ~ all ~

The without shapes allocate 4% to 23% more bytes and 2% to 3% more allocations (the folded rows are kept per stream before the fold); the time differences are within the spread of this machine (5 to 15% per side), with two cells at p<0.05 (the instant sum without, and the sum by control whose bytes and allocations are identical, so it is noise). Output: bench739/benchstat-final-{proxy,translator,sumwithout}.txt.

Real Grafana range matrix

Head 98c963c2 (the code is that of ee10d7dd; the later commits add the A/B results and remove one Grafana page) against main cd174a7d. Isolated stack (bench/visual/stack.py up --hours 168: VictoriaLogs seeded for 7 days, Loki for the last 1.5 h), Grafana 13.2.1 with the pinned plugins, three datasources: main, this branch, Loki direct. 43 pages at 5m, 15m, 1h, 6h, 24h and 7d through Grafana's own query splitting (258 captures, all captured) plus Live tail; compare.py over 260 page × range rows. VictoriaLogs restarts 0 to 0 on every run.

  • Main and branch answer the same on every row except the two pages this change fixes, sum without (pod) (rate(... | logfmt amount | unwrap amount [$__auto])) and topk(5, ...) of it: main answers the single series {}, the branch the two stream series (level=error and level=info of payment-service), at every range. The count_over_time page is identical on both builds and on Loki.
  • Against Loki (5m, 15m and 1h, the ranges it holds): the two pages read improved, 0 rows regressed, and no row has a difference from Loki that main does not have. On the fixed pages the data query matches Loki's series, timestamps and values; the one request that still differs is Loki's index/stats, which Grafana sends with every query and which differs from main the same way, also on control pages (Loki's estimate counts 11,234 entries where its own count_over_time and the proxy count 21,892). At 6h, 24h and 7d Loki holds no data; the branch's difference is the one proven closer to Loki at the shorter ranges.
  • Fix proof (semantics/unwrap-sum-without-collapses): 4 captures improved, no failure.
  • dd-fields (unchanged by this change, equal on both builds at 24h): Grafana was killed by the host's memory limit twice while loading main's 7d trace_id/span_id breakdown (about 257k frames), so that one cell is from the first capture and did not settle.
  • The max_over_time page was removed from the spec: its series and values equal Loki's, but its samples sit one step early, the open row semantics/unwrap-over-time-range-windows (main has the same shift on sum(max_over_time ...)); it returns with the fix for that row.
  • Main, branch and Loki side by side (24h has no Loki data and shows main and branch):

explore-sum-without-rate 1h
explore-topk-sum-without 1h
explore-sum-without-rate 24h
explore-topk-sum-without 24h

  • Pixel difference main against branch: 6.2% on the two 24h pages whose answer changed (the series the fix adds).

Backend-safety load

Same stack and VictoriaLogs, one build after the other, each proxy restarted fresh: 8 concurrent clients for 240 s over 49 request types (W1 to W9 of bench/ab/shapes.json at 24h and 7d, and the control set), each request with a shifted window, /labels probed every second, proxy RSS sampled every 2 s, VictoriaLogs restart count and OOM flag read before and after.

main this branch
requests 858 905
200 / 400 / 502 803 / 34 / 21 849 / 35 / 21
p50 / p95 / p99 / max 1.54 / 7.58 / 11.84 / 17.1 s 1.51 / 7.23 / 9.38 / 11.9 s
proxy RSS peak 4,593 MB 3,463 MB
/labels 236 of 236 answered 200, p99 0.21 s 237 of 237 answered 200, p99 0.12 s
VictoriaLogs restarts / OOM 0 to 0 / false 0 to 0 / false

The errors are the same on both builds: sum without (pod) (count_over_time({env="production"}[1m])) at 24h answers 502 (the 64 MiB response bound; 200 at 7d), and json rate by level without a filter answers 400 with Loki's own JSONParserErr. Two earlier 300 s runs of each build agree (branch 1,254 and 1,325 requests, p99 10.3 and 10.6 s; main 1,128, p99 10.0 s); the RSS peaks of all runs overlap (branch 3.5 to 5.6 GB, main 3.3 to 4.6 GB). Most 7d requests are served from the cache (5 distinct windows), so the load on VictoriaLogs is mostly the 24h shapes.

Tests

  • TestSumWithoutDifferential: VictoriaLogs v1.52.0/v1.40.0, no parser and logfmt, rate and the over-time functions, sum/max/min outers and instant. 72 subtests fail without the fix.
  • TestSumWithoutSeriesLimitOnFoldedAnswer: 16 cases, including Drilldown, topk and two tenants.
  • TestWithoutKeepsSeriesInLastStatsPipe and TestKeepWithoutSeries (translator).
  • TestWithoutWrappedByAnOperator (the label_replace wrapper is pinned by the label it adds, the comparison by a threshold between the groups' values that drops some and keeps others), TestWithoutOffset (sum, max and count without over a range function with an offset), TestWithoutBinaryOperands, TestWithoutFoldRowBudgetIsNotASeriesLimit, TestWithoutMarkerCarriesTheAggregation, TestWithoutComparisonAboveEveryValueDropsEverySeries.
  • TestSumWithoutExtractionList: | logfmt amount and | logfmt amount, route, rate and the over-time functions, sum/max/min/count/avg outers, instant and query_range, VictoriaLogs v1.52.0 and v1.40.0, against Loki's evaluator over rows with a unique id in every line; each of the three code changes (inner grouping, rate grouping, sliding-window labels) fails it when reverted. TestParsedSeriesLabels and TestUnwrapInnerGroupingFollowsParsedSeries (translator) pin the parsers that name their keys (logfmt, json, regexp, pattern), the ones that fall back, the selector-label collision and that a query that is not the fold groups by _msg. TestExtractionListOutsideWithoutKeepsItsGrouping pins 23 other shapes (69 cases, with the raw-row requests too) to the LogsQL and the answers of cd174a7d; leaking the grouping to the general paths fails it and TestUnwrapInnerGroupingFollowsParsedSeries, and reverting the fold's grouping fails TestSumWithoutExtractionList. TestSumWithoutExtractedLabelCollision pins the two collision cases.
  • TestTopValueRankingIsBoundByTheTenantLimitWhenFolding: a folding request whose row budget is above the tenant's limit still hits the tenant's series limit on the busiest-values ranking.
  • TestWithoutFoldIsLimitedToTheOperatorsItProves and TestFoldingWithoutOp (translator, now with text after a wrapper, topk(5, sum without ...) > 10 and stddev(sum without ...) by (level)): the shapes the fold covers, and the answers of topk/bottomk without and of a without under another aggregation, pinned byte for byte to the answers before this change.
  • TestWithoutFoldRowBudgetError: the row-budget error is told by the request, with equal and unequal limits, and the tenant's own limit check at the budget's value is Loki's message.
  • TestSeriesLimit_SlidingWindowStatsKeepsTheStreamsOfAFoldingRequest: the sliding window evaluator leaves the tenant's limit to the fold. It calls the evaluator directly: no without query is routed there today.
  • TestWithoutShapesStillDifferFromLoki, TestInstantLabelReplaceOnAVectorAddsNoLabel and TestRangeLabelReplaceOfABinaryExpressionSendsTheMarker reproduce the open rows below.
  • TestInstantAggregationFailureIsRedacted pins the status, envelope and redaction of a failing instant aggregation (without, label_replace, label_join).

Capped unit run (GOMEMLIMIT=3GiB GOMAXPROCS=4 go test -short -p 1 ./..., peak RSS 1.55 GB), -race on the touched tests (GOMEMLIMIT=2GiB), go vet (also -tags e2e), golangci-lint, gofmt -s, the conformance gate, the changelog gate and the CI script tests pass.

Registry

semantics/unwrap-sum-without-collapses is fixed (#739), with TestCompat_UnwrapSumWithoutKeepsSeries, TestSumWithoutDifferential and TestSumWithoutSeriesLimitOnFoldedAnswer as its proofs. limits/series-limit-error and series-limits-and-partial-results describe the limit on the folded answer. The A/B set unwrap-sum-without and three Grafana pages (explore-sum-without-rate, the fix proof, explore-topk-sum-without and explore-sum-without-count) are registered.

Changed rows: semantics/unwrap-sum-without-collapses (request is the bounded rate over | logfmt amount; the text states the extraction-list grouping, which is read for the fold only, and the collision fallback), semantics/unwrap-count-avg-without-after-parser (right after an extraction list, open for a bare parser; the request is an instant count without over a bare | logfmt [1m], 415 series on Loki against the proxy's one), and the requests of the open rows around without below (not the two quantile rows), which use the generator's level label and a bounded parser, with the values measured against Loki.

New open rows, found on the way:

  • count/avg without over unwrap after a parser.
  • An ungrouped sum of a quantile answering [].
  • sum by over quantile taking the group's quantile.
  • semantics/without-empty-grouping-range-collapses: sum without () (max_over_time(... | unwrap ...)) on query_range answers {} (1 series on the fixture), Loki keeps the 8 streams (without () drops no label).
  • semantics/without-over-inner-aggregation-collapses: sum without (pod) (sum by (pod, level) (rate(...))) and over count_over_time, range and instant, answers {}; Loki a series per level.
  • semantics/without-nested-under-aggregation: max(sum without (pod) (...)) and sum by (level) (sum without (pod) (...)) answer the total, Loki the largest per-level sum and a series per level.
  • semantics/without-comparison-inside-ignored: sum without (pod) (rate(...) > 0.33) sums every series; Loki only the ones above the threshold (on origin/main an instant query also answers {}).
  • semantics/topk-bottomk-without-strips-label: topk/bottomk without (pod) (1, ...) strips the label and answers other values; Loki keeps the top/bottom series with all labels.
  • semantics/label-replace-instant-vector: label_replace around a vector at an instant adds no label (applyLabelReplace handles matrices only; semantics/label-replace-new-label covers query_range).
  • semantics/label-replace-range-binary-marker: label_replace around a binary expression on query_range sends the __binary__ marker to VictoriaLogs: the proxy answers an empty matrix on v1.52.0 (Loki three series) and 502 from a backend that rejects the query.
  • semantics/without-instant-detected-level-unknown: an instant without answer lacks detected_level=unknown on a stream with no level label.
  • semantics/unwrap-extraction-list-series-identity: sum, avg, count, max, min and topk of per-series functions, and count_over_time and rate, after an extraction list read other series than Loki outside a without (figures above, from the emulator).
  • semantics/unwrap-sum-without-extracted-label-collision: a parsed key that is also a stream label is not named <key>_extracted after the fold.
  • semantics/unwrap-sum-by-per-series-function-extraction-list: sum by (level) (max_over_time(... | logfmt amount | unwrap amount [5m])) answers the maximum of the level, not the sum of the per-series maxima.
  • semantics/unwrap-bare-range-exposes-parsed-labels: rate(... | logfmt amount | unwrap amount [5m]) exposes every parsed label (2,035 series against Loki's 268).

The two quantile rows (the second and third above) are addressed in #731 (open). The series-limit rows are updated, and the limits reference states the row-budget error as a 400 next to the 502 of the raw-row reads.

szibis added a commit that referenced this pull request Oct 12, 2026
@github-actions github-actions Bot added size/XL Extra large change scope/translator LogQL translator scope/proxy Proxy core scope/docs Documentation scope/tests Tests bugfix Bug fix and removed size/XL Extra large change labels Oct 12, 2026
@github-actions

github-actions Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

⚪ Performance A/B: no change beyond noise

120 shape×range — 120 ⚪ same · results vs Loki: 98 same, 0 new difference(s), 22 pre-existing

Every shape is within noise of the base build (±30% and 100 ms).

120 unchanged shape×range (within noise)
shape range base p50 PR p50 change Loki p50 PR ÷ Loki result vs Loki
⚪ bytes_over_time by app 1h 42 ms 40 ms ⚪ -5% 56 ms 0.71× ✅ same
⚪ drilldown service landing 1h 20 ms 17 ms ⚪ -15% 17 ms 1.00× ✅ same
⚪ instant json filtered instant 228 ms 241 ms ⚪ +6% 50 ms 4.82× ✅ same
⚪ instant json volume instant 272 ms 284 ms ⚪ +4% 63 ms 4.51× ✅ same
⚪ instant ungrouped rate instant 5 ms 5 ms ⚪ +0% 49 ms 0.10× ✅ same
⚪ json rate by level (no filter, no drop) 1h 400 400 ⚪ +0% 400 — ✅ same
⚪ json volume no filters 1h 101 ms 102 ms ⚪ +1% 2 ms 51× ✅ same
⚪ logfmt count by level, filter, drop 1h 242 ms 246 ms ⚪ +2% 33 ms 7.45× ✅ same
⚪ logfmt volume 1h 157 ms 161 ms ⚪ +3% 39 ms 4.13× ✅ same
⚪ logs plain 1h 40 ms 40 ms ⚪ +0% 30 ms 1.33× ✅ same
⚪ logs | json 1h 58 ms 58 ms ⚪ +0% 43 ms 1.35× ✅ same
⚪ logs | logfmt 1h 92 ms 89 ms ⚪ -3% 36 ms 2.47× ✅ same
⚪ stream-label count by app 1h 14 ms 13 ms ⚪ -7% 26 ms 0.50× ✅ same
⚪ topk rate by app 1h 24 ms 26 ms ⚪ +8% 55 ms 0.47× ➖ differs (as base)
⚪ ungrouped rate 1h 6 ms 6 ms ⚪ +0% 51 ms 0.12× ✅ same
⚪ DB1 label breakdown pod 1h 45 ms 46 ms ⚪ +2% 67 ms 0.69× ✅ same
⚪ DB2 label breakdown namespace 1h 12 ms 10 ms ⚪ -17% 22 ms 0.45× ✅ same
⚪ DB3 json field breakdown user_id 1h 27 ms 25 ms ⚪ -7% 17 ms 1.47× ✅ same
⚪ DB4 json field breakdown batch_id 1h 54 ms 53 ms ⚪ -2% 31 ms 1.71× ✅ same
⚪ X1 json filter level_extracted, by level 1h 212 ms 206 ms ⚪ -3% 28 ms 7.36× ✅ same
⚪ X2 json group by level_extracted 1h 164 ms 167 ms ⚪ +2% 28 ms 5.96× ✅ same
⚪ X3 json rate, level_extracted equality 1h 158 ms 154 ms ⚪ -3% 23 ms 6.70× ✅ same
⚪ X4 json bytes, level_extracted regexp 1h 215 ms 220 ms ⚪ +2% 59 ms 3.73× ✅ same
⚪ X5 logfmt filter level_extracted, by level 1h 381 ms 382 ms ⚪ +0% 32 ms 12× ✅ same
⚪ E1 logs, json parser over stream labels 1h 65 ms 67 ms ⚪ +3% 41 ms 1.63× ✅ same
⚪ E2 logs, logfmt parser over stream labels 1h 43 ms 39 ms ⚪ -9% 33 ms 1.18× ✅ same
⚪ E3 logs, json, existence filter on level_extracted 1h 100 ms 81 ms ⚪ -19% 52 ms 1.56× ✅ same
⚪ E4 logs, logfmt, existence filter on level_extracted 1h 51 ms 57 ms ⚪ +12% 40 ms 1.43× ✅ same
⚪ E5 logs, json, drop level_extracted 1h 70 ms 67 ms ⚪ -4% 39 ms 1.72× ✅ same
⚪ E6 logfmt count by level_extracted 1h 26 ms 25 ms ⚪ -4% 5 ms 5.00× ✅ same
⚪ E7 json count by level_extracted 1h 77 ms 78 ms ⚪ +1% 10 ms 7.80× ✅ same
⚪ E8 instant logfmt by level_extracted instant 5 ms 4 ms ⚪ -20% 7 ms 0.57× ✅ same
⚪ A explore volume, 2 filters, drop 1h 82 ms 83 ms ⚪ +1% 27 ms 3.07× ✅ same
⚪ B grouped sum, filter, no drop 1h 63 ms 59 ms ⚪ -6% 24 ms 2.46× ✅ same
⚪ C ungrouped sum, filter, no drop 1h 57 ms 57 ms ⚪ +0% 23 ms 2.48× ✅ same
⚪ D volume, no filters 1h 107 ms 116 ms ⚪ +8% 31 ms 3.74× ✅ same
⚪ E field breakdown pipeline (explore) 1h 58 ms 61 ms ⚪ +5% 24 ms 2.54× ✅ same
⚪ F field breakdown service_version (explore) 1h 75 ms 80 ms ⚪ +7% 28 ms 2.86× ✅ same
⚪ G field breakdown pipeline (drilldown) 1h 24 ms 23 ms ⚪ -4% 16 ms 1.44× ✅ same
⚪ H field breakdown service_version (drilldown) 1h 45 ms 48 ms ⚪ +7% 17 ms 2.82× ✅ same
⚪ I grafana volume of B 1h 55 ms 56 ms ⚪ +2% 23 ms 2.43× ✅ same
⚪ O2 explore log volume (Grafana form), underscore structured-metadata filter 1h 38 ms 38 ms ⚪ +0% 24 ms 1.58× ✅ same
⚪ refresh label values env 1h 14 ms 14 ms ⚪ +0% 10 ms 1.40× ✅ same
⚪ refresh label values service_name 1h 5 ms 6 ms ⚪ +20% 11 ms 0.55× ✅ same
⚪ refresh labels (label browser) 1h 9 ms 11 ms ⚪ +22% 2 ms 5.50× ✅ same
⚪ refresh labels with selector 1h 12 ms 10 ms ⚪ -17% 11 ms 0.91× ✅ same
⚪ DL1 drilldown field breakdown json-expr, level filter 1h 282 ms 269 ms ⚪ -5% 16 ms 17× ✅ same
⚪ DL2 drilldown field breakdown, level + stream-label filter before json 1h 28 ms 27 ms ⚪ -4% 5 ms 5.40× ✅ same
⚪ DL3 drilldown logfmt field breakdown, level filter 1h 332 ms 338 ms ⚪ +2% 52 ms 6.50× ➖ differs (as base)
⚪ DL4 instant count, level filter, json+logfmt, 1h instant 275 ms 277 ms ⚪ +1% 785 ms 0.35× ✅ sum ±0.27%
⚪ DL6 explore volume, level filter before json 1h 302 ms 303 ms ⚪ +0% 27 ms 11× ✅ sum ±0.27%
⚪ OR1 logs, or alternatives 1h 49 ms 47 ms ⚪ -4% 45 ms 1.04× ✅ same
⚪ OR2 logs, negated or alternatives 1h 45 ms 45 ms ⚪ +0% 30 ms 1.50× ✅ same
⚪ OR3 logs, regexp or alternatives 1h 36 ms 34 ms ⚪ -6% 47 ms 0.72× ✅ same
⚪ OR4 count by level, or alternatives with parser 1h 45 ms 48 ms ⚪ +7% 22 ms 2.18× ✅ same
⚪ OR5 rate, negated regexp or alternatives 1h 33 ms 36 ms ⚪ +9% 55 ms 0.65× ✅ same
⚪ OR6 instant, or alternatives instant 5 ms 5 ms ⚪ +0% 42 ms 0.12× ✅ same
⚪ P1 api-gateway empty pattern 1h 13 ms 13 ms ⚪ +0% 5 ms 2.60× ✅ same
⚪ P2 api-gateway not empty pattern, line filter 1h 48 ms 47 ms ⚪ -2% 15 ms 3.13× ✅ same
⚪ P3 nginx count_over_time, pattern with spaces 1h 7 ms 8 ms ⚪ +14% 5 ms 1.60× ✅ same
⚪ P4 nginx count_over_time, GET request pattern 1h 9 ms 8 ms ⚪ -11% 5 ms 1.60× ✅ same
⚪ A1 nginx include GET, log query 1h 12 ms 14 ms ⚪ +17% 8 ms 1.75× ✅ same
⚪ A2 nginx exclude GET, log query 1h 10 ms 11 ms ⚪ +10% 8 ms 1.38× ✅ same
⚪ A3 nginx include GET, count_over_time 1h 1 ms 1 ms ⚪ +0% 2 ms 0.50× ✅ same
⚪ A4 nginx exclude GET, count_over_time 1h 12 ms 11 ms ⚪ -8% 5 ms 2.20× ✅ same
⚪ A5 drilldown exclude, volume by detected_level 1h 15 ms 15 ms ⚪ +0% 5 ms 3.00× ✅ same
⚪ A6 nginx include ends on a literal, log query 1h 8 ms 8 ms ⚪ +0% 4 ms 2.00× ✅ same
⚪ A7 nginx exclude ends on a literal, log query 1h 11 ms 11 ms ⚪ +0% 9 ms 1.22× ✅ same
⚪ A8 nginx include two literals, count_over_time 1h 14 ms 15 ms ⚪ +7% 5 ms 3.00× ✅ same
⚪ A9 nginx include two literals, ends on a literal, count_over_time 1h 13 ms 13 ms ⚪ +0% 4 ms 3.25× ✅ same
⚪ P1 logs, json, level equals 1h 74 ms 73 ms ⚪ -1% 63 ms 1.16× ✅ same
⚪ P2 logs, logfmt, level not equal 1h 17 ms 17 ms ⚪ +0% 4 ms 4.25× ✅ same
⚪ P3 logs, json, level existence 1h 73 ms 78 ms ⚪ +7% 53 ms 1.47× ✅ same
⚪ P4 logfmt count by level 1h 18 ms 18 ms ⚪ +0% 5 ms 3.60× ✅ same
⚪ P5 json count by level 1h 45 ms 44 ms ⚪ -2% 10 ms 4.40× ✅ same
⚪ P6 json count, level filter 1h 9 ms 9 ms ⚪ +0% 9 ms 1.00× ✅ same
⚪ P7 json rate by level, level regexp 1h 11 ms 10 ms ⚪ -9% 9 ms 1.11× ✅ same
⚪ P8 json count by a name that is no stream label 1h 48 ms 44 ms ⚪ -8% 9 ms 4.89× ✅ same
⚪ P9 instant logfmt by level instant 4 ms 5 ms ⚪ +25% 7 ms 0.71× ✅ same
⚪ E1 json duration_ms sum by service 1h 45 ms 47 ms ⚪ +4% 10 ms 4.70× ➖ differs (as base)
⚪ E2 json duration_ms p95 by service 1h 234 ms 216 ms ⚪ -8% 10 ms 22× ✅ same
⚪ E3 logfmt latency_ms rate, production 1h 267 ms 276 ms ⚪ +3% 76 ms 3.63× ➖ differs (as base)
⚪ E4 logfmt size_bytes max by service 1h 26 ms 24 ms ⚪ -8% 9 ms 2.67× ➖ differs (as base)
⚪ E5 json duration_ms sum, instant instant 20 ms 21 ms ⚪ +5% 18 ms 1.17× ✅ same
⚪ E6 logfmt ttl unit string (Loki 400) 1h 400 400 ⚪ +3% 400 — ✅ same
⚪ E7 json hexadecimal trace_id (Loki 400) 1h 400 400 ⚪ +3% 400 — ✅ same
⚪ E8 json duration_ms sum by service, namespace prod 1h 102 ms 101 ms ⚪ -1% 54 ms 1.87× ➖ differs (as base)
⚪ E9 logfmt latency_ms sum by service, namespace prod 1h 170 ms 169 ms ⚪ -1% 59 ms 2.86× ➖ differs (as base)
⚪ W1 sum without pod, max_over_time 1h 29 ms 28 ms ⚪ -3% 12 ms 2.33× ➖ differs (as base)
⚪ W2 sum without pod, sum_over_time 1h 31 ms 27 ms ⚪ -13% 11 ms 2.45× ➖ differs (as base)
⚪ W3 sum without pod, rate 1h 77 ms 76 ms ⚪ -1% 11 ms 6.91× ✅ same
⚪ W4 max without pod, max_over_time 1h 29 ms 27 ms ⚪ -7% 12 ms 2.25× ➖ differs (as base)
⚪ W5 sum without pod, max_over_time, instant instant 7 ms 5 ms ⚪ -29% 10 ms 0.50× ✅ same
⚪ W6 sum without pod, rate, instant instant 4 ms 6 ms ⚪ +50% 10 ms 0.60× ✅ same
⚪ W7 topk 5 of sum without pod 1h 32 ms 28 ms ⚪ -12% 12 ms 2.33× ➖ differs (as base)
⚪ W8 sum without pod, count_over_time (production) 1h 489 ms 524 ms ⚪ +7% 0 — ➖ differs (as base)
⚪ W9 label_replace control, instant instant 3 ms 2 ms ⚪ -33% 46 ms 0.04× ✅ same
⚪ U1 logfmt amount rate 1h 21 ms 24 ms ⚪ +14% 9 ms 2.67× ✅ same
⚪ U2 json duration_ms rate 1h 49 ms 48 ms ⚪ -2% 16 ms 3.00× ✅ same
⚪ U3 json duration_ms rate, error filter 1h 49 ms 45 ms ⚪ -8% 18 ms 2.50× ✅ same
⚪ U4 logfmt latency_ms rate, production 1h 185 ms 189 ms ⚪ +2% 75 ms 2.52× ➖ differs (as base)
⚪ U5 logfmt amount rate, instant instant 6 ms 4 ms ⚪ -33% 8 ms 0.50× ✅ same
⚪ U6 json duration_ms rate, instant instant 9 ms 9 ms ⚪ +0% 15 ms 0.60× ✅ same
⚪ U7 json duration_ms sum_over_time, instant instant 11 ms 11 ms ⚪ +0% 15 ms 0.73× ✅ same
⚪ U8 logfmt amount rate by service (grouped) 1h 23 ms 26 ms ⚪ +13% 9 ms 2.89× ✅ same
⚪ U9 logfmt amount rate_counter (folded per series) 1h 149 ms 145 ms ⚪ -3% 21 ms 6.90× ➖ differs (as base)
⚪ U1 json duration_ms sum by service 1h 42 ms 47 ms ⚪ +12% 9 ms 5.22× ➖ differs (as base)
⚪ U10 instant sum_over_time unwrap instant 20 ms 18 ms ⚪ -10% 18 ms 1.00× ✅ same
⚪ U11 instant max_over_time missing label instant 3 ms 3 ms ⚪ +0% 10 ms 0.30× ✅ same
⚪ U12 json duration_ms over every service 1h 124 ms 123 ms ⚪ -1% 26 ms 4.73× ➖ differs (as base)
⚪ U13 logfmt latency_ms over every service 1h 277 ms 272 ms ⚪ -2% 35 ms 7.77× ➖ differs (as base)
⚪ U14 duration() of a unit string 1h 25 ms 23 ms ⚪ -8% 5 ms 4.60× ➖ differs (as base)
⚪ U2 json duration_ms max by service 1h 52 ms 53 ms ⚪ +2% 24 ms 2.21× ➖ differs (as base)
⚪ U3 json duration_ms sliding sum 1h 45 ms 43 ms ⚪ -4% 21 ms 2.05× ➖ differs (as base)
⚪ U4 logfmt size_bytes sum by service 1h 21 ms 24 ms ⚪ +14% 5 ms 4.80× ➖ differs (as base)
⚪ U5 logfmt ttl unit string (no sample) 1h 21 ms 21 ms ⚪ +0% 5 ms 4.20× ✅ same
⚪ U6 missing label, no parser 1h 13 ms 12 ms ⚪ -8% 7 ms 1.71× ✅ same
⚪ U7 missing label, ungrouped sum 1h 13 ms 12 ms ⚪ -8% 7 ms 1.71× ✅ same
⚪ U8 rate of an unwrapped label 1h 46 ms 46 ms ⚪ +0% 10 ms 4.60× ✅ same
⚪ U9 bytes() conversion 1h 28 ms 29 ms ⚪ +4% 8 ms 3.62× ➖ differs (as base)
Cold (first-run) timings — what a user opening a panel sees
shape range base cold PR cold Loki cold PR ÷ Loki cold
bytes_over_time by app 1h 39 ms 40 ms 473 ms 0.08×
drilldown service landing 1h 23 ms 17 ms 454 ms 0.04×
instant json filtered instant 249 ms 231 ms 50 ms 4.62×
instant json volume instant 271 ms 271 ms 62 ms 4.37×
instant ungrouped rate instant 9 ms 5 ms 46 ms 0.11×
json rate by level (no filter, no drop) 1h 400 400 400 —
json volume no filters 1h 103 ms 105 ms 2 ms 52.50×
logfmt count by level, filter, drop 1h 244 ms 246 ms 721 ms 0.34×
logfmt volume 1h 115 ms 117 ms 752 ms 0.16×
logs plain 1h 58 ms 39 ms 32 ms 1.22×
logs | json 1h 55 ms 54 ms 57 ms 0.95×
logs | logfmt 1h 90 ms 84 ms 34 ms 2.47×
stream-label count by app 1h 14 ms 16 ms 414 ms 0.04×
topk rate by app 1h 25 ms 25 ms 492 ms 0.05×
ungrouped rate 1h 6 ms 8 ms 470 ms 0.02×
DB1 label breakdown pod 1h 66 ms 44 ms 597 ms 0.07×
DB2 label breakdown namespace 1h 9 ms 8 ms 424 ms 0.02×
DB3 json field breakdown user_id 1h 28 ms 27 ms 418 ms 0.06×
DB4 json field breakdown batch_id 1h 52 ms 55 ms 432 ms 0.13×
X1 json filter level_extracted, by level 1h 1.98 s 1.96 s 556 ms 3.53×
X2 json group by level_extracted 1h 174 ms 174 ms 542 ms 0.32×
X3 json rate, level_extracted equality 1h 157 ms 151 ms 396 ms 0.38×
X4 json bytes, level_extracted regexp 1h 220 ms 218 ms 471 ms 0.46×
X5 logfmt filter level_extracted, by level 1h 370 ms 371 ms 598 ms 0.62×
E1 logs, json parser over stream labels 1h 87 ms 64 ms 39 ms 1.64×
E2 logs, logfmt parser over stream labels 1h 40 ms 44 ms 32 ms 1.38×
E3 logs, json, existence filter on level_extracted 1h 87 ms 82 ms 92 ms 0.89×
E4 logs, logfmt, existence filter on level_extracted 1h 55 ms 48 ms 35 ms 1.37×
E5 logs, json, drop level_extracted 1h 64 ms 75 ms 38 ms 1.97×
E6 logfmt count by level_extracted 1h 30 ms 25 ms 32 ms 0.78×
E7 json count by level_extracted 1h 774 ms 767 ms 115 ms 6.67×
E8 instant logfmt by level_extracted instant 5 ms 4 ms 7 ms 0.57×
A explore volume, 2 filters, drop 1h 798 ms 804 ms 822 ms 0.98×
B grouped sum, filter, no drop 1h 78 ms 70 ms 370 ms 0.19×
C ungrouped sum, filter, no drop 1h 66 ms 68 ms 421 ms 0.16×
D volume, no filters 1h 107 ms 106 ms 547 ms 0.19×
E field breakdown pipeline (explore) 1h 61 ms 63 ms 405 ms 0.16×
F field breakdown service_version (explore) 1h 729 ms 716 ms 398 ms 1.80×
G field breakdown pipeline (drilldown) 1h 33 ms 24 ms 404 ms 0.06×
H field breakdown service_version (drilldown) 1h 75 ms 69 ms 390 ms 0.18×
I grafana volume of B 1h 67 ms 61 ms 358 ms 0.17×
O2 explore log volume (Grafana form), underscore structured-metadata filter 1h 106 ms 92 ms 377 ms 0.24×
refresh label values env 1h 53 ms 49 ms 10 ms 4.90×
refresh label values service_name 1h 18 ms 19 ms 11 ms 1.73×
refresh labels (label browser) 1h 16 ms 3 ms 2 ms 1.50×
refresh labels with selector 1h 43 ms 43 ms 11 ms 3.91×
DL1 drilldown field breakdown json-expr, level filter 1h 284 ms 273 ms 359 ms 0.76×
DL2 drilldown field breakdown, level + stream-label filter before json 1h 48 ms 48 ms 19 ms 2.53×
DL3 drilldown logfmt field breakdown, level filter 1h 360 ms 347 ms 667 ms 0.52×
DL4 instant count, level filter, json+logfmt, 1h instant 288 ms 283 ms 782 ms 0.36×
DL6 explore volume, level filter before json 1h 589 ms 574 ms 470 ms 1.22×
OR1 logs, or alternatives 1h 66 ms 48 ms 80 ms 0.60×
OR2 logs, negated or alternatives 1h 49 ms 44 ms 31 ms 1.42×
OR3 logs, regexp or alternatives 1h 35 ms 37 ms 47 ms 0.79×
OR4 count by level, or alternatives with parser 1h 72 ms 79 ms 317 ms 0.25×
OR5 rate, negated regexp or alternatives 1h 32 ms 36 ms 467 ms 0.08×
OR6 instant, or alternatives instant 5 ms 5 ms 42 ms 0.12×
P1 api-gateway empty pattern 1h 28 ms 14 ms 50 ms 0.28×
P2 api-gateway not empty pattern, line filter 1h 49 ms 49 ms 15 ms 3.27×
P3 nginx count_over_time, pattern with spaces 1h 8 ms 7 ms 17 ms 0.41×
P4 nginx count_over_time, GET request pattern 1h 8 ms 9 ms 21 ms 0.43×
A1 nginx include GET, log query 1h 27 ms 14 ms 9 ms 1.56×
A2 nginx exclude GET, log query 1h 10 ms 9 ms 8 ms 1.12×
A3 nginx include GET, count_over_time 1h 1 ms 1 ms 2 ms 0.50×
A4 nginx exclude GET, count_over_time 1h 13 ms 11 ms 30 ms 0.37×
A5 drilldown exclude, volume by detected_level 1h 24 ms 31 ms 20 ms 1.55×
A6 nginx include ends on a literal, log query 1h 9 ms 8 ms 15 ms 0.53×
A7 nginx exclude ends on a literal, log query 1h 12 ms 10 ms 9 ms 1.11×
A8 nginx include two literals, count_over_time 1h 14 ms 14 ms 17 ms 0.82×
A9 nginx include two literals, ends on a literal, count_over_time 1h 13 ms 12 ms 16 ms 0.75×
P1 logs, json, level equals 1h 98 ms 100 ms 50 ms 2.00×
P2 logs, logfmt, level not equal 1h 17 ms 17 ms 24 ms 0.71×
P3 logs, json, level existence 1h 72 ms 71 ms 42 ms 1.69×
P4 logfmt count by level 1h 19 ms 19 ms 28 ms 0.68×
P5 json count by level 1h 53 ms 50 ms 117 ms 0.43×
P6 json count, level filter 1h 10 ms 9 ms 105 ms 0.09×
P7 json rate by level, level regexp 1h 10 ms 10 ms 111 ms 0.09×
P8 json count by a name that is no stream label 1h 53 ms 50 ms 97 ms 0.52×
P9 instant logfmt by level instant 3 ms 3 ms 7 ms 0.43×
E1 json duration_ms sum by service 1h 57 ms 44 ms 121 ms 0.36×
E2 json duration_ms p95 by service 1h 224 ms 224 ms 132 ms 1.70×
E3 logfmt latency_ms rate, production 1h 270 ms 280 ms 658 ms 0.43×
E4 logfmt size_bytes max by service 1h 24 ms 24 ms 182 ms 0.13×
E5 json duration_ms sum, instant instant 21 ms 21 ms 18 ms 1.17×
E6 logfmt ttl unit string (Loki 400) 1h 400 400 400 —
E7 json hexadecimal trace_id (Loki 400) 1h 400 400 400 —
E8 json duration_ms sum by service, namespace prod 1h 102 ms 102 ms 381 ms 0.27×
E9 logfmt latency_ms sum by service, namespace prod 1h 168 ms 171 ms 462 ms 0.37×
W1 sum without pod, max_over_time 1h 49 ms 36 ms 61 ms 0.59×
W2 sum without pod, sum_over_time 1h 41 ms 26 ms 55 ms 0.47×
W3 sum without pod, rate 1h 75 ms 85 ms 63 ms 1.35×
W4 max without pod, max_over_time 1h 37 ms 27 ms 65 ms 0.42×
W5 sum without pod, max_over_time, instant instant 5 ms 6 ms 9 ms 0.67×
W6 sum without pod, rate, instant instant 4 ms 7 ms 10 ms 0.70×
W7 topk 5 of sum without pod 1h 31 ms 29 ms 63 ms 0.46×
W8 sum without pod, count_over_time (production) 1h 491 ms 514 ms 0 —
W9 label_replace control, instant instant 3 ms 4 ms 65 ms 0.06×
U1 logfmt amount rate 1h 36 ms 23 ms 41 ms 0.56×
U2 json duration_ms rate 1h 47 ms 48 ms 102 ms 0.47×
U3 json duration_ms rate, error filter 1h 49 ms 49 ms 125 ms 0.39×
U4 logfmt latency_ms rate, production 1h 187 ms 206 ms 701 ms 0.29×
U5 logfmt amount rate, instant instant 6 ms 4 ms 10 ms 0.40×
U6 json duration_ms rate, instant instant 8 ms 8 ms 16 ms 0.50×
U7 json duration_ms sum_over_time, instant instant 11 ms 11 ms 17 ms 0.65×
U8 logfmt amount rate by service (grouped) 1h 24 ms 23 ms 39 ms 0.59×
U9 logfmt amount rate_counter (folded per series) 1h 149 ms 150 ms 267 ms 0.56×
U1 json duration_ms sum by service 1h 57 ms 47 ms 153 ms 0.31×
U10 instant sum_over_time unwrap instant 26 ms 17 ms 17 ms 1.00×
U11 instant max_over_time missing label instant 4 ms 4 ms 11 ms 0.36×
U12 json duration_ms over every service 1h 129 ms 123 ms 476 ms 0.26×
U13 logfmt latency_ms over every service 1h 289 ms 270 ms 662 ms 0.41×
U14 duration() of a unit string 1h 27 ms 23 ms 31 ms 0.74×
U2 json duration_ms max by service 1h 49 ms 52 ms 505 ms 0.10×
U3 json duration_ms sliding sum 1h 46 ms 43 ms 142 ms 0.30×
U4 logfmt size_bytes sum by service 1h 24 ms 21 ms 31 ms 0.68×
U5 logfmt ttl unit string (no sample) 1h 22 ms 22 ms 29 ms 0.76×
U6 missing label, no parser 1h 13 ms 15 ms 55 ms 0.27×
U7 missing label, ungrouped sum 1h 12 ms 12 ms 77 ms 0.16×
U8 rate of an unwrapped label 1h 80 ms 72 ms 123 ms 0.59×
U9 bytes() conversion 1h 27 ms 32 ms 31 ms 1.03×
Why these shapes
  • internal/translator/translator.go → 112: control/bytes_over_time by app, control/instant json filtered, control/instant json volume, control/instant ungrouped rate, control/json rate by level (no filter, no drop), control/json volume no filters, control/logfmt count by level, filter, drop, control/logfmt volume …
  • internal/proxy/metric_agg.go → 22: control/bytes_over_time by app, control/instant json volume, control/instant ungrouped rate, control/json volume no filters, control/stream-label count by app, control/topk rate by app, control/ungrouped rate, drilldown-breakdown-exact/DB1 label breakdown pod …
  • internal/proxy/range_metric_compat.go → 21: control/bytes_over_time by app, control/instant json volume, control/instant ungrouped rate, control/json volume no filters, control/stream-label count by app, control/topk rate by app, control/ungrouped rate, json-filter-pushdown/A explore volume, 2 filters, drop …
  • internal/proxy/vector_matching.go → 19: control/stream-label count by app, unwrap-sum-without/W1 sum without pod, max_over_time, unwrap-sum-without/W2 sum without pod, sum_over_time, unwrap-sum-without/W3 sum without pod, rate, unwrap-sum-without/W4 max without pod, max_over_time, unwrap-sum-without/W5 sum without pod, max_over_time, instant, unwrap-sum-without/W6 sum without pod, rate, instant, unwrap-sum-without/W7 topk 5 of sum without pod …
  • shape added or edited → 9: unwrap-sum-without/W1 sum without pod, max_over_time, unwrap-sum-without/W2 sum without pod, sum_over_time, unwrap-sum-without/W3 sum without pod, rate, unwrap-sum-without/W4 max without pod, max_over_time, unwrap-sum-without/W5 sum without pod, max_over_time, instant, unwrap-sum-without/W6 sum without pod, rate, instant, unwrap-sum-without/W7 topk 5 of sum without pod, unwrap-sum-without/W8 sum without pod, count_over_time (production) …
  • control smoke → 8: control/drilldown service landing, control/instant ungrouped rate, control/json rate by level (no filter, no drop), control/json volume no filters, control/logs plain, control/logs | json, control/stream-label count by app, control/topk rate by app
  • internal/proxy/proxy.go (endpoint handler) → 8: control/drilldown service landing, control/instant ungrouped rate, control/json rate by level (no filter, no drop), control/json volume no filters, control/logs plain, control/logs | json, control/stream-label count by app, control/topk rate by app
  • internal/proxy/manual_metric_budget.go → 5: control/bytes_over_time by app, drilldown-breakdown-exact/DB1 label breakdown pod, drilldown-breakdown-exact/DB2 label breakdown namespace, drilldown-breakdown-exact/DB3 json field breakdown user_id, drilldown-breakdown-exact/DB4 json field breakdown batch_id
  • internal/config/limits.go → 4: metadata-inventory/refresh label values env, metadata-inventory/refresh label values service_name, metadata-inventory/refresh labels (label browser), metadata-inventory/refresh labels with selector

base cd174a7d43 → PR d4c3523fe6 · 4 runs (first is cold), warm p50 · ranges 1h · noise ±30% and 100 ms · VictoriaLogs restarts 0→0 · 16.4 min. Same runner, one fresh stack, targets interleaved per request. How to read and reproduce: bench/ab/README.md.

@github-actions

github-actions Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

PR Quality Report

Compared against base branch main.

Coverage and tests

Signal Base PR Delta
Test count 10738 11242 504
Coverage 88.0% 88.3% +0.3% (improved)

Compatibility

Track Base PR Delta
Loki API 100.0% 11/11 (100.0%) 0.0% (stable)
Logs Drilldown 100.0% 17/17 (100.0%) 0.0% (stable)
VictoriaLogs 100.0% 11/11 (100.0%) 0.0% (stable)

Performance smoke

Lower CPU cost (ns/op) is better. Lower benchmark memory cost (B/op, allocs/op) is better. Higher throughput is better. Lower load-test memory growth is better. Benchmark rows are medians from repeated samples.

Signal Base PR Delta
QueryRange cache-hit CPU cost 3449.0 ns/op 3341.0 ns/op -3.1% (stable)
QueryRange cache-hit memory 1240.0 B/op 1240.0 B/op 0.0% (stable)
QueryRange cache-hit allocations 27.0 allocs/op 27.0 allocs/op 0.0% (stable)
QueryRange cache-bypass CPU cost 4097.0 ns/op 4041.0 ns/op -1.4% (stable)
QueryRange cache-bypass memory 1301.0 B/op 1301.0 B/op 0.0% (stable)
QueryRange cache-bypass allocations 27.0 allocs/op 27.0 allocs/op 0.0% (stable)
Labels cache-hit CPU cost 881.2 ns/op 879.5 ns/op -0.2% (stable)
Labels cache-hit memory 48.0 B/op 48.0 B/op 0.0% (stable)
Labels cache-hit allocations 3.0 allocs/op 3.0 allocs/op 0.0% (stable)
Labels cache-bypass CPU cost 1515.0 ns/op 1487.0 ns/op -1.8% (stable)
Labels cache-bypass memory 400.0 B/op 399.0 B/op -0.2% (stable)
Labels cache-bypass allocations 14.0 allocs/op 14.0 allocs/op 0.0% (stable)

State

  • Coverage, compatibility, and sampled performance are reported here from the same PR workflow.
  • This is a delta report, not a release gate by itself. Required checks still decide merge safety.
  • Performance is a smoke comparison, not a full benchmark lab run.
  • Delta states use the same noise guards as the quality gate (percent + absolute + low-baseline checks), so report labels match merge-gate behavior.

@github-actions

github-actions Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

✅ Visual smoke: passed with warnings

Base cd174a7d43 vs PR 98c963c203, Grafana through the proxy builds and Loki on one fresh stack; run.
Loaded again, once and more patiently, after a first-pass difference or an unsettled page: explore-sum-without-rate 15m, explore-sum-without-rate 1h, explore-sum-without-rate 6h, explore-topk-sum-without 1h.
Core set: 5 capture(s) at 1h. Detailed set: 29 entries (explore-sum-without-rate, explore-metric-plain, explore-A, explore-B, explore-C, explore-E, explore-F, explore-logs-json, ...). 37 capture(s), 15m/1h/6h, 24h and 7d stay out of PR runs.
Trimmed to the 1h range for the capture budget: explore-A, explore-B, explore-C, explore-E, explore-F, explore-logs-json, explore-logfmt-metric, explore-logs-extracted, explore-extracted-metric, explore-extracted-filter-metric, explore-extracted-group-metric, explore-logs-plain-level, explore-plain-level-metric, explore-logs-line-filter-or, explore-metric-line-filter-or, explore-unwrap-sum, explore-unwrap-unit-string, explore-unwrap-missing-label, explore-unwrap-conversion-error, explore-unwrap-rate-sum, explore-empty-pattern-filter, explore-pattern-filter-anchored, dd-labels-namespace, dd-field-service_version, dd-field-pipeline, explore-topk-sum-without, explore-sum-without-count.
Not run (capture budget): explore-sum-without-count.

Fix proof

Captures of the exact cases this pull request adds or fixes (registry case, base | PR | Loki). The verdict compares the PR with Loki where the base did not match. The gate fails a case unless one capture is fixed or improved, or, beyond the Loki data window, the base failed where Loki and the PR both answered with data (the label does not excuse it; only visual: none on the case does).

semantics/unwrap-sum-without-collapses: 🟡 improved

page range base = PR (data) vs Loki (PR) verdict
explore-sum-without-rate 15m differs (1) 1 difference(s), 1 on base too 🟡 improved
explore-sum-without-rate 1h differs (1) 1 difference(s), 1 on base too 🟡 improved
explore-sum-without-rate 6h differs (1) n/a (beyond the Loki window) ⚪ undecided
explore-sum-without-rate-15m: improved

explore-sum-without-rate-15m

explore-sum-without-rate-1h: improved

explore-sum-without-rate-1h

explore-sum-without-rate-6h: undecided

explore-sum-without-rate-6h

Regression check

page range set base = PR (data) vs Loki (PR) pixel diff result
dd-fields 1h core ✅ identical identical (4 explained, 1 history-dependent) 0.11% ✅
dd-landing 1h core ✅ identical identical (3 explained) 0.13% ✅
dd-service-logs 1h core ✅ identical identical (3 explained, 1 history-dependent) 0.11% ✅
explore-logs-plain 1h core ✅ identical identical (1 explained) 0.07% ✅
explore-metric-plain 1h core ✅ identical identical (1 explained) 0.04% ✅
dd-field-pipeline 1h detailed ✅ identical identical (3 explained, 1 history-dependent) 0.11% ✅
dd-field-service_version 1h detailed ✅ identical identical (3 explained, 1 history-dependent) 0.11% ✅
dd-labels-namespace 1h detailed ✅ identical identical (3 explained, 1 history-dependent) 0.11% ✅
explore-A 1h detailed ✅ identical identical (1 explained) 0.04% ✅
explore-B 1h detailed ✅ identical identical (1 explained) 0.04% ✅
explore-C 1h detailed ✅ identical identical (1 explained) 0.04% ✅
explore-E 1h detailed ✅ identical identical (1 explained) 0.04% ✅
explore-F 1h detailed ✅ identical identical (1 explained) 0.04% ✅
explore-empty-pattern-filter 1h detailed ✅ identical identical (1 explained) 0.07% ✅
explore-extracted-filter-metric 1h detailed ✅ identical identical (1 explained) 0.04% ✅
explore-extracted-group-metric 1h detailed ✅ identical identical (1 explained) 0.04% ✅
explore-extracted-metric 1h detailed ✅ identical identical (1 explained) 0.04% ✅
explore-logfmt-metric 1h detailed ✅ identical identical (1 explained) 0.04% ✅
explore-logs-extracted 1h detailed ✅ identical identical (1 explained) 0.07% ✅
explore-logs-json 1h detailed ✅ identical identical (1 explained) 0.07% ✅
explore-logs-line-filter-or 1h detailed ✅ identical identical (1 explained) 0.07% ✅
explore-logs-plain-level 1h detailed ✅ identical identical (2 explained) 0.07% ✅
explore-metric-line-filter-or 1h detailed ✅ identical identical (1 explained) 0.04% ✅
explore-metric-plain 15m detailed ✅ identical identical (1 explained) 0.04% ✅
explore-metric-plain 6h detailed ✅ identical n/a (beyond the Loki window) 0.04% ✅
explore-pattern-filter-anchored 1h detailed ✅ identical identical (1 explained) 0.07% ✅
explore-plain-level-metric 1h detailed ✅ identical identical (1 explained) 0.04% ✅
explore-sum-without-rate 15m detailed differs (1): query ('sum without (pod) (rate({service_name="payment-servi 1 difference(s), 1 on base too 0.92% ✅ improved (closer to Loki)
explore-sum-without-rate 1h detailed differs (1): query ('sum without (pod) (rate({service_name="payment-servi 1 difference(s), 1 on base too 2.01% ✅ improved (closer to Loki)
explore-sum-without-rate 6h detailed differs (1): query ('sum without (pod) (rate({service_name="payment-servi n/a (beyond the Loki window) 6.40% ✅ improved (closer to Loki)
explore-topk-sum-without 1h detailed differs (1): query ('topk(5, sum without (pod) (rate({service_name="payme 1 difference(s), 1 on base too 2.01% ✅ improved (closer to Loki)
explore-unwrap-conversion-error 1h detailed ✅ identical identical (1 explained) 0.04% ✅
explore-unwrap-missing-label 1h detailed ✅ identical identical (1 explained) 0.04% ✅
explore-unwrap-rate-sum 1h detailed ✅ identical identical (1 explained) 0.04% ✅
explore-unwrap-sum 1h detailed ✅ identical 1 difference(s), 1 on base too; 1 explained 0.04% ✅
explore-unwrap-unit-string 1h detailed ✅ identical identical (1 explained) 0.04% ✅
tail-json-filter live detailed ✅ identical entries in the window: PR 1905, Loki 684, in both 684 6.11% ✅

Gate: a base-vs-PR data difference passes when the PR is closer to Loki, and fails when the PR diverges from Loki or Loki cannot decide it (at a range Loki does not hold, a difference passes when the same page proved it closer to Loki at a shorter range; otherwise label the pull request visual-change-expected to accept an intended change). Also failing: a panel empty on the PR but not on the base, an error banner, panel error, error answer or new "No data" panel on the PR, a PR side that never settled, a difference that did not reproduce on the recapture. Pixel differences only warn.

explore-sum-without-rate-6h: no Loki data at this range; the same differences match Loki at a shorter range (1): query ('sum without (pod) (rate({ser
  • no Loki data at this range; the same differences match Loki at a shorter range (1): query ('sum without (pod) (rate({service_name="payment-service"} | logfmt amount | unwrap amount: series sets differ: 1 only left ["('A', '', '{}')"], 2 only ri
  • pixel diff 6.4%

explore-sum-without-rate-6h

Core set montages (5): base | PR | Loki

dd-fields-1h

dd-fields-1h

dd-landing-1h

dd-landing-1h

dd-service-logs-1h

dd-service-logs-1h

explore-logs-plain-1h

explore-logs-plain-1h

explore-metric-plain-1h

explore-metric-plain-1h

All montages of this run: pr-visuals/pr-739.

…aining label set, like Loki

`sum without (pod) (max_over_time({app="payment-service"} | logfmt | unwrap
amount [5m]))` answered one series on query_range and at an instant, where
Loki keeps every label but pod. The translation of an outer aggregation over
an unwrap function, and of a sum over rate, ended in a stats pipe with no
grouping, so VictoriaLogs returned one row and the without step had no labels
to group by. The last stats pipe of an `<op> without (...)` now keeps the
stream (the range aggregation's own grouping when it has one), VictoriaLogs
answers a row per stream from stats, and the proxy drops the excluded labels
and folds the rows with the outer operator, as Loki's VectorAggEvaluator does.
szibis added a commit that referenced this pull request Oct 17, 2026
@szibis
szibis force-pushed the fix/unwrap-sum-without branch from 0d3b2b4 to e019f0a Compare October 17, 2026 16:26
@github-actions github-actions Bot added size/XL Extra large change and removed size/XL Extra large change labels Oct 17, 2026
@szibis
szibis marked this pull request as ready for review October 17, 2026 16:43
…tion, like Loki

Loki counts max_query_series on the answer of the expression, after the
without fold. The proxy applied the limit to the per-stream rows it reads
before the fold, so a `without` query failed as soon as the streams
outnumbered the limit even when the answer fit it. A `without` request now
reads the per-stream rows under the raw-row budget and applies the tenant's
limit to the folded answer: Loki's 400 for a plain client, the busiest series
with Loki's warning for Logs Drilldown, on query_range, at an instant and
under topk, sort, stddev and stdvar.
… over an unwrap function

Adds the unwrap-sum-without A/B set and its saved 1h and 6h/24h/7d results next to
the control set, a visual page that fixes the registered case, and brings the series
limit rows up to date: the limit counts the answer of the aggregation after the
without fold, as Loki does. The harness leaves out the log limit and direction
parameters for a range metric shape marked no_limit, which Loki holds an answer of
thousands of series for until the client gives up.
…t over an unwrap function

count and avg without after a parser keep one series where Loki keeps one per remaining label set; sum and sum without over quantile_over_time answer an empty result; sum by over quantile_over_time takes the quantile of the group instead of adding the per-series quantiles.
…eps its status and Loki's error format

A test pins the status, the envelope and the redaction of the backend's echo of
the query for a rejected and a failing backend, on the instant queries that are
rewritten after VictoriaLogs answers (without, label_replace, label_join).
The translator records the operator of the without aggregation in the marker
(__without__:<op>:pod) and the consumers fold with it, so a topk, sort,
label_replace or a comparison around a max, min or count without no longer adds
the series up (pods valued 10 and 4 gave 14 for a max). A binary expression folds
its operands on their own and is no longer folded again over the combined answer,
and the ranking of an instant topk or bottomk runs after the fold, not over the
per-stream rows.

The reads before the fold reuse the mark of an ungrouped sum folded afterwards
instead of a key of their own. When the raw-row budget trips, not the tenant
limit, the client reads the row-limit error naming -manual-range-metric-row-limit,
still a rejection. The ranking of the top values of a grouping and the sliding
window evaluator take the tenant's limit.
…and limit behaviour

Drops the stray empty 2.11.1 heading, states the folded operator, the row-limit
error and the wrappers, and points to the registry row for the rest.
…ed busiest series

The raw-row budget replaces the tenant limit before the fold only for a request that
withWithoutFold marked (it records the budget); an ungrouped sum folded afterwards keeps
the tenant limit, as before. The Drilldown case of the folded series limit now asserts
that the kept series is the busiest of the folded answer, which a limit applied to the
streams before the fold would not keep. A topk of a comparison over a count without is
pinned at an instant (on query_range a topk over a binary expression is answered 502 with
or without without, and the range handler drops the fold for a binary inner like the
instant one), and the translator test pins that stddev and stdvar without keep a single
row.
szibis added 11 commits October 17, 2026 19:26
… of a without request

The evaluator that answers a grouped range query from stats buckets cut the
series at the tenant's limit while reading them. A request that folds its series
afterwards (a without aggregation reads a row per stream) is limited on the folded
answer, as Loki counts it, so the evaluator takes the bound the other routes take:
resolvedMaxStatsQuerySeries.
…t is the outermost

The proxy folds a row per stream by the without clause for sum, min, max, avg
and count without, the outermost aggregation of the query (a topk, bottomk,
sort, stddev or stdvar may wrap it, and a label_replace on query_range: the proxy
applies it to the folded series). The marker of any other query that holds a without
clause (topk, bottomk and stddev without; a without under another aggregation
such as max(sum without ...)) names no operator: the translation, the fold and
the series limit are what they were before the fold existed, and the tests pin
those answers.
…ot by the limit value

A without aggregation reads a series per stream before it folds them. The error
it hits then was recognised as the raw-row budget by the budget being equal to
the limit that tripped, so an operator who set both limits to one value, or a
check against the tenant's own limit at that value, got the other text. The
request records whether the budget is the bound it reads under (it is, unless
the tenant's limit is lower), the error follows that, and the tenant's own
limit check clears the fold from its context.

The error answers 400, a rejection of the query, where the raw-row reads answer
502: the limit reference and the operations notes say both.
…ies and an offset on a without

The label_replace wrapper of the without differential now asserts the dst label
it adds, the comparison wrapper uses a threshold between the groups' values so
it drops some and keeps others, and a new test pins sum, max and count without
over a range function with an offset against Loki's window.
…er from Loki

sum without () over an over-time function on query_range, a without over an
inner sum by or count_over_time, a without under max or sum by, a comparison
inside the aggregation, topk and bottomk without, label_replace around a vector
at an instant and around a binary expression on query_range. Each row states
the answer of the proxy and Loki's for the shapes a unit test reproduces.
@szibis
szibis force-pushed the fix/unwrap-sum-without branch from e019f0a to ba4b37c Compare October 17, 2026 17:35
@github-actions github-actions Bot added size/XL Extra large change and removed size/XL Extra large change labels Oct 17, 2026
…er's extraction list names

A Loki series is the stream plus the labels its parsers extract, less the
unwrapped one. With an extraction list (| logfmt amount, | logfmt amount, route)
that set is known, so the inner stats of the sum, min, max, avg or count without
that the proxy folds now groups by the stream and those labels instead of by
_msg, which split the series per line: sum without (pod) (max_over_time(... |
logfmt amount | unwrap amount [5m])) answered a sum of per-line maxima. The
grouping applies to that fold only, on its instant, tumbling and sliding routes;
every other query, whatever its parser, keeps its translation and its answers.
A bare | logfmt or | json, label_format, drop, keep, a logfmt alias and a key
that is also a label of the stream selector (Loki names it <key>_extracted) keep
the previous grouping.
…ective series limit, and is a 400 the 5xx alert does not see
… keeps Loki's answer bounded

A bare | logfmt makes every payment line a series of its own on Loki (thousands of series), so its capture never settles and the A/B shapes time out. | logfmt amount makes the stream the series, which Loki answers in two series and the proxy now answers alike. The fix-proof page is the rate over [$__auto]; the max_over_time page stays as a cover.
…r data, and register two more

The requests use the level label and a bounded parser, so each reproduces on the generator data, with the values measured against Loki 3.7.7. count and avg without are right after an extraction list and stay open for a bare parser. Two shapes found on the way: sum by over a per-series function and a bare range function that exposes every parsed label.
…comparison threshold that splits the series, and the max-over-without numbers

The two quantile rows keep their earlier requests.
…s previous answers after an extraction list, and register what differs

The extraction list names the labels of a Loki series, but the proxy reads them for the without fold only. The differential test sends the other shapes (sum, avg, count, max, min and topk of per-series functions, rate and count_over_time, a without under another aggregation) through the handler on an emulated VictoriaLogs, as an instant query, a tumbling window and a sliding one, and pins the LogsQL sent and the answer to the ones origin/main produced. A second test pins the proxy's answers beside a reference evaluator that follows the Loki sources, and two open rows register the series identity outside without and a parsed key that is also a stream label (Loki names it key_extracted). The figures come from the emulator, not from a Loki run.
@github-actions github-actions Bot added size/XL Extra large change and removed size/XL Extra large change labels Oct 17, 2026
…n the final tree

The affected set (W1 to W7 with | logfmt amount, W8, W9) at 1h, 6h, 24h and
7d and the control set, base cd174a7 against the branch on one stack, with
VictoriaLogs at 2 CPUs and 0 restarts. The performance evidence report is
regenerated from the saved runs.
The max_over_time page could not match Loki: its values follow the over-time window difference that is still open, so it stays out until that is fixed. The rate, topk and count pages match Loki exactly.
@github-actions github-actions Bot added size/XL Extra large change and removed size/XL Extra large change labels Oct 18, 2026
@szibis
szibis merged commit bbd22ae into main Oct 18, 2026
64 checks passed
@szibis
szibis deleted the fix/unwrap-sum-without branch October 18, 2026 06:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugfix Bug fix scope/docs Documentation scope/proxy Proxy core scope/tests Tests scope/translator LogQL translator size/XL Extra large change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant