Skip to content

fix: regexp label matchers select what Loki selects, on selectors and label filters - #909

Merged
szibis merged 6 commits into
mainfrom
fix/label-matcher-regexp-anchoring
Oct 28, 2026
Merged

szibis merged 6 commits into
mainfrom
fix/label-matcher-regexp-anchoring

Conversation

@szibis

@szibis szibis commented Oct 28, 2026

Copy link
Copy Markdown
Collaborator

Supersedes #572 by @clain23: its commit is kept here as authored (fix(logql): anchor label-matcher regexps to the whole label value), and the next commit completes it for label filters, where Loki does not simply anchor.

Summary

Regexp label matchers now select what Loki selects. Contributed by @clain23 (first commit, kept as authored); the second commit completes it for label filters and keeps VictoriaLogs on its fast filters.

Selectors. Loki builds {app=~"api|web"} with Prometheus' labels.NewMatcher, which compiles ^(?s:api|web)$ (vendor/github.com/prometheus/prometheus/model/labels/regexp.go:76). It selects app api and web only. The proxy sent app:~"api|web", and VictoriaLogs' regexp filter is a substring match (lib/logstorage/filter_regexp.go, matchRow). So a Grafana multi-value variable also selected api-gateway, my-api and webhook, and {namespace=~"nch"} selected anch. Nothing errored; panels and log lists just held more. Only the derived service_name matcher was anchored.

Where it applied. Every route that takes a selector was affected:

  • query and query_range;
  • labels and label values;
  • series;
  • index volume, volume_range and stats;
  • detected labels, detected fields and detected field values;
  • patterns;
  • tail.

Multi-tenant fan-out. {__tenant_id__=~"prod"} also queried prod-eu and nonprod. Loki matches tenant ids with the anchored matcher (pkg/querier/multi_tenant_querier.go:421, filterValuesByMatchers).

Label filters are different. Loki does not anchor | label =~ "re" as written. parseRegexpFilter (pkg/logql/log/filter.go:645, reached from NewStringLabelFilter in label_filter.go:360) first runs RegexSimplifier.Simplify (filter.go:696). It anchors only what it cannot simplify, as ^(?:re)$ without the s flag:

  • a literal is an equality check, case-insensitive under (?i);
  • a literal alternation (api|web) is equality per branch;
  • api.* and .*api are substring checks;
  • a factored alternation is one substring check per branch. Go's parser factors common prefixes, so api|api-gateway and prod-eu|prod are factored;
  • =~"" and .* keep every line, and .+ keeps non-empty values;
  • an alternation drops a branch that keeps everything (newOrFilter, filter.go:309), so GET| is an equality check on GET.

The contributor's commit wrapped label filters in ^(?s:re)$ too. That answered 24 of 80 label-filter cases differently from Loki. The second commit ports the simplifier.

VictoriaLogs semantics checked

  • Regexp filter. field:~"re" is an unanchored match (filter_regexp.go) in which . matches a newline: regexutil parses with syntax.DotNL. Exactly "" and .* become a no-op filter, and .+ becomes the non-empty prefix filter (parser.go, newFilterRegexpOpt).
  • Stream filter. VictoriaLogs' own {f=~"re"} is already anchored. regexutil.NewPromRegex compiles ^(?:re)$, and indexdb.go getStreamIDsForTagFilter treats a regexp that matches the empty value like Prometheus does.
  • No index use lost. The proxy never sends a regexp matcher as a stream filter: canUseStreamSelector accepts only = on configured stream fields, on main and here. Routing =~ to _stream:{} would be a separate improvement (listed under follow-ups).
  • Identical at both ends of the support range. Checked in the v1.49.0 and v1.53.0 sources; the vendored regexutil is the same file in both.

Fix

internal/translator/label_matcher_regexp.go:

  • selectorRegexpFieldFilter, for selectors. It matches the whole value with . matching newlines. It sends:

    • a whole-value literal as an exact filter ({app=~"api"} → app:="api");
    • .* and .+ as they are (VictoriaLogs' match-all and non-empty filters);
    • .*literal.* as the substring regexp;
    • anything else as ^(?s:re)$.
  • lokiLabelFilterPattern plus labelFilterRegexpFieldFilter, for label filters. They emit the filter Loki's simplification describes:

    • a literal as an exact filter;
    • a literal alternation as exact filters joined by OR (| method!~"GET|HEAD" → -(method:="GET" OR method:="HEAD")), at most 16;
    • a substring check as the substring regexp;
    • anything else anchored, spelled ^(?-s:re)$ when it contains a ., because VictoriaLogs compiles . to match newlines.

    translatedFilterFieldOp reads the field inside an OR group, so the same-field dedupe of repeated Drilldown filters keeps its key.

  • Escaping. A literal with a control character keeps the regexp form, quoted with Go's rules.

  • Proxy-side conditions. NewLabelFilterCondition compiles a label filter with the same semantics. The ordered JSON metric pushdown and the Loki profile now use it to evaluate rows and to build their | filter (they compiled ^(?:re)$ through NewDropCondition). NewDropCondition stays for drop and keep matchers.

Every label-filter site uses one of these:

  • translateSingleLabelFilter;
  • the sanitized-key variants (parsedKeyMatchesEmpty);
  • the service_name label filter;
  • logsQLLabelCondition.

internal/proxy/multitenant.go (contributor): __tenant_id__ regexps are anchored as ^(?s:re)$.

Line filters (|~, !~) are unchanged. They are substring regexps in Loki too, and the tests pin that.

Breaking: a selector written for substring matching selects fewer streams, as in Loki. {namespace=~"prod"} no longer selects production; write .*prod.* where a substring is meant. Rules migrated with rules-migrate should be checked for the same.

Evidence

Loki's answers. Loki v3.7.7's own code produced them: syntax.ParseMatchers for selectors, and the stream pipeline of {app="x"} | json | v=~"…" for label filters. The table covers 40 patterns and both operators over 28 values: values that are prefixes of each other, the empty value, and one with a newline. It is embedded in TestLabelRegexpMatchesWhatLokiMatches.

VictoriaLogs' answers. The emitted filters were run on VictoriaLogs storage at v1.49.0 and v1.53.0: stored fields, stream fields, | unpack_json | filter and the stored-field search path.

selector answers equal to Loki label-filter answers equal to Loki
origin/main 26 / 80 36 / 80
contributor commit 80 / 80 56 / 80
this branch 80 / 80 (both versions) 80 / 80 (both versions)

VictoriaLogs query cost. 200k rows, 8 apps, GOMAXPROCS=1, median of 6 (15 for the label-filter alternation rows). Rows: what each form returns (main's are wrong).

shape rows main → branch v1.49.0 main → branch v1.53.0 main → branch
selector multi-value app=~"api-gateway|auth-service" 100000 → 50000 12.9 → 6.7 ms 8.4 → 4.2 ms
selector single value (exact filter) 75000 → 25000 25.0 → 3.3 ms 6.1 → 2.2 ms
selector prefix api-.* 75000 → 50000 9.6 → 6.2 ms 6.5 → 4.4 ms
selector .* (same query, noise) 200000 → 200000 24.4 → 21.7 ms 19.3 → 22.4 ms
label filter, literal alternation as anchored regexp (not sent) 17857 → 14286 15.8 → 17.9 ms 14.9 → 16.3 ms
label filter, literal alternation as OR of exact filters (sent) 17857 → 14286 17.0 → 17.5 ms 11.7 → 13.8 ms
label filter, single literal (exact) 10714 → 7143 14.5 → 13.8 ms 15.6 → 15.4 ms
label filter 5.. 10000 → 10000 13.8 → 13.3 ms 15.7 → 15.6 ms
label filter us-.* (substring) 14286 → 14286 12.7 → 11.5 ms 14.5 → 13.7 ms
stored field, multi-value 149999 → 100002 12.4 → 9.2 ms 17.5 → 13.8 ms
stored field, single value 99998 → 50001 10.4 → 7.4 ms 14.6 → 11.0 ms
  • The host was loaded (load average 40 to 70). The .* row sends the same query on both sides, and across runs it moved from -27% to +11%. So the label-filter rows over unpacked fields, within ±15%, are noise here; the A/B on the stack decides them.

  • Matching cost. Isolated lib/regexutil v1.53.0, one value, GOMAXPROCS=1:

    • main's substring alternation: about 12 ns;
    • the anchored alternation: about 100 ns;
    • an exact compare: about 1 ns.

    That is why a label filter's literal alternation is sent as OR of exact filters and not as the anchored regexp.

Translation cost. benchstat, -cpu 1 -count 10, interleaved builds, loaded host:

  • BenchmarkTranslate (internal/logql): unchanged, except many_labels +1 alloc/op (+40 B) from the anchoring string.
  • New BenchmarkTranslateLogQL_RegexpMatchers (five regexp matchers): 84 → 234 allocs/op and 2.5 → 11.1 KiB/op, from the regexp/syntax parses that decide the filter form. Wall time 16.6 → 23.2 µs (p=0.063, ±66% on the loaded host).
  • Controls without regexp matchers (Simple, MultiLabel, JSONPipeline): identical allocs, no significant time change.
  • The proxy caches translations, so a repeated query pays this once.

Tests

  • TestLabelRegexpMatchesWhatLokiMatches (internal/translator): the Loki table above, for selector and label filter, =~ and !~, translated with the v1.49.0 and v1.53.0 capability sets. Answers are evaluated with VictoriaLogs' filter semantics, which the storage run above validated. On origin/main it fails 196 subtests.

  • TestLabelFilterConditionAgreesWithLoki: the same table for the proxy-side conditions and the LogsQL the ordered JSON path sends.

  • FuzzSelectorRegexpFilter: the exact, substring and match-all forms against the reference ^(?s:re)$. Its 1120 seeds run in -short, plus 60 s of fuzzing locally.

  • TestLabelRegexpEmittedForms: the emitted forms, including line filters left unanchored.

  • TestLabelMatcherRegexpAnchoredOnEveryRoute (internal/proxy): a recording backend on 13 routes, plus the query tail sends. No unanchored matcher reaches VictoriaLogs. On origin/main all 13 routes fail.

  • TestTenantMatcherRegexpIsAnchored: extended with .*, .+, "", (?i), .*prod and a factored alternation.

  • Contributor tests:

    • kept as written: TestAnchorLabelMatcherRegex and TestAnchorLabelMatcherRegexMatchesLokiSemantics (the ^(?s:re)$ helper).
    • snapshots updated to the forms above: TestLabelMatchersAreAnchoredLineFiltersAreNot, TestRegexpMatchersReachVictoriaLogsUnescaped, FuzzRegexpMatcherEscaping (it now skips the exact and substring forms, which FuzzSelectorRegexpFilter checks).
  • e2e vs Loki, TestCompat_LabelMatcherRegexpAnchoring (test/e2e-compat, group core). Its own fixture is five streams, with app api / api-gateway / my-api / web / webhook and env prod / prod-eu / nonprod. It covers:

    • log queries with selector =~ and !~, (?i), .*api and api.+;
    • label filters after | json, including Loki's substring answer for svc=~"api|api-gateway";
    • metric queries, including the ordered JSON pushdown;
    • label values with a query, series, volume and detected labels;
    • a multi-tenant request (X-Scope-OrgID: 0|fake). Loki runs single-tenant in the compose stack, so this subtest is checked against Loki's matcher semantics.

    Each comparison fails when Loki answers nothing or answers the widened set.

Registry

  • profiles/stream-selector-regexp-anchoring (new, status: fixed).
  • profiles/label-filter-regexp-anchoring (was open, now fixed; title and why rewritten for Loki's simplifier).
  • A/B set label-matcher-anchoring (M1 to M6): multi-value and single-value selectors, an alternation over many streams, and label filters after | json.
  • Visual fix-proof pages:
    • explore-logs-multivalue-regexp: {app=~"api-gateway|auth"}. On main it also shows auth-service.
    • explore-volume-multivalue-regexp.
    • explore-volume-label-filter-regexp: | json | region=~"us-east|eu-west-1". On main it also counts us-east-1.

Checks

  • go vet ./..., go vet -tags e2e ./test/e2e-compat/ and gofmt -l: clean. golangci-lint: 0 issues.
  • -race on the touched tests in internal/translator, internal/proxy, internal/logql and internal/logsql: pass.
  • Capped full run (GOMEMLIMIT=3GiB GOMAXPROCS=4 go test -short -count=1 -p 1 ./cmd/... ./internal/...): every package ok. Peak RSS on the final head: 2.12 GB (1.92 GB on an earlier run); origin/main on the same host: 2.07 GB.
  • check_conformance.py: conformance gate ok. check_changelog_pr.py --base origin/main --head HEAD: ok.

Pending before ready

  • TestCompat_LabelMatcherRegexpAnchoring against Loki on VictoriaLogs v1.49.0 and v1.53.0 (-run '^(TestSetup_IngestLogs|TestCompat_LabelMatcherRegexpAnchoring)$').
  • A/B: set label-matcher-anchoring plus the control set, main vs branch vs Loki.
  • Visual smoke with the three fix-proof pages.
  • Backend-safety load test.

Follow-ups (not in this PR)

  • A regexp matcher on a configured -stream-fields label could use VictoriaLogs' stream index ({f=~"re"}, which VictoriaLogs anchors the same way). Today only = on stream fields does.
  • | detected_level=~ on a stored level field (detected_level_logsql.go) keeps its own ^(?:re)$. It differs from Loki only for an alternation with an empty branch, such as error|.
  • splitDropSpec (only used by a test) compiles drop and keep matcher regexps unanchored.
  • In a label filter, case-insensitive equality on non-ASCII values: Loki lowercases both sides, while the regexp folds case.

Update: rebased onto main (2026-10-28)

  • The two registry rows carry the perf: blocks main's conformance gate requires (bound 2× Loki, target 1×, unlocked until the daily run measures them).
  • FuzzSelectorRegexpFilter is registered in the CI fuzz-smoke job (local run: 2 min, about 650k execs, no failure).
  • The CHANGELOG entry is under [Unreleased], and the generated reports and quality section are regenerated.
  • Checks:
    • GOMEMLIMIT=2GiB GOMAXPROCS=4 go test -short -count=1 -p 1 on internal/proxy, internal/translator, internal/logql and internal/logsql passes.
    • go vet passes, including -tags e2e.
    • The conformance and changelog gates and actionlint pass.

@github-actions github-actions Bot added size/XL Extra large change scope/translator LogQL translator scope/proxy Proxy core scope/ci CI/CD scope/docs Documentation scope/tests Tests bugfix Bug fix labels Oct 28, 2026
clain23 and others added 4 commits October 28, 2026 14:49
A LogQL label matcher is a full-value match: Loki builds it with
Prometheus' labels.NewMatcher, which compiles a MatchRegexp as
"^(?s:" + v + ")$". VictoriaLogs' field:~"re" is an unanchored match
and the proxy passed the pattern through verbatim, so every =~ / !~
matcher was silently widened: {namespace=~"nch"} also selected "anch"
and "nch-b", {app=~"a|b"} matched "ab".

AnchorLabelMatcherRegex wraps the pattern once, in internal/logsql, and
every emitter of a label-matcher regexp uses it: the stream selector,
pipeline label filters, the derived service_name matcher and label
filter (which already anchored with the same form) and the multi-tenant
__tenant_id__ fan-out, which compiled the bare pattern with Go regexp.
The form is Prometheus' own ^(?s:...)$, so the Go-compiled call site is
literally what Prometheus compiles too.

Line filters (|~, !~) are substring regexps in Loki and are left alone.

A parity case with a prefix matcher ({app=~"api"}, empty on Loki) joins
the query-semantics matrix, and the docs record that results shrink for
existing substring-style matchers.
…st paths

Builds on the selector anchoring: a stream-selector matcher is a Prometheus
matcher, ^(?s:re)$ (vendor/.../model/labels/regexp.go:76), but a label filter
after a parser is not. Loki runs RegexSimplifier.Simplify first
(pkg/logql/log/filter.go:645 parseRegexpFilter, :696 Simplify, :309
newOrFilter) and anchors only what it cannot simplify, as ^(?:re)$ without
the s flag: a literal or a literal alternation compares whole values, api.*,
.*api and a factored alternation (api|api-gateway, prod-eu|prod) compare by
substring, =~"" keeps every line and GET| equals GET. Wrapping every label
filter in ^(?s:re)$ answered 24 of 80 label-filter cases differently from
Loki v3.7.7; lokiLabelFilterPattern ports the simplifier and every label
filter site uses it: translateSingleLabelFilter, the sanitized-key variants
(parsedKeyMatchesEmpty), the service_name label filter, and the ordered JSON
metric pushdown and Loki profile conditions (NewLabelFilterCondition), which
compiled ^(?:re)$.

VictoriaLogs compiles '.' to match newlines (regexutil parses with DotNL), so
an anchored label filter with a '.' is spelled ^(?-s:re)$. Where a cheaper
filter means the same, the proxy sends it: a whole-value literal as an exact
filter, .* and .+ as VictoriaLogs' own match-all and non-empty filters, a
.*-wrapped literal as the substring regexp, and a label filter's literal
alternation as exact filters joined by OR (Loki's OR of equality checks;
VictoriaLogs' anchored alternation costs about 100 ns per value against
12 ns for main's substring match and 1 ns for an exact compare, measured on
lib/regexutil v1.53.0). A literal with a control character keeps the regexp
form, which is quoted with Go's rules. translatedFilterFieldOp reads the
field of an OR group so the Drilldown same-field dedupe keeps its key.

Evidence: the Loki answers were produced by Loki v3.7.7's own code
(syntax.ParseMatchers and the label filter's pipeline) for 40 patterns over
28 values, and the emitted filters were run on VictoriaLogs v1.49.0 and
v1.53.0 storage (stored fields, stream fields, | unpack_json | filter): 80/80
selector and 80/80 label-filter answers match on both versions; origin/main
matched 26 and 36. On 200k rows VictoriaLogs answers the anchored selector
forms faster than the unanchored ones (multi-value about -50%, single value
-64 to -70%) because they select fewer rows and the single value is an exact
filter; label-filter shapes over unpacked fields stay within the loaded
host's noise.

Every route that takes a selector is pinned by a recording-backend test, the
__tenant_id__ matcher by an extended table, and the answers by
TestLabelRegexpMatchesWhatLokiMatches (both VictoriaLogs capability sets),
TestLabelFilterConditionAgreesWithLoki and FuzzSelectorRegexpFilter.
TestCompat_LabelMatcherRegexpAnchoring compares Loki and the proxy over log,
metric, label values, series, volume, detected labels and multi-tenant
requests. Registry: profiles/stream-selector-regexp-anchoring (new) and
profiles/label-filter-regexp-anchoring, both fixed; A/B set
label-matcher-anchoring and three visual fix-proof pages.
@szibis
szibis force-pushed the fix/label-matcher-regexp-anchoring branch from be7bb4d to 73b9ba4 Compare October 28, 2026 13:53
@github-actions

github-actions Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

⚪ Performance A/B: no change beyond noise

190 shape×range — 190 ⚪ same · results vs Loki: 181 same, 0 new difference(s), 9 pre-existing

Loki profiles compared (like for like):

  • aligned: not run (no build supports -align-queries-with-step, so every aligned row would be not like-for-like)
  • loki-defaults: 190 shape×range; proxy flag base off, pr off

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

190 unchanged shape×range (within noise)
shape range Loki profile base p50 PR p50 change Loki p50 PR ÷ Loki result vs Loki
⚪ bytes_over_time by app 1h defaults 8 ms 9 ms ⚪ +12% 76 ms 0.12× ✅ same
⚪ drilldown service landing 1h defaults 5 ms 5 ms ⚪ +0% 37 ms 0.14× ✅ same
⚪ instant json filtered instant defaults 7 ms 7 ms ⚪ +0% 60 ms 0.12× ✅ same
⚪ instant json volume instant defaults 17 ms 17 ms ⚪ +0% 76 ms 0.22× ✅ same
⚪ instant ungrouped rate instant defaults 5 ms 5 ms ⚪ +0% 62 ms 0.08× ✅ same
⚪ json rate by level (no filter, no drop) 1h defaults 400 400 ⚪ -33% 400 — ✅ same
⚪ json volume no filters 1h defaults 23 ms 22 ms ⚪ -4% 68 ms 0.32× ✅ same
⚪ logfmt count by level, filter, drop 1h defaults 8 ms 7 ms ⚪ -12% 54 ms 0.13× ✅ same
⚪ logfmt volume 1h defaults 54 ms 50 ms ⚪ -7% 60 ms 0.83× ✅ same
⚪ logs plain 1h defaults 49 ms 45 ms ⚪ -8% 41 ms 1.10× ✅ same
⚪ logs | json 1h defaults 71 ms 59 ms ⚪ -17% 55 ms 1.07× ✅ same
⚪ logs | logfmt 1h defaults 90 ms 103 ms ⚪ +14% 48 ms 2.15× ✅ same
⚪ stream-label count by app 1h defaults 5 ms 5 ms ⚪ +0% 45 ms 0.11× ✅ same
⚪ topk rate by app 1h defaults 31 ms 26 ms ⚪ -16% 73 ms 0.36× ✅ sum ±0.02%
⚪ ungrouped rate 1h defaults 4 ms 3 ms ⚪ -25% 89 ms 0.03× ✅ same
⚪ DB3 json field breakdown user_id 1h defaults 6 ms 5 ms ⚪ -17% 55 ms 0.09× ✅ same
⚪ DB4 json field breakdown batch_id 1h defaults 36 ms 51 ms ⚪ +42% 48 ms 1.06× ✅ same
⚪ X1 json filter level_extracted, by level 1h defaults 77 ms 82 ms ⚪ +6% 59 ms 1.39× ✅ same
⚪ X2 json group by level_extracted 1h defaults 7 ms 7 ms ⚪ +0% 45 ms 0.16× ✅ same
⚪ X3 json rate, level_extracted equality 1h defaults 6 ms 6 ms ⚪ +0% 38 ms 0.16× ✅ same
⚪ X4 json bytes, level_extracted regexp 1h defaults 66 ms 62 ms ⚪ -6% 86 ms 0.72× ✅ same
⚪ X5 logfmt filter level_extracted, by level 1h defaults 11 ms 10 ms ⚪ -9% 46 ms 0.22× ✅ same
⚪ E1 logs, json parser over stream labels 1h defaults 72 ms 71 ms ⚪ -1% 40 ms 1.77× ✅ same
⚪ E2 logs, logfmt parser over stream labels 1h defaults 48 ms 36 ms ⚪ -25% 31 ms 1.16× ✅ same
⚪ E3 logs, json, existence filter on level_extracted 1h defaults 127 ms 120 ms ⚪ -6% 59 ms 2.03× ✅ same
⚪ E4 logs, logfmt, existence filter on level_extracted 1h defaults 73 ms 61 ms ⚪ -16% 44 ms 1.39× ✅ same
⚪ E5 logs, json, drop level_extracted 1h defaults 68 ms 71 ms ⚪ +4% 40 ms 1.77× ✅ same
⚪ E6 logfmt count by level_extracted 1h defaults 4 ms 4 ms ⚪ +0% 5 ms 0.80× ✅ same
⚪ E7 json count by level_extracted 1h defaults 44 ms 45 ms ⚪ +2% 14 ms 3.21× ✅ same
⚪ E8 instant logfmt by level_extracted instant defaults 4 ms 4 ms ⚪ +0% 6 ms 0.67× ✅ same
⚪ J1 json no-error count by service_name, one service 1h defaults 14 ms 15 ms ⚪ +7% 22 ms 0.68× ✅ same
⚪ J2 json no-error count by app, namespace 1h defaults 32 ms 30 ms ⚪ -6% 69 ms 0.43× ✅ same
⚪ J3 json no-error count by parsed method 1h defaults 25 ms 24 ms ⚪ -4% 21 ms 1.14× ✅ same
⚪ J4 json no-error count by service_name, all services 1h defaults 17 ms 16 ms ⚪ -6% 78 ms 0.21× ✅ same
⚪ J5 json no-error bytes_rate by app 1h defaults 9 ms 8 ms ⚪ -11% 60 ms 0.13× ✅ same
⚪ A explore volume, 2 filters, drop 1h defaults 60 ms 55 ms ⚪ -8% 102 ms 0.54× ✅ same
⚪ B grouped sum, filter, no drop 1h defaults 45 ms 44 ms ⚪ -2% 88 ms 0.50× ✅ same
⚪ C ungrouped sum, filter, no drop 1h defaults 43 ms 42 ms ⚪ -2% 85 ms 0.49× ✅ same
⚪ D volume, no filters 1h defaults 47 ms 47 ms ⚪ +0% 110 ms 0.43× ✅ same
⚪ E field breakdown pipeline (explore) 1h defaults 43 ms 42 ms ⚪ -2% 89 ms 0.47× ✅ same
⚪ F field breakdown service_version (explore) 1h defaults 58 ms 58 ms ⚪ +0% 109 ms 0.53× ✅ same
⚪ G field breakdown pipeline (drilldown) 1h defaults 8 ms 7 ms ⚪ -12% 95 ms 0.07× ✅ same
⚪ H field breakdown service_version (drilldown) 1h defaults 26 ms 26 ms ⚪ +0% 88 ms 0.30× ✅ same
⚪ I grafana volume of B 1h defaults 46 ms 45 ms ⚪ -2% 90 ms 0.50× ✅ same
⚪ M1 multi-value variable, log query 1h defaults 32 ms 34 ms ⚪ +6% 13 ms 2.62× ✅ same
⚪ M2 multi-value variable, count by app 1h defaults 3 ms 3 ms ⚪ +0% 17 ms 0.18× ✅ same
⚪ M3 single-value variable, log query 1h defaults 15 ms 16 ms ⚪ +7% 10 ms 1.60× ✅ same
⚪ M4 alternation over many streams, count 1h defaults 4 ms 3 ms ⚪ -25% 49 ms 0.06× ✅ same
⚪ M5 json label filter, literal alternation 1h defaults 22 ms 23 ms ⚪ +5% 14 ms 1.64× ✅ same
⚪ M6 json label filter, prefix (substring in Loki) 1h defaults 24 ms 21 ms ⚪ -12% 19 ms 1.11× ✅ same
⚪ LF13 explore volume, level filter before json 1h defaults 25 ms 25 ms ⚪ +0% 32 ms 0.78× ✅ same
⚪ LF15 drilldown field breakdown, level filter 1h defaults 6 ms 6 ms ⚪ +0% 6 ms 1.00× ✅ same
⚪ LF16 drilldown label breakdown, level filter 1h defaults 25 ms 26 ms ⚪ +4% 6 ms 4.33× ✅ same
⚪ LF20 logfmt count by detected_level 1h defaults 11 ms 10 ms ⚪ -9% 7 ms 1.43× ✅ same
⚪ LF21 logfmt count by detected_level, level filter 1h defaults 6 ms 6 ms ⚪ +0% 4 ms 1.50× ✅ same
⚪ LF23 json count by level, level filter 1h defaults 26 ms 22 ms ⚪ -15% 12 ms 1.83× ✅ same
⚪ LF24 rate of errors by service 1h defaults 8 ms 8 ms ⚪ +0% 58 ms 0.14× ➖ differs (as base)
⚪ LF25 count of warnings, error label dropped 1h defaults 9 ms 9 ms ⚪ +0% 37 ms 0.24× ➖ differs (as base)
⚪ LF26 topk services by errors 1h defaults 438 ms 480 ms ⚪ +10% 59 ms 8.14× ➖ differs (as base)
⚪ LF28 logs, keep detected_level 1h defaults 27 ms 25 ms ⚪ -7% 13 ms 1.92× ✅ same
⚪ LF29 logs, drop detected_level 1h defaults 38 ms 50 ms ⚪ +32% 13 ms 3.85× ✅ same
⚪ LF30 logs, drop level 1h defaults 42 ms 50 ms ⚪ +19% 14 ms 3.57× ✅ same
⚪ LF31 count by detected_level, level dropped 1h defaults 11 ms 11 ms ⚪ +0% 12 ms 0.92× ✅ same
⚪ LF32 count by detected_level, detected_level kept 1h defaults 6 ms 6 ms ⚪ +0% 10 ms 0.60× ✅ same
⚪ LF7 logs, json then level equals 1h defaults 81 ms 96 ms ⚪ +19% 110 ms 0.87× ✅ same
⚪ LF8 logs, logfmt then level regexp 1h defaults 46 ms 44 ms ⚪ -4% 44 ms 1.00× ✅ same
⚪ LS1 logs, level stream label 1h defaults 32 ms 31 ms ⚪ -3% 49 ms 0.63× ✅ same
⚪ LS2 logs, level stream label regexp, all services 1h defaults 56 ms 61 ms ⚪ +9% 52 ms 1.17× ✅ same
⚪ LS3 logs, level in the selector 1h defaults 26 ms 28 ms ⚪ +8% 15 ms 1.87× ✅ same
⚪ LS4 logs, json then level_extracted 1h defaults 119 ms 107 ms ⚪ -10% 116 ms 0.92× ✅ same
⚪ LS5 logs, logfmt then level_extracted regexp 1h defaults 43 ms 44 ms ⚪ +2% 37 ms 1.19× ✅ same
⚪ LS6 level stream label count 1h defaults 3 ms 3 ms ⚪ +0% 34 ms 0.09× ✅ same
⚪ LS7 rate of level stream label by service 1h defaults 5 ms 4 ms ⚪ -20% 57 ms 0.07× ✅ same
⚪ LS8 json count by level_extracted, collision 1h defaults 42 ms 44 ms ⚪ +5% 12 ms 3.67× ✅ same
⚪ O1 explore logs, underscore structured-metadata filter 1h defaults 201 ms 224 ms ⚪ +11% 231 ms 0.97× ✅ same
⚪ O2 explore log volume (Grafana form), underscore structured-metadata filter 1h defaults 23 ms 23 ms ⚪ +0% 42 ms 0.55× ✅ same
⚪ O3 explore logs, dotted structured-metadata filter 1h defaults 400 400 ⚪ n/a 400 — ✅ same
⚪ O4 explore log volume (Grafana form), dotted structured-metadata filter 1h defaults 400 400 ⚪ n/a 400 — ✅ same
⚪ DL1 drilldown field breakdown json-expr, level filter 1h defaults 11 ms 9 ms ⚪ -18% 36 ms 0.25× ✅ same
⚪ DL2 drilldown field breakdown, level + stream-label filter before json 1h defaults 23 ms 22 ms ⚪ -4% 5 ms 4.40× ✅ same
⚪ DL3 drilldown logfmt field breakdown, level filter 1h defaults 94 ms 74 ms ⚪ -21% 83 ms 0.89× ➖ differs (as base)
⚪ DL4 instant count, level filter, json+logfmt, 1h instant defaults 264 ms 261 ms ⚪ -1% 727 ms 0.36× ✅ sum ±0.27%
⚪ DL6 explore volume, level filter before json 1h defaults 37 ms 30 ms ⚪ -19% 68 ms 0.44× ✅ same
⚪ N1 sum by app of max by app,pod, count 1h defaults 6 ms 6 ms ⚪ +0% 40 ms 0.15× ✅ same
⚪ N10 instant min of avg, rate instant defaults 6 ms 6 ms ⚪ +0% 56 ms 0.11× ✅ same
⚪ N11 instant three levels instant defaults 5 ms 5 ms ⚪ +0% 46 ms 0.11× ✅ same
⚪ N12 instant json inner, count of sum instant defaults 10 ms 10 ms ⚪ +0% 55 ms 0.18× ✅ same
⚪ N13 instant without under by instant defaults 12 ms 12 ms ⚪ +0% 62 ms 0.19× ✅ same
⚪ N14 sliding sum of max 1h defaults 277 ms 290 ms ⚪ +5% 87 ms 3.33× ✅ same
⚪ N15 sliding sums only 1h defaults 7 ms 6 ms ⚪ -14% 52 ms 0.12× ✅ same
⚪ N16 innermost without under by, tumbling 1h defaults 11 ms 9 ms ⚪ -18% 58 ms 0.16× ✅ same
⚪ N2 max by namespace of sum by namespace,app, rate 1h defaults 4 ms 4 ms ⚪ +0% 28 ms 0.14× ✅ same
⚪ N3 count by namespace of sum by namespace,app 1h defaults 4 ms 4 ms ⚪ +0% 28 ms 0.14× ✅ same
⚪ N4 avg by app of min by app,pod 1h defaults 5 ms 5 ms ⚪ +0% 44 ms 0.11× ✅ same
⚪ N5 three levels, max min sum 1h defaults 4 ms 4 ms ⚪ +0% 27 ms 0.15× ✅ same
⚪ N6 json inner, sum of max 1h defaults 5 ms 5 ms ⚪ +0% 31 ms 0.16× ✅ same
⚪ N7 logfmt inner, max of sum 1h defaults 4 ms 4 ms ⚪ +0% 8 ms 0.50× ✅ same
⚪ N8 without over a named by level, tumbling 1h defaults 9 ms 7 ms ⚪ -22% 56 ms 0.12× ✅ same
⚪ N9 instant sum of max instant defaults 6 ms 7 ms ⚪ +17% 56 ms 0.12× ✅ same
⚪ OR1 logs, or alternatives 1h defaults 53 ms 45 ms ⚪ -15% 54 ms 0.83× ✅ same
⚪ OR2 logs, negated or alternatives 1h defaults 47 ms 49 ms ⚪ +4% 56 ms 0.88× ✅ same
⚪ OR3 logs, regexp or alternatives 1h defaults 42 ms 49 ms ⚪ +17% 49 ms 1.00× ✅ same
⚪ OR4 count by level, or alternatives with parser 1h defaults 21 ms 22 ms ⚪ +5% 38 ms 0.58× ✅ same
⚪ OR5 rate, negated regexp or alternatives 1h defaults 4 ms 4 ms ⚪ +0% 68 ms 0.06× ✅ same
⚪ OR6 instant, or alternatives instant defaults 5 ms 5 ms ⚪ +0% 54 ms 0.09× ✅ same
⚪ P1 api-gateway empty pattern 1h defaults 13 ms 12 ms ⚪ -8% 12 ms 1.00× ✅ same
⚪ P2 api-gateway not empty pattern, line filter 1h defaults 39 ms 32 ms ⚪ -18% 15 ms 2.13× ✅ same
⚪ P3 nginx count_over_time, pattern with spaces 1h defaults 3 ms 3 ms ⚪ +0% 3 ms 1.00× ✅ same
⚪ P4 nginx count_over_time, GET request pattern 1h defaults 3 ms 3 ms ⚪ +0% 3 ms 1.00× ✅ same
⚪ A1 nginx include GET, log query 1h defaults 29 ms 29 ms ⚪ +0% 7 ms 4.14× ✅ same
⚪ A2 nginx exclude GET, log query 1h defaults 15 ms 14 ms ⚪ -7% 7 ms 2.00× ✅ same
⚪ A3 nginx include GET, count_over_time 1h defaults 5 ms 5 ms ⚪ +0% 3 ms 1.67× ✅ same
⚪ A4 nginx exclude GET, count_over_time 1h defaults 5 ms 5 ms ⚪ +0% 3 ms 1.67× ✅ same
⚪ A5 drilldown exclude, volume by detected_level 1h defaults 12 ms 10 ms ⚪ -17% 4 ms 2.50× ✅ same
⚪ A6 nginx include ends on a literal, log query 1h defaults 9 ms 9 ms ⚪ +0% 4 ms 2.25× ✅ same
⚪ A7 nginx exclude ends on a literal, log query 1h defaults 15 ms 21 ms ⚪ +40% 8 ms 2.62× ✅ same
⚪ A8 nginx include two literals, count_over_time 1h defaults 5 ms 5 ms ⚪ +0% 3 ms 1.67× ✅ same
⚪ A9 nginx include two literals, ends on a literal, count_over_time 1h defaults 5 ms 5 ms ⚪ +0% 3 ms 1.67× ✅ same
⚪ P1 logs, json, level equals 1h defaults 68 ms 63 ms ⚪ -7% 55 ms 1.15× ✅ same
⚪ P2 logs, logfmt, level not equal 1h defaults 18 ms 16 ms ⚪ -11% 5 ms 3.20× ✅ same
⚪ P3 logs, json, level existence 1h defaults 56 ms 62 ms ⚪ +11% 47 ms 1.32× ✅ same
⚪ P4 logfmt count by level 1h defaults 4 ms 4 ms ⚪ +0% 5 ms 0.80× ✅ same
⚪ P5 json count by level 1h defaults 22 ms 21 ms ⚪ -5% 13 ms 1.62× ✅ same
⚪ P6 json count, level filter 1h defaults 4 ms 4 ms ⚪ +0% 10 ms 0.40× ✅ same
⚪ P7 json rate by level, level regexp 1h defaults 4 ms 4 ms ⚪ +0% 10 ms 0.40× ✅ same
⚪ P8 json count by a name that is no stream label 1h defaults 23 ms 23 ms ⚪ +0% 12 ms 1.92× ✅ same
⚪ P9 instant logfmt by level instant defaults 4 ms 4 ms ⚪ +0% 6 ms 0.67× ✅ same
⚪ SF1 logs, json extraction list 1h defaults 76 ms 77 ms ⚪ +1% 47 ms 1.64× ✅ same
⚪ SF10 volume, structured-metadata filter 1h defaults 7 ms 6 ms ⚪ -14% 36 ms 0.17× ✅ same
⚪ SF2 logs, logfmt extraction list 1h defaults 118 ms 111 ms ⚪ -6% 47 ms 2.36× ✅ same
⚪ SF3 logs, json then line_format 1h defaults 76 ms 87 ms ⚪ +14% 57 ms 1.53× ✅ same
⚪ SF4 logs, line_format without a parser 1h defaults 65 ms 61 ms ⚪ -6% 42 ms 1.45× ✅ same
⚪ SF5 logs, regexp capture 1h defaults 119 ms 120 ms ⚪ +1% 49 ms 2.45× ✅ same
⚪ SF6 logs, structured-metadata filter 1h defaults 40 ms 39 ms ⚪ -3% 91 ms 0.43× ✅ same
⚪ SF7 logs, filter on a line key without a parser 1h defaults 36 ms 36 ms ⚪ +0% 33 ms 1.09× ✅ same
⚪ SF8 volume, json extraction list 1h defaults 4 ms 4 ms ⚪ +0% 46 ms 0.09× ✅ same
⚪ SF9 volume, logfmt label_format rename 1h defaults 9 ms 8 ms ⚪ -11% 55 ms 0.15× ✅ same
⚪ E1 json duration_ms sum by service 1h defaults 5 ms 5 ms ⚪ +0% 16 ms 0.31× ✅ same
⚪ E2 json duration_ms p95 by service 1h defaults 73 ms 66 ms ⚪ -10% 13 ms 5.08× ✅ same
⚪ E3 logfmt latency_ms rate, production 1h defaults 9 ms 9 ms ⚪ +0% 107 ms 0.08× ➖ differs (as base)
⚪ E4 logfmt size_bytes max by service 1h defaults 4 ms 4 ms ⚪ +0% 9 ms 0.44× ✅ same
⚪ E5 json duration_ms sum, instant instant defaults 18 ms 17 ms ⚪ -6% 20 ms 0.85× ✅ same
⚪ E6 logfmt ttl unit string (Loki 400) 1h defaults 400 400 ⚪ +0% 400 — ✅ same
⚪ E7 json hexadecimal trace_id (Loki 400) 1h defaults 400 400 ⚪ +0% 400 — ✅ same
⚪ E8 json duration_ms sum by service, namespace prod 1h defaults 7 ms 6 ms ⚪ -14% 76 ms 0.08× ✅ same
⚪ E9 logfmt latency_ms sum by service, namespace prod 1h defaults 7 ms 7 ms ⚪ +0% 65 ms 0.11× ✅ same
⚪ G1 logfmt amount sum_over_time, no filter 1h defaults 6 ms 6 ms ⚪ +0% 10 ms 0.60× ✅ same
⚪ G10 logfmt latency_ms sum_over_time, production 1h defaults 11 ms 10 ms ⚪ -9% 93 ms 0.11× ➖ differs (as base)
⚪ G11 logfmt amount sum_over_time, instant instant defaults 4 ms 4 ms ⚪ +0% 8 ms 0.50× ✅ same
⚪ G12 json duration_ms sum_over_time, instant instant defaults 17 ms 18 ms ⚪ +6% 19 ms 0.95× ✅ same
⚪ G2 json duration_ms sum_over_time, no filter 1h defaults 14 ms 13 ms ⚪ -7% 26 ms 0.50× ✅ same
⚪ G3 logfmt amount sum_over_time, error filter 1h defaults 6 ms 6 ms ⚪ +0% 8 ms 0.75× ✅ same
⚪ G4 json duration_ms sum_over_time, error filter 1h defaults 14 ms 14 ms ⚪ +0% 22 ms 0.64× ✅ same
⚪ G5 logfmt amount sum by rate 1h defaults 6 ms 6 ms ⚪ +0% 9 ms 0.67× ✅ same
⚪ G6 json duration_ms sum by rate 1h defaults 7 ms 7 ms ⚪ +0% 21 ms 0.33× ✅ same
⚪ G7 duration() of a unit string 1h defaults 11 ms 11 ms ⚪ +0% 6 ms 1.83× ✅ same
⚪ G8 bytes() of a number 1h defaults 9 ms 10 ms ⚪ +11% 6 ms 1.67× ✅ same
⚪ G9 json duration_ms max_over_time 1h defaults 7 ms 6 ms ⚪ -14% 85 ms 0.07× ✅ same
⚪ W1 sum without pod, max_over_time 1h defaults 25 ms 25 ms ⚪ +0% 15 ms 1.67× ✅ same
⚪ W2 sum without pod, sum_over_time 1h defaults 27 ms 29 ms ⚪ +7% 12 ms 2.42× ✅ same
⚪ W3 sum without pod, rate 1h defaults 33 ms 32 ms ⚪ -3% 12 ms 2.67× ✅ same
⚪ W4 max without pod, max_over_time 1h defaults 20 ms 19 ms ⚪ -5% 12 ms 1.58× ✅ same
⚪ W5 sum without pod, max_over_time, instant instant defaults 5 ms 5 ms ⚪ +0% 10 ms 0.50× ✅ same
⚪ W6 sum without pod, rate, instant instant defaults 5 ms 4 ms ⚪ -20% 9 ms 0.44× ✅ same
⚪ W7 topk 5 of sum without pod 1h defaults 20 ms 20 ms ⚪ +0% 13 ms 1.54× ✅ same
⚪ W8 sum without pod, count_over_time (production) 1h defaults 183 ms 173 ms ⚪ -5% 48 ms 3.60× ✅ same
⚪ W9 label_replace control, instant instant defaults 4 ms 4 ms ⚪ +0% 59 ms 0.07× ✅ same
⚪ U1 logfmt amount rate 1h defaults 5 ms 4 ms ⚪ -20% 11 ms 0.36× ✅ same
⚪ U2 json duration_ms rate 1h defaults 5 ms 4 ms ⚪ -20% 21 ms 0.19× ✅ same
⚪ U3 json duration_ms rate, error filter 1h defaults 5 ms 5 ms ⚪ +0% 19 ms 0.26× ✅ same
⚪ U4 logfmt latency_ms rate, production 1h defaults 8 ms 7 ms ⚪ -12% 109 ms 0.06× ➖ differs (as base)
⚪ U5 logfmt amount rate, instant instant defaults 4 ms 4 ms ⚪ +0% 8 ms 0.50× ✅ same
⚪ U6 json duration_ms rate, instant instant defaults 7 ms 6 ms ⚪ -14% 16 ms 0.38× ✅ same
⚪ U7 json duration_ms sum_over_time, instant instant defaults 9 ms 9 ms ⚪ +0% 17 ms 0.53× ✅ same
⚪ U8 logfmt amount rate by service (grouped) 1h defaults 4 ms 4 ms ⚪ +0% 9 ms 0.44× ✅ same
⚪ U9 logfmt amount rate_counter (folded per series) 1h defaults 119 ms 122 ms ⚪ +3% 26 ms 4.69× ➖ differs (as base)
⚪ U1 json duration_ms sum by service 1h defaults 5 ms 5 ms ⚪ +0% 13 ms 0.38× ✅ same
⚪ U10 instant sum_over_time unwrap instant defaults 17 ms 17 ms ⚪ +0% 20 ms 0.85× ✅ same
⚪ U11 instant max_over_time missing label instant defaults 4 ms 3 ms ⚪ -25% 11 ms 0.27× ✅ same
⚪ U12 json duration_ms over every service 1h defaults 6 ms 6 ms ⚪ +0% 61 ms 0.10× ✅ same
⚪ U13 logfmt latency_ms over every service 1h defaults 9 ms 8 ms ⚪ -11% 53 ms 0.15× ➖ differs (as base)
⚪ U14 duration() of a unit string 1h defaults 10 ms 11 ms ⚪ +10% 4 ms 2.75× ✅ same
⚪ U2 json duration_ms max by service 1h defaults 8 ms 5 ms ⚪ -38% 74 ms 0.07× ✅ same
⚪ U3 json duration_ms sliding sum 1h defaults 5 ms 4 ms ⚪ -20% 22 ms 0.18× ✅ same
⚪ U4 logfmt size_bytes sum by service 1h defaults 4 ms 4 ms ⚪ +0% 4 ms 1.00× ✅ same
⚪ U5 logfmt ttl unit string (no sample) 1h defaults 4 ms 4 ms ⚪ +0% 3 ms 1.33× ✅ same
⚪ U6 missing label, no parser 1h defaults 3 ms 3 ms ⚪ +0% 8 ms 0.38× ✅ same
⚪ U7 missing label, ungrouped sum 1h defaults 3 ms 3 ms ⚪ +0% 8 ms 0.38× ✅ same
⚪ U8 rate of an unwrapped label 1h defaults 5 ms 5 ms ⚪ +0% 12 ms 0.42× ✅ same
⚪ U9 bytes() conversion 1h defaults 8 ms 8 ms ⚪ +0% 6 ms 1.33× ✅ same
Cold (first-run) timings — what a user opening a panel sees
shape range Loki profile base cold PR cold Loki cold PR ÷ Loki cold
bytes_over_time by app 1h defaults 35 ms 34 ms 574 ms 0.06×
drilldown service landing 1h defaults 17 ms 17 ms 334 ms 0.05×
instant json filtered instant defaults 6 ms 6 ms 63 ms 0.10×
instant json volume instant defaults 15 ms 15 ms 77 ms 0.19×
instant ungrouped rate instant defaults 5 ms 5 ms 63 ms 0.08×
json rate by level (no filter, no drop) 1h defaults 400 400 400 —
json volume no filters 1h defaults 35 ms 31 ms 439 ms ↺ 0.07×
logfmt count by level, filter, drop 1h defaults 276 ms 229 ms 540 ms 0.42×
logfmt volume 1h defaults 111 ms 113 ms 843 ms 0.13×
logs plain 1h defaults 52 ms 38 ms 63 ms 0.60×
logs | json 1h defaults 68 ms 60 ms 53 ms 1.13×
logs | logfmt 1h defaults 100 ms 96 ms 47 ms 2.04×
stream-label count by app 1h defaults 15 ms 12 ms 339 ms 0.04×
topk rate by app 1h defaults 24 ms 25 ms 384 ms 0.07×
ungrouped rate 1h defaults 7 ms 6 ms 364 ms 0.02×
DB3 json field breakdown user_id 1h defaults 33 ms 36 ms 456 ms 0.08×
DB4 json field breakdown batch_id 1h defaults 58 ms 55 ms 592 ms 0.09×
X1 json filter level_extracted, by level 1h defaults 1.72 s 1.69 s 600 ms 2.81×
X2 json group by level_extracted 1h defaults 171 ms 161 ms 414 ms 0.39×
X3 json rate, level_extracted equality 1h defaults 144 ms 145 ms 304 ms 0.48×
X4 json bytes, level_extracted regexp 1h defaults 219 ms 200 ms 362 ms 0.55×
X5 logfmt filter level_extracted, by level 1h defaults 348 ms 356 ms 630 ms 0.57×
E1 logs, json parser over stream labels 1h defaults 76 ms 74 ms 46 ms † —
E2 logs, logfmt parser over stream labels 1h defaults 39 ms 58 ms 32 ms † —
E3 logs, json, existence filter on level_extracted 1h defaults 163 ms 92 ms 63 ms † —
E4 logs, logfmt, existence filter on level_extracted 1h defaults 85 ms 58 ms 43 ms † —
E5 logs, json, drop level_extracted 1h defaults 105 ms 79 ms 44 ms † —
E6 logfmt count by level_extracted 1h defaults 25 ms 25 ms 25 ms 1.00×
E7 json count by level_extracted 1h defaults 706 ms 688 ms 93 ms 7.40×
E8 instant logfmt by level_extracted instant defaults 5 ms 4 ms 6 ms 0.67×
J1 json no-error count by service_name, one service 1h defaults 104 ms 101 ms 108 ms 0.94×
J2 json no-error count by app, namespace 1h defaults 230 ms 225 ms 477 ms 0.47×
J3 json no-error count by parsed method 1h defaults 91 ms 77 ms 97 ms 0.79×
J4 json no-error count by service_name, all services 1h defaults 261 ms 282 ms 418 ms 0.67×
J5 json no-error bytes_rate by app 1h defaults 137 ms 150 ms 281 ms 0.53×
A explore volume, 2 filters, drop 1h defaults 769 ms 742 ms 371 ms 2.00×
B grouped sum, filter, no drop 1h defaults 81 ms 70 ms 369 ms 0.19×
C ungrouped sum, filter, no drop 1h defaults 65 ms 66 ms 331 ms 0.20×
D volume, no filters 1h defaults 119 ms 104 ms 439 ms 0.24×
E field breakdown pipeline (explore) 1h defaults 62 ms 65 ms 342 ms 0.19×
F field breakdown service_version (explore) 1h defaults 723 ms 756 ms 390 ms 1.94×
G field breakdown pipeline (drilldown) 1h defaults 27 ms 25 ms 505 ms 0.05×
H field breakdown service_version (drilldown) 1h defaults 44 ms 53 ms 355 ms 0.15×
I grafana volume of B 1h defaults 63 ms 61 ms 349 ms 0.17×
M1 multi-value variable, log query 1h defaults 42 ms 30 ms 20 ms 1.50×
M2 multi-value variable, count by app 1h defaults 7 ms 7 ms 76 ms 0.09×
M3 single-value variable, log query 1h defaults 16 ms 14 ms 13 ms 1.08×
M4 alternation over many streams, count 1h defaults 7 ms 7 ms 220 ms 0.03×
M5 json label filter, literal alternation 1h defaults 62 ms 59 ms 83 ms 0.71×
M6 json label filter, prefix (substring in Loki) 1h defaults 31 ms 24 ms 20 ms † —
LF13 explore volume, level filter before json 1h defaults 64 ms 60 ms 318 ms 0.19×
LF15 drilldown field breakdown, level filter 1h defaults 51 ms 49 ms 67 ms 0.73×
LF16 drilldown label breakdown, level filter 1h defaults 33 ms 46 ms 32 ms 1.44×
LF20 logfmt count by detected_level 1h defaults 28 ms 28 ms 39 ms 0.72×
LF21 logfmt count by detected_level, level filter 1h defaults 20 ms 20 ms 31 ms 0.65×
LF23 json count by level, level filter 1h defaults 30 ms 30 ms 102 ms 0.29×
LF24 rate of errors by service 1h defaults 226 ms 227 ms 351 ms 0.65×
LF25 count of warnings, error label dropped 1h defaults 235 ms 219 ms 499 ms 0.44×
LF26 topk services by errors 1h defaults 445 ms 448 ms 349 ms 1.28×
LF28 logs, keep detected_level 1h defaults 32 ms 25 ms 21 ms † —
LF29 logs, drop detected_level 1h defaults 46 ms 36 ms 20 ms 1.80×
LF30 logs, drop level 1h defaults 36 ms 46 ms 21 ms 2.19×
LF31 count by detected_level, level dropped 1h defaults 29 ms 28 ms 86 ms 0.33×
LF32 count by detected_level, detected_level kept 1h defaults 16 ms 16 ms 101 ms 0.16×
LF7 logs, json then level equals 1h defaults 84 ms 79 ms 178 ms † —
LF8 logs, logfmt then level regexp 1h defaults 45 ms 49 ms 43 ms † —
LS1 logs, level stream label 1h defaults 47 ms 30 ms 44 ms 0.68×
LS2 logs, level stream label regexp, all services 1h defaults 63 ms 61 ms 58 ms 1.05×
LS3 logs, level in the selector 1h defaults 25 ms 27 ms 17 ms 1.59×
LS4 logs, json then level_extracted 1h defaults 100 ms 98 ms 105 ms † —
LS5 logs, logfmt then level_extracted regexp 1h defaults 57 ms 45 ms 38 ms † —
LS6 level stream label count 1h defaults 13 ms 7 ms 286 ms 0.02×
LS7 rate of level stream label by service 1h defaults 11 ms 10 ms 303 ms 0.03×
LS8 json count by level_extracted, collision 1h defaults 675 ms 687 ms 91 ms 7.55×
O1 explore logs, underscore structured-metadata filter 1h defaults 223 ms 210 ms 236 ms † —
O2 explore log volume (Grafana form), underscore structured-metadata filter 1h defaults 81 ms 84 ms 387 ms 0.22×
O3 explore logs, dotted structured-metadata filter 1h defaults 400 400 400 —
O4 explore log volume (Grafana form), dotted structured-metadata filter 1h defaults 400 400 400 —
DL1 drilldown field breakdown json-expr, level filter 1h defaults 277 ms 258 ms 328 ms 0.79×
DL2 drilldown field breakdown, level + stream-label filter before json 1h defaults 28 ms 29 ms 20 ms 1.45×
DL3 drilldown logfmt field breakdown, level filter 1h defaults 366 ms 370 ms 730 ms 0.51×
DL4 instant count, level filter, json+logfmt, 1h instant defaults 257 ms 254 ms 797 ms 0.32×
DL6 explore volume, level filter before json 1h defaults 113 ms 104 ms 465 ms 0.22×
N1 sum by app of max by app,pod, count 1h defaults 40 ms 30 ms 466 ms 0.06×
N10 instant min of avg, rate instant defaults 6 ms 6 ms 54 ms 0.11×
N11 instant three levels instant defaults 6 ms 5 ms 46 ms 0.11×
N12 instant json inner, count of sum instant defaults 9 ms 9 ms 54 ms † —
N13 instant without under by instant defaults 12 ms 12 ms 55 ms 0.22×
N14 sliding sum of max 1h defaults 346 ms 335 ms 746 ms 0.45×
N15 sliding sums only 1h defaults 14 ms 13 ms 243 ms 0.05×
N16 innermost without under by, tumbling 1h defaults 41 ms 39 ms 403 ms 0.10×
N2 max by namespace of sum by namespace,app, rate 1h defaults 17 ms 12 ms 207 ms 0.06×
N3 count by namespace of sum by namespace,app 1h defaults 10 ms 10 ms 209 ms † —
N4 avg by app of min by app,pod 1h defaults 31 ms 37 ms 288 ms 0.13×
N5 three levels, max min sum 1h defaults 20 ms 19 ms 312 ms 0.06×
N6 json inner, sum of max 1h defaults 91 ms 71 ms 202 ms 0.35×
N7 logfmt inner, max of sum 1h defaults 13 ms 12 ms 38 ms 0.32×
N8 without over a named by level, tumbling 1h defaults 31 ms 40 ms 417 ms 0.10×
N9 instant sum of max instant defaults 11 ms 11 ms 71 ms 0.15×
OR1 logs, or alternatives 1h defaults 52 ms 42 ms 49 ms 0.86×
OR2 logs, negated or alternatives 1h defaults 37 ms 45 ms 42 ms 1.07×
OR3 logs, regexp or alternatives 1h defaults 57 ms 42 ms 55 ms 0.76×
OR4 count by level, or alternatives with parser 1h defaults 71 ms 70 ms 306 ms 0.23×
OR5 rate, negated regexp or alternatives 1h defaults 32 ms 33 ms 424 ms 0.08×
OR6 instant, or alternatives instant defaults 5 ms 5 ms 54 ms 0.09×
P1 api-gateway empty pattern 1h defaults 18 ms 13 ms 63 ms † —
P2 api-gateway not empty pattern, line filter 1h defaults 40 ms 39 ms 17 ms 2.29×
P3 nginx count_over_time, pattern with spaces 1h defaults 7 ms 7 ms 18 ms † —
P4 nginx count_over_time, GET request pattern 1h defaults 7 ms 13 ms 20 ms 0.65×
A1 nginx include GET, log query 1h defaults 35 ms 28 ms 8 ms 3.50×
A2 nginx exclude GET, log query 1h defaults 16 ms 14 ms 7 ms † —
A3 nginx include GET, count_over_time 1h defaults 6 ms 6 ms 20 ms ↺ 0.30×
A4 nginx exclude GET, count_over_time 1h defaults 10 ms 13 ms 20 ms 0.65×
A5 drilldown exclude, volume by detected_level 1h defaults 24 ms 24 ms 20 ms 1.20×
A6 nginx include ends on a literal, log query 1h defaults 9 ms 8 ms 14 ms † —
A7 nginx exclude ends on a literal, log query 1h defaults 16 ms 14 ms 9 ms 1.56×
A8 nginx include two literals, count_over_time 1h defaults 12 ms 12 ms 17 ms 0.71×
A9 nginx include two literals, ends on a literal, count_over_time 1h defaults 13 ms 12 ms 15 ms † —
P1 logs, json, level equals 1h defaults 60 ms 70 ms 52 ms † —
P2 logs, logfmt, level not equal 1h defaults 18 ms 18 ms 27 ms † —
P3 logs, json, level existence 1h defaults 65 ms 55 ms 47 ms † —
P4 logfmt count by level 1h defaults 19 ms 17 ms 31 ms 0.55×
P5 json count by level 1h defaults 47 ms 47 ms 138 ms 0.34×
P6 json count, level filter 1h defaults 11 ms 9 ms 121 ms 0.07×
P7 json rate by level, level regexp 1h defaults 18 ms 10 ms 132 ms 0.08×
P8 json count by a name that is no stream label 1h defaults 51 ms 48 ms 111 ms 0.43×
P9 instant logfmt by level instant defaults 8 ms 4 ms 6 ms 0.67×
SF1 logs, json extraction list 1h defaults 93 ms 92 ms 46 ms 2.00×
SF10 volume, structured-metadata filter 1h defaults 12 ms 18 ms 236 ms 0.08×
SF2 logs, logfmt extraction list 1h defaults 134 ms 136 ms 48 ms 2.83×
SF3 logs, json then line_format 1h defaults 89 ms 93 ms 148 ms 0.63×
SF4 logs, line_format without a parser 1h defaults 67 ms 64 ms 42 ms 1.52×
SF5 logs, regexp capture 1h defaults 138 ms 133 ms 50 ms 2.66×
SF6 logs, structured-metadata filter 1h defaults 36 ms 37 ms 94 ms † —
SF7 logs, filter on a line key without a parser 1h defaults 45 ms 39 ms 248 ms † —
SF8 volume, json extraction list 1h defaults 11 ms 26 ms 609 ms 0.04×
SF9 volume, logfmt label_format rename 1h defaults 279 ms 280 ms 612 ms 0.46×
E1 json duration_ms sum by service 1h defaults 48 ms 43 ms 89 ms 0.48×
E2 json duration_ms p95 by service 1h defaults 142 ms 156 ms 114 ms 1.37×
E3 logfmt latency_ms rate, production 1h defaults 254 ms 255 ms 506 ms 0.50×
E4 logfmt size_bytes max by service 1h defaults 25 ms 24 ms 113 ms 0.21×
E5 json duration_ms sum, instant instant defaults 17 ms 24 ms 20 ms 1.20×
E6 logfmt ttl unit string (Loki 400) 1h defaults 400 400 400 —
E7 json hexadecimal trace_id (Loki 400) 1h defaults 400 400 400 —
E8 json duration_ms sum by service, namespace prod 1h defaults 102 ms 114 ms 495 ms 0.23×
E9 logfmt latency_ms sum by service, namespace prod 1h defaults 172 ms 167 ms 347 ms 0.48×
G1 logfmt amount sum_over_time, no filter 1h defaults 28 ms 22 ms 38 ms 0.58×
G10 logfmt latency_ms sum_over_time, production 1h defaults 279 ms 275 ms 604 ms 0.46×
G11 logfmt amount sum_over_time, instant instant defaults 9 ms 5 ms 9 ms 0.56×
G12 json duration_ms sum_over_time, instant instant defaults 18 ms 19 ms 19 ms 1.00×
G2 json duration_ms sum_over_time, no filter 1h defaults 48 ms 46 ms 132 ms 0.35×
G3 logfmt amount sum_over_time, error filter 1h defaults 26 ms 22 ms 37 ms 0.59×
G4 json duration_ms sum_over_time, error filter 1h defaults 58 ms 47 ms 174 ms ↺ 0.27×
G5 logfmt amount sum by rate 1h defaults 21 ms 20 ms 40 ms ↺ 0.50×
G6 json duration_ms sum by rate 1h defaults 49 ms 47 ms 122 ms 0.39×
G7 duration() of a unit string 1h defaults 35 ms 35 ms 32 ms 1.09×
G8 bytes() of a number 1h defaults 48 ms 35 ms 31 ms 1.13×
G9 json duration_ms max_over_time 1h defaults 47 ms 47 ms 1.27 s 0.04×
W1 sum without pod, max_over_time 1h defaults 71 ms 47 ms 55 ms 0.85×
W2 sum without pod, sum_over_time 1h defaults 43 ms 41 ms 47 ms 0.87×
W3 sum without pod, rate 1h defaults 48 ms 49 ms 46 ms 1.07×
W4 max without pod, max_over_time 1h defaults 20 ms 20 ms 61 ms 0.33×
W5 sum without pod, max_over_time, instant instant defaults 5 ms 8 ms 11 ms 0.73×
W6 sum without pod, rate, instant instant defaults 4 ms 5 ms 9 ms 0.56×
W7 topk 5 of sum without pod 1h defaults 19 ms 20 ms 56 ms 0.36×
W8 sum without pod, count_over_time (production) 1h defaults 219 ms 208 ms 282 ms 0.74×
W9 label_replace control, instant instant defaults 4 ms 4 ms 59 ms 0.07×
U1 logfmt amount rate 1h defaults 24 ms 18 ms 38 ms 0.47×
U2 json duration_ms rate 1h defaults 41 ms 41 ms 94 ms 0.44×
U3 json duration_ms rate, error filter 1h defaults 50 ms 44 ms 114 ms 0.39×
U4 logfmt latency_ms rate, production 1h defaults 178 ms 178 ms 606 ms 0.29×
U5 logfmt amount rate, instant instant defaults 5 ms 5 ms 9 ms 0.56×
U6 json duration_ms rate, instant instant defaults 6 ms 6 ms 17 ms 0.35×
U7 json duration_ms sum_over_time, instant instant defaults 8 ms 12 ms 17 ms 0.71×
U8 logfmt amount rate by service (grouped) 1h defaults 21 ms 21 ms 40 ms 0.53×
U9 logfmt amount rate_counter (folded per series) 1h defaults 126 ms 123 ms 455 ms † —
U1 json duration_ms sum by service 1h defaults 51 ms 46 ms 128 ms 0.36×
U10 instant sum_over_time unwrap instant defaults 16 ms 18 ms 20 ms 0.90×
U11 instant max_over_time missing label instant defaults 4 ms 4 ms 12 ms † —
U12 json duration_ms over every service 1h defaults 126 ms 135 ms 455 ms 0.30×
U13 logfmt latency_ms over every service 1h defaults 255 ms 254 ms 653 ms 0.39×
U14 duration() of a unit string 1h defaults 32 ms 31 ms 29 ms 1.07×
U2 json duration_ms max by service 1h defaults 46 ms 44 ms 404 ms 0.11×
U3 json duration_ms sliding sum 1h defaults 8 ms 10 ms 174 ms 0.06×
U4 logfmt size_bytes sum by service 1h defaults 25 ms 26 ms 28 ms 0.93×
U5 logfmt ttl unit string (no sample) 1h defaults 22 ms 23 ms 26 ms † —
U6 missing label, no parser 1h defaults 9 ms 9 ms 55 ms † —
U7 missing label, ungrouped sum 1h defaults 8 ms 8 ms 53 ms † —
U8 rate of an unwrapped label 1h defaults 45 ms 46 ms 124 ms 0.37×
U9 bytes() conversion 1h defaults 35 ms 38 ms 35 ms 1.09×

† Loki's results cache answered this cold run (its own counters, or identical answers for windows a minute apart): not a computed time, so it is in no ratio.

↺ Loki cold taken from the first set that ran the same query; Loki's cache answered this one.

14 shape×range above 2× Loki already (listed, or as slow on the base build)
profile mode shape range PR Loki PR ÷ Loki base ÷ Loki listed ÷ Loki why
known defaults warm logs | logfmt 1h 103 ms 48 ms 2.15× 1.88× — within noise of base
known defaults cold SF2 logs, logfmt extraction list 1h 136 ms 48 ms 2.83× 2.79× — as slow on base
known defaults warm SF2 logs, logfmt extraction list 1h 111 ms 47 ms 2.36× 2.51× — as slow on base
known defaults cold SF5 logs, regexp capture 1h 133 ms 50 ms 2.66× 2.76× — as slow on base
known defaults warm SF5 logs, regexp capture 1h 120 ms 49 ms 2.45× 2.43× — as slow on base
known defaults warm E3 logs, json, existence filter on level_extracted 1h 120 ms 59 ms 2.03× 2.15× — as slow on base
known defaults cold E7 json count by level_extracted 1h 688 ms 93 ms 7.40× 7.59× — as slow on base
known defaults cold X1 json filter level_extracted, by level 1h 1.69 s 600 ms 2.81× 2.86× — as slow on base
known defaults warm E2 json duration_ms p95 by service 1h 66 ms 13 ms 5.08× 5.62× — as slow on base
known defaults warm U9 logfmt amount rate_counter (folded per series) 1h 122 ms 26 ms 4.69× 4.58× — as slow on base
known defaults warm W8 sum without pod, count_over_time (production) 1h 173 ms 48 ms 3.60× 3.81× — as slow on base
known defaults warm LF26 topk services by errors 1h 480 ms 59 ms 8.14× 7.42× — as slow on base
known defaults cold LS8 json count by level_extracted, collision 1h 687 ms 91 ms 7.55× 7.42× — as slow on base
known defaults warm N14 sliding sum of max 1h 290 ms 87 ms 3.33× 3.18× — as slow on base
Why these shapes
  • internal/translator/translator.go → 168: 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/loki_profile.go → 105: control/instant json filtered, control/instant json volume, control/json rate by level (no filter, no drop), control/json volume no filters, extracted-pushdown/X1 json filter level_extracted, by level, extracted-pushdown/X2 json group by level_extracted, extracted-pushdown/X3 json rate, level_extracted equality, extracted-pushdown/X4 json bytes, level_extracted regexp …
  • internal/proxy/ordered_json_metric.go → 102: control/bytes_over_time by app, control/instant json filtered, control/instant json volume, control/json rate by level (no filter, no drop), control/json volume no filters, control/logfmt count by level, filter, drop, control/logfmt volume, control/logs | json …
  • internal/translator/parsed_key_variants.go → 86: control/instant json filtered, control/instant json volume, control/json rate by level (no filter, no drop), control/json volume no filters, extracted-pushdown/X1 json filter level_extracted, by level, extracted-pushdown/X2 json group by level_extracted, extracted-pushdown/X3 json rate, level_extracted equality, extracted-pushdown/X4 json bytes, level_extracted regexp …
  • 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/translator/label_matcher_regexp.go → 6: label-matcher-anchoring/M1 multi-value variable, log query, label-matcher-anchoring/M2 multi-value variable, count by app, label-matcher-anchoring/M3 single-value variable, log query, label-matcher-anchoring/M4 alternation over many streams, count, label-matcher-anchoring/M5 json label filter, literal alternation, label-matcher-anchoring/M6 json label filter, prefix (substring in Loki)
  • shape added or edited → 6: label-matcher-anchoring/M1 multi-value variable, log query, label-matcher-anchoring/M2 multi-value variable, count by app, label-matcher-anchoring/M3 single-value variable, log query, label-matcher-anchoring/M4 alternation over many streams, count, label-matcher-anchoring/M5 json label filter, literal alternation, label-matcher-anchoring/M6 json label filter, prefix (substring in Loki)
  • internal/proxy/multitenant.go → 4: label-matcher-anchoring/M1 multi-value variable, log query, label-matcher-anchoring/M2 multi-value variable, count by app, label-matcher-anchoring/M3 single-value variable, log query, label-matcher-anchoring/M4 alternation over many streams, count

base 4e8ee746ac → PR d8156c1a39 · 4 runs (first is cold), warm p50 · ranges 1h · noise ±30% and 100 ms · VictoriaLogs restarts 0→0 · 5.4 min · phases stack_up 40s, seed_and_build 76s, loki-defaults:loki_profile 0s, loki-defaults:proxies 2s, loki-defaults:matrix 190s. Same runner, one fresh stack, targets interleaved per request. How to read and reproduce: bench/ab/README.md.

@github-actions github-actions Bot added size/XL Extra large change and removed size/XL Extra large change labels Oct 28, 2026
@github-actions

github-actions Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

PR Quality Report

Compared against base branch main.

Coverage and tests

Signal Base PR Delta
Test count 16603 18090 1487
Coverage 88.6% 88.7% +0.1% (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 2205.0 ns/op 2199.0 ns/op -0.3% (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 2652.0 ns/op 2614.0 ns/op -1.4% (stable)
QueryRange cache-bypass memory 1282.0 B/op 1284.0 B/op +0.2% (stable)
QueryRange cache-bypass allocations 27.0 allocs/op 27.0 allocs/op 0.0% (stable)
Labels cache-hit CPU cost 641.2 ns/op 623.2 ns/op -2.8% (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 1057.0 ns/op 988.5 ns/op -6.5% (stable)
Labels cache-bypass memory 397.0 B/op 396.0 B/op -0.3% (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 28, 2026 •

Copy link
Copy Markdown
Contributor

✅ Visual smoke: passed with warnings

Base 4e8ee746ac vs PR c89a351be9, 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-logs-multivalue-regexp 15m, explore-logs-multivalue-regexp 1h, explore-volume-label-filter-regexp 15m, explore-volume-label-filter-regexp 1h, explore-volume-label-filter-regexp 6h, explore-volume-multivalue-regexp 15m, explore-volume-multivalue-regexp 1h.
Core set: 5 capture(s) at 1h. Detailed set: 27 entries (explore-logs-multivalue-regexp, explore-volume-multivalue-regexp, explore-volume-label-filter-regexp, explore-metric-plain, lvl-dd-fields-pill, lvl-dd-field-method-pill, lvl-dd-field-pipeline-pill, lvl-explore-volume-dl-prefilter-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-json-extraction, 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-format, 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, explore-unwrap-quantile-over-time, explore-json-error-filter-metric, explore-unwrap-first-last-under-sum, explore-max-by-count-over-time-sliding, explore-nested-aggregation-outer-by, explore-without-over-inner-aggregation, explore-without-nested-under-aggregation, dd-field-service_version, dd-field-pipeline, explore-sum-without-rate, explore-topk-sum-without, explore-sum-without-count, explore-topk-without-keeps-labels.
Not run (capture budget): explore-topk-without-keeps-labels, explore-sum-without-count, explore-topk-sum-without, explore-sum-without-rate, dd-field-pipeline, dd-field-service_version, explore-without-nested-under-aggregation, explore-without-over-inner-aggregation, explore-nested-aggregation-outer-by, explore-max-by-count-over-time-sliding, explore-unwrap-first-last-under-sum, explore-json-error-filter-metric, explore-unwrap-quantile-over-time, explore-pattern-filter-anchored, explore-empty-pattern-filter, explore-unwrap-rate-sum, explore-unwrap-conversion-error, explore-unwrap-missing-label, explore-unwrap-unit-string, explore-unwrap-sum, explore-metric-line-filter-or, explore-logs-line-filter-or, explore-logs-line-format, explore-plain-level-metric, explore-logs-plain-level, explore-extracted-group-metric, explore-extracted-filter-metric, explore-extracted-metric, explore-logs-extracted, explore-logs-json-extraction, explore-logfmt-metric, explore-logs-json, explore-F, explore-E, explore-C, explore-B, explore-A, lvl-explore-volume-level-extracted-stored.

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).

profiles/label-filter-regexp-anchoring: 🟡 improved

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

explore-volume-label-filter-regexp-15m

explore-volume-label-filter-regexp-1h: improved

explore-volume-label-filter-regexp-1h

explore-volume-label-filter-regexp-6h: undecided

explore-volume-label-filter-regexp-6h

profiles/stream-selector-regexp-anchoring: 🟡 improved

page range base = PR (data) vs Loki (PR) verdict
explore-logs-multivalue-regexp 15m differs (3) 1 difference(s), 1 on base too 🟡 improved
explore-logs-multivalue-regexp 1h differs (3) 1 difference(s), 1 on base too 🟡 improved
explore-volume-multivalue-regexp 15m differs (2) 1 difference(s), 1 on base too 🟡 improved
explore-volume-multivalue-regexp 1h differs (2) 1 difference(s), 1 on base too 🟡 improved
explore-logs-multivalue-regexp-15m: improved

explore-logs-multivalue-regexp-15m

explore-logs-multivalue-regexp-1h: improved

explore-logs-multivalue-regexp-1h

explore-volume-multivalue-regexp-15m: improved

explore-volume-multivalue-regexp-15m

explore-volume-multivalue-regexp-1h: improved

explore-volume-multivalue-regexp-1h

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.14% ✅
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% ✅
explore-logs-multivalue-regexp 15m detailed differs (3): query ('{app=~"api-gateway|auth"}', 'range', '1793197320000' 1 difference(s), 1 on base too 3.22% ✅ improved (closer to Loki)
explore-logs-multivalue-regexp 1h detailed differs (3): query ('{app=~"api-gateway|auth"}', 'range', '1793194620000' 1 difference(s), 1 on base too 4.84% ✅ improved (closer to Loki)
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-volume-label-filter-regexp 15m detailed differs (1): query ('sum by (region) (count_over_time({app="api-gateway"} 1 difference(s), 1 on base too 0.59% ✅ improved (closer to Loki)
explore-volume-label-filter-regexp 1h detailed differs (1): query ('sum by (region) (count_over_time({app="api-gateway"} 1 difference(s), 1 on base too 1.71% ✅ improved (closer to Loki)
explore-volume-label-filter-regexp 6h detailed differs (1): query ('sum by (region) (count_over_time({app="api-gateway"} n/a (beyond the Loki window) 4.60% ✅ improved (closer to Loki)
explore-volume-multivalue-regexp 15m detailed differs (2): query ('sum by (app) (count_over_time({app=~"api-gateway|aut 1 difference(s), 1 on base too 0.51% ✅ improved (closer to Loki)
explore-volume-multivalue-regexp 1h detailed differs (2): query ('sum by (app) (count_over_time({app=~"api-gateway|aut 1 difference(s), 1 on base too 1.55% ✅ improved (closer to Loki)
lvl-dd-field-method-pill 1h detailed ✅ identical 1 difference(s), 1 on base too; 2 explained 0.11% ✅
lvl-dd-field-pipeline-pill 1h detailed ✅ identical 1 difference(s), 1 on base too; 2 explained, 1 history-dependent 0.11% ✅
lvl-dd-fields-pill 1h detailed ✅ identical 25 difference(s), 25 on base too; 4 explained 0.11% ✅
lvl-explore-count-warn-by-service 1h detailed ✅ identical 1 difference(s), 1 on base too; 1 explained 0.04% ✅
lvl-explore-logs-drop-dl 1h detailed ✅ identical 2 difference(s), 2 on base too; 1 explained 0.07% ✅
lvl-explore-logs-drop-level 1h detailed ✅ identical 1 difference(s), 1 on base too; 1 explained 0.07% ✅
lvl-explore-logs-json-level 1h detailed ✅ identical identical (1 explained) 0.07% ✅
lvl-explore-logs-json-level-extracted-stored 1h detailed ✅ identical identical (1 explained) 0.07% ✅
lvl-explore-logs-keep-dl 1h detailed ✅ identical 1 difference(s), 1 on base too; 1 explained 0.07% ✅
lvl-explore-logs-level-label-stored 1h detailed ✅ identical identical (1 explained) 0.07% ✅
lvl-explore-logs-level-selector-stored 1h detailed ✅ identical identical (1 explained) 0.07% ✅
lvl-explore-logs-logfmt-level-extracted-stored 1h detailed ✅ identical identical (1 explained) 0.07% ✅
lvl-explore-logs-logfmt-level-regexp 1h detailed ✅ identical identical (1 explained) 0.07% ✅
lvl-explore-rate-errors-by-service 1h detailed ✅ identical 1 difference(s), 1 on base too; 1 explained 0.04% ✅
lvl-explore-topk-errors-by-service 1h detailed ✅ identical 1 difference(s), 1 on base too; 1 explained 0.04% ✅
lvl-explore-volume-dl-prefilter-json 1h detailed ✅ identical identical (1 explained) 0.04% ✅
lvl-explore-volume-drop-level 1h detailed ✅ identical identical (1 explained) 0.04% ✅
lvl-explore-volume-json-level-filter 1h detailed ✅ identical identical (1 explained) 0.04% ✅
lvl-explore-volume-keep-dl 1h detailed ✅ identical identical (1 explained) 0.04% ✅
lvl-explore-volume-level-label-stored 1h detailed ✅ identical identical (1 explained) 0.04% ✅
lvl-explore-volume-logfmt-dl 1h detailed ✅ identical identical (1 explained) 0.04% ✅
lvl-explore-volume-logfmt-dl-filter 1h detailed ✅ identical identical (1 explained) 0.04% ✅
tail-json-filter live detailed ✅ identical entries in the window: PR 1314, Loki 467, in both 467 1.38% ✅

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 that query to answer like 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-logs-multivalue-regexp-15m: pixel diff 3.2%
  • pixel diff 3.2%
  • vs Loki: resource /api/datasources/uid/*/resources/index/stats?query=%7Bapp%3D~%22api-gateway%7Cauth%22%7D&s: body: content differs (rows 0 vs 0)

explore-logs-multivalue-regexp-15m

explore-logs-multivalue-regexp-1h: pixel diff 4.8%
  • pixel diff 4.8%
  • vs Loki: resource /api/datasources/uid/*/resources/index/stats?query=%7Bapp%3D~%22api-gateway%7Cauth%22%7D&s: body: content differs (rows 0 vs 0)

explore-logs-multivalue-regexp-1h

explore-volume-label-filter-regexp-6h: no Loki data at this range; the same differences match Loki at a shorter range (1): query ('sum by (region) (count_over_
  • no Loki data at this range; the same differences match Loki at a shorter range (1): query ('sum by (region) (count_over_time({app="api-gateway"} | json | region=~"us-east-1|west.*": series sets differ: 0 only left [], 2 only right ['(\'A\', \'\
  • pixel diff 4.6%

explore-volume-label-filter-regexp-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-909.

@github-actions github-actions Bot added size/XL Extra large change and removed size/XL Extra large change labels Oct 28, 2026
…s wrongly and pin the selector pages to the Loki ranges

The label-filter Fix-proof page filtered region=~"us-east|eu-west-1". The base
already anchors a label filter on the ordered JSON metric path (^(?:re)$), and for
two unrelated literals that selects what Loki's equality-per-branch selects, so base
and PR answered identically and the page could not show the fix. It now filters
region=~"us-east-1|west.*": Loki ORs an equality check on us-east-1 with a substring
check on west and selects all three seeded regions (us-east-1, us-west-2, eu-west-1);
the base's anchored form selects us-east-1 only. Checked against Loki v3.7.9's
pipeline and VictoriaLogs v1.49.0 and v1.53.0 storage, field and stream datasets: the
PR's filter selects Loki's rows, the base's does not.

The two multi-value selector pages showed the fix at 15m and 1h, but at 6h the
index/stats request for the changed selector also differs base vs PR (the base counts
the extra stream). The gate never carries a resource difference to the range Loki does
not hold (test_a_resource_difference_at_6h_is_never_carried), so those rows failed.
Their entries are pinned to 15m and 1h like the other fix pages, and the README says
when to do so. Two tests pin the label-filter page's gate shape and the resource case.
@github-actions github-actions Bot added size/XL Extra large change and removed size/XL Extra large change labels Oct 28, 2026
@szibis
szibis merged commit da88200 into main Oct 28, 2026
50 checks passed
@szibis
szibis deleted the fix/label-matcher-regexp-anchoring branch October 28, 2026 14:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugfix Bug fix scope/ci CI/CD 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.

3 participants