Repository navigation
Conversation
🔴 Performance A/B: regression104 shape×range — 2 🔴 slower · 102 ⚪ same · results vs Loki: 86 same, 0 new difference(s), 18 pre-existing
¹ re-measured with 7 runs, on windows the first pass did not use, after it moved beyond noise. 102 unchanged shape×range (within noise)
Cold (first-run) timings — what a user opening a panel sees
Why these shapes
base |
PR Quality ReportCompared against base branch Coverage and tests
Compatibility
Performance smokeLower CPU cost (
State
|
✅ Visual smoke: passedBase Fix proofCaptures 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
Regression check
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 Core set montages (5): base | PR | Lokidd-fields-1h dd-landing-1h dd-service-logs-1h explore-logs-plain-1h explore-metric-plain-1h All montages of this run: |
3f174b0 to
a52da44
Compare
17c6e6b to
45187a3
Compare
45187a3 to
c9c6d52
Compare
…reads escaped regexps The transform ran on range matrices only, so an instant vector came back without the new label. It now rewrites the labels of a vector and of a matrix alike, keeps the rest of the answer, expands the replacement as Loki does and files the points of series that end with one label set under one series on a range query, as JoinSampleVector does. The string arguments are unquoted like the LogQL lexer does, a call without the four string arguments is a 400 parse error, topk/sort/stddev over a label_replace no longer send the translator's marker to VictoriaLogs, a label_replace around a binary expression on query_range routes by the expression under it, and the without marker no longer swallows the markers that follow it.
… the expression under it, and reports Loki's argument errors The series of a label_replace answer are ordered by label set after the transform (a range result, and an instant vector unless the query sorts by value). The request is cloned once, above the routing chain, when a label transform wraps the expression. A malformed call reports the token names and the string start column of Loki's parser. label_join is no longer claimed as a feature of the transform. Ranking over duplicate label sets that a label_replace produced is registered as an open row.
…n query, so each operand keeps its parser
…stream, and around a json metric keeps the parser-error semantics An instant label_replace(count_over_time(...)) lost the grouping by stream (the check looked for a query that starts with the function) and answered one unlabeled total. A label_replace around a metric over | json skipped the evaluator that owns JSON parser errors and answered 200 for lines Loki fails or drops. Both now follow the expression under the transform; the e2e leaves the structured metadata Loki adds to a bare range function's series (an owner-kept deviation) out of the comparison and ranks one series under topk so ties do not decide the shape.
…th the label_replace A/B results
… regenerate the reports
c9c6d52 to
d7da182
Compare
… JSON content type





Summary
Gap
semantics/label-replace-new-label(cluster a9d4f85c, instant queries). On Loki 3.7.7,label_replaceworks per step on both a vector and a matrix (pkg/logql/evaluator.go:1275-1309). The proxy'sapplyLabelReplace/applyLabelJoindecoded only a matrix, so an instantvectorcame back without the new label.Fixed, matching Loki:
transformSeriesLabelsrewrites labels on a vector or a matrix and keeps the rest of the answer, e.g.data.stats.ExpandString. A series with no match is unchanged, and an empty expansion removes the destination label.JoinSampleVector/vectorsToSeries(engine.go:440-470, 530-600): an instant query keeps both samples, and a range query files the points under one series.syntax/lex.go:191-198)."\\."is now the regexp\., and escaped quotes work.topk/bottomk/sort/stddevover alabel_replacesent an internal marker to VictoriaLogs and got a 400. The transform now runs on the inner expression before the ranking, as Loki evaluates it.label_replacearound a binary expression now routes by the inner expression, as fix: an instant query keeps its failure status and always answers a resultType #735 does for instant.label_replace(sum without (level)(...))no longer loses its transform.VictoriaLogs still does all aggregation. The proxy only rewrites labels on the aggregated series.
Tests
TestLabelReplaceEdgeTable: 29 rules × matrix and vector.TestLabelReplaceDifferential: random nested transforms on both endpoints. Its reference is independent for the plumbing (translator markers, both endpoints, nested chains) and for Loki's duplicate merge; the per-series rule reuses Loki'sExpandStringsequence.TestCompat_LabelReplaceNewLabel, about 38 shapes ×query_rangeandquery.Local proofs (e2e vs Loki 3.7.7 on VictoriaLogs v1.52.0 and v1.47.0, A/B base e9b5ef1 vs branch)
Changes since the first push
The first e2e run in CI found two product bugs and two test problems; all are fixed here.
label_replace(count_over_time({env="production"}[5m]), ...)(any bare range function) was one unlabeled{}total instead of one series per stream. The grouping by stream was added only for a query that itself starts with the function; it now follows the expression under the transform (preserveMetricStreamIdentity, and the same check in the two handlers).label_replacearound a metric over| jsonskipped the evaluator that owns JSON parser errors: a line that is not JSON no longer failed the query, and| json | __error__=""kept every stream (14 apps) instead of the ones that parse (8, as Loki). It is now answered by that evaluator and the transform is applied to its answer.metric-identity/structured-metadata-splits-series, owner-kept), so the two bare shapes leave those label names out of both sides;app_short,buzz, the stream labels anddetected_levelare still compared.topk(2, ...)over tied values (several apps at 6) lets Loki pick different ties per step; the shape is nowtopk(1, ...), where api-gateway is the unique maximum.Unit tests:
TestLabelReplaceOverBareRangeFunctionKeepsOneSeriesPerStreamandTestLabelReplaceOverJSONMetricUsesTheOrderedJSONEvaluator; both fail without the fixes.go test -shortofinternal/translator,internal/proxy,internal/logql,go vet(also-tags e2e) and golangci-lint v2.14.0 are clean.e2e vs Loki 3.7.7, fresh isolated stack per VictoriaLogs version
VictoriaLogs RestartCount 0 before and after each run (no OOM).
TestCompat_LabelReplaceNewLabelTestCompat_LabelReplaceDuplicatesBeforeRankingTestMissingOps_LabelReplacelabel_replace_sum, skipped by the test itself: the aggregation-over-label_replace gap, now registered)TestCompat_InstantResultTypeAlwaysSetTestLogQL_Exhaustive_ErrorParity/_QueryParity/_KnownGapsTestHardeningLive_ParserError*(4 tests, JSON parser-error parity, regression set for fix 2)Notes:
TestCompat_LabelReplaceDuplicatesBeforeRankingpasses on this data althoughsemantics/label-replace-duplicates-before-rankingis open: the shape it ranks does not reproduce a point-count difference here. The row stays open.two series with one label set,all labels removed, query_range) pass on a fresh stack and fail (153 points on Loki, 140 on the proxy) on every re-run against the same stack: a re-run ingests the fixture again, and VictoriaLogs counts the duplicates where Loki deduplicates them. They are not loosened; run the group once per fresh stack, as CI does.A/B (base = e9b5ef1, branch = 4e21fc8, which holds the fixes; 1h with Loki, 5 runs, warm p50, one stack, targets interleaved)
Affected shapes (set
label-replace):{}total)| json | __error__=""sum by| json | __error__=""sum bytopk(1)).label_replacetakes 531 ms on the base and 509 ms on the branch for a 1h range, and 40 ms and 41 ms instant. The A/B flags L6 as the one slower shape; I did not hide it. On this generator data everyapi-gatewayline is valid JSON, so base and branch give the same series here; on a stream with invalid lines the base answered wrongly (14 apps against Loki's 8 in the e2e fixtures).Control set (20 shapes), 1h with Loki: 20 of 20 same, VictoriaLogs restarts 0 to 0, new differences from Loki 0, pre-existing 1 (topk rate by app, also on base). Largest absolute increases: instant json filtered 150 to 163 ms, drilldown service landing 10 to 14 ms, logs | logfmt 42 to 45 ms.
Saved results:
bench/ab/results/2026-10-12-labelreplace-1h.jsonand2026-10-12-labelreplace-control-1h.json; shape setlabel-replace.Registry
semantics/label-replace-new-label(text and planned tests updated).semantics/label-replace-aggregation-over-transform(an aggregation over a label_replace is a 400 on the proxy and answers on Loki) andsemantics/label-replace-around-ranking-and-vector(a label_replace around topk, sort orvector()). Both answer the same on the base, so neither is new. The PR body's draft about a binary operand (label_replace(...) * 2) no longer reproduces and is not registered.label_joinis accepted by the proxy but not part of Loki 3.7.7's grammar; it is handled by fix: reject group(), label_join() and other names Loki does not have, and accept keywords in any case, like Loki #748 and only noted in the row text here.visual: none(the visible difference is on instant queries, which the visual harness does not capture; the json difference needs a stream with invalid lines).