Skip to content

fix: Loki-compatible profile holds requests to Loki's contract; e2e stack tests it by default - #624

Merged
szibis merged 15 commits into
mainfrom
feat/loki-compat-test-mode
Sep 30, 2026
Merged

szibis merged 15 commits into
mainfrom
feat/loki-compat-test-mode

Conversation

@szibis

@szibis szibis commented Sep 24, 2026 •

Copy link
Copy Markdown
Collaborator

What and why

The e2e stack's Grafana-facing proxies ran -metadata-field-mode=hybrid, so Explore offered k8s.namespace.name beside k8s_namespace_name. A user built {env="production"} | json | pipeline=metrics/prometheus | k8s.namespace.name=monitoring``. Loki v3.7.7 rejects that with 400 parse error at line 1, col 64: syntax error: unexpected .. The proxy accepted it (even in translated mode) and ran it on the raw-row path: 3.2 s at 1h, `502 manual range metric row limit exceeded` above ~3h.

This PR makes Loki compatibility the stack's primary test target and holds the Loki-compatible profile to Loki's request contract as well as its response contract.

1. Loki compatibility mode is the primary testing target

  • Loki (via VL proxy) (+ multi-tenant, loki-vl-proxy-underscore) and the Logs Drilldown default Loki (via VL proxy patterns autodetect) now run -label-style=underscores -metadata-field-mode=translated. Datasource names, and so UIDs, are unchanged.
  • Hybrid moved to a new explicitly named datasource, Loki (via VL proxy OTel hybrid) (loki-vl-proxy-otel-hybrid, port 13111). It replaces loki-vl-proxy-translated-metadata, which duplicated the Loki profile. native metadata is unchanged.
  • TestCompat_StackProfilesMatchDatasources (core group) fails CI if a Grafana-facing or parity datasource leaves the Loki profile, if warm-up flags drift, or if a member of the parity proxy's peer ring does not start with the stack.
  • No -compat-profile flag. The three flags already default to the Loki profile, and each also changes one surface on its own. A profile flag would need precedence rules against them. Documented in docs/configuration.md.

2. Strict LogQL names in Loki mode

  • internal/logql/dotted_names.go emulates Loki's lexer (DOT token) and parser state. A dotted name gets Loki's exact message: line, character column, and the expected-token list goyacc prints there (expecting , or ), = or =~ or !~ or !=, =, RANGE or |, ), IDENTIFIER or ( ...; name.5 → unexpected NUMBER).
    • Recorded against live Loki 3.7.7 for 192 query shapes (unit table, fuzz test).
    • Applied to query/query_range/tail and every selector parameter (labels, label values, series match[], index/, patterns, detected_), before any VictoriaLogs call.
    • Hybrid, native and passthrough keep accepting dotted names. This is a registered, profile-scoped deviation.
  • Leniencies audited:
    • Label endpoints: limit/offset/search are now ignored in Loki mode, as in Loki (loghttp.ParseLabelQuery), unless -label-values-indexed-cache opts in. They used to truncate and were forwarded to VictoriaLogs.
    • Grouping lists (by()/without()/on()/ignoring()/group_*) are covered by the dotted-name check.
    • Error envelope: gained message. Grafana shows the JSON message field or the raw body, so Explore displayed the whole JSON instead of Loki's text.
  • Label-endpoint gaps the coordinator reported (all diagnosed live):
    • /labels 7d, 39 vs 24 names: VL-only fixture streams (data difference). The fixture helper now fails setup on a rejected push.
    • service_name values: Loki over-reports by index-day granularity (TSDBIndex.LabelValues ignores from/through). Proxy answers the exact window. Documented in profiles/label-endpoints-window-and-data.
    • Unbounded /label/pod/values: new -label-values-max-response-bytes (default 64 MiB, per tenant). Over the cap it answers Loki's 500 rpc error: code = ResourceExhausted ... (N vs. LIMIT) naming the flag, and never caches the result.
    • 502 chunked line ends with bare LF: VictoriaLogs' hijack-abort after headers is now Loki's 504 request timed out... (or a sanitized 502) on GET reads.
  • Open gaps found and registered (not fixed here):
    • parsed labels on queries without a parser.
    • service_name_extracted for structured metadata that collides with a stream label.
    • Structured metadata merged into streams without categorize-labels.

3. Background warm-up in the test stack

4. Parity suites target Loki mode

  • The Go groups compare Loki with the Loki-profile proxies.
  • New TestCompat_DottedNameParseErrorsMatchLoki (20 endpoint×position cases × parity + both Grafana-facing proxies) and TestCompat_LabelBrowseParamsIgnoredLikeLoki.
  • New Playwright tests in the datasource shard go through Grafana /api/ds/query: dotted error text equals Loki's; structured-metadata keys are Loki's.

Tests moved and why

test before now why
TestDetectedFields_UnderscoreProxy/hybrid_fields_..., detected_field_values_service_dot_name 13102 hybrid 13111 asserts dotted names (hybrid behaviour); new loki_profile_fields_are_loki_names_only on 13102
TestGrafanaDrilldown_UnderscoreProxy/drilldown_detected_fields_for_service 13102 hybrid 13102 no-dots + new _otel_hybrid subtest on 13111 same split
TestOTelCompatibilityScore detected_fields dotted on 13102 sanitized on 13102 + dotted on 13111 same
TestStructuredMetadata_HybridMode... 13102 13111 hybrid assertions unchanged
TestStructuredMetadata_TranslatedMode... 13107 13102 13102 is now the translated profile
TestDrilldown_GrafanaResourceContracts/structured_metadata_fields_and_values DS Loki (via VL proxy) DS ... OTel hybrid + new structured_metadata_fields_loki_profile same split
.../datasource_proxy_query_range_accepts_dotted_field_filters Explore DS 200 Explore DS = Loki's 400; underscore filter 200; dotted 200 on hybrid DS Loki rejects dotted names
.../label_values_honor_limit Explore DS limit=1 → 1 Explore DS ignores limit like Loki; limit honoured on hybrid DS Loki ignores limit
TestOTelDots_ProxyPassthrough/query_with_dotted_* 13100 accepted 13100 = Loki's 400; dotted filter on hybrid same
TestDrilldownTrackScore hybrid field dotted on Drilldown DS no dotted OTel names on Drilldown DS Loki profile
TestPinnedCompatibilityMatrixMatchesCompose underscore=hybrid, translated-metadata underscore=translated, otel-hybrid=hybrid new wiring

Live proof (shared stack, read-only; my builds on 13271/13273)

Builds: branch vs main (4d517df), same stack flags + -backend=http://127.0.0.1:19428 -rate-limit-per-second=0 -rate-limit-burst=0, each on its own port with -admin-listen. Listener PIDs verified.

Dotted names (1h):

case Loki branch main
owner query (log) 400 col 64: unexpected . 400 identical 200
owner log volume 400 col 112: unexpected . 400 identical 200 in 3.99 s (raw rows)
sum by (k8s.namespace.name) 400 col 12 ... expecting , or ) identical 200
{k8s.namespace.name="x"} 400 col 5 ... expecting = or =~ or !~ or != identical 200
/labels?query= / /series match[] dotted 400 identical 200

Structured-metadata keys ({app="otel-collector"}, 15m, categorize-labels):

  • Loki and branch both return deployment_environment, detected_level, k8s_cluster_name, k8s_namespace_name, k8s_pod_name, service_version, span_id, trace_id. No dotted keys.
  • Remaining differences are registered open gaps: Loki's service_name_extracted vs the proxy's service_name, and the proxy's extra parsed.

A/B on the final head (bench/ab, 5 runs, warm p50 unless noted; main = origin/main at ba148c5, branch = this head, each on its own verified port with -admin-listen; Loki direct; shared-stack VictoriaLogs restarts 34→34, so every run is valid). Saved in bench/ab/results/2026-09-30-pr624-final-*.json.

loki-profile-otel-metadata (changes: dotted filters answer Loki's 400 instead of 200/502; O2 24h 2.15x faster; every underscore shape equal to main):

shape range main branch loki change result vs loki
O1 explore logs, underscore structured-metadata filter 1h 0.32s† 0.37s† 0.60s† same same
O1 explore logs, underscore structured-metadata filter 3h 0.22s† 0.23s† 0.60s† same same
O1 explore logs, underscore structured-metadata filter 6h 0.25s† 0.26s† 0.83s† same same
O1 explore logs, underscore structured-metadata filter 24h 0.49s 0.33s† 0.66s† 1.49× same
O2 explore log volume (Grafana form), underscore structured-metadata filter 1h 0.07s 0.06s 0.02s same differs
O2 explore log volume (Grafana form), underscore structured-metadata filter 3h 0.12s 0.11s 0.02s same same series, sum ±0.73%
O2 explore log volume (Grafana form), underscore structured-metadata filter 6h 0.09s 0.10s 0.00s same differs
O2 explore log volume (Grafana form), underscore structured-metadata filter 24h 0.46s 0.21s 0.00s 2.15× differs
O3 explore logs, dotted structured-metadata filter 1h 0.26s† 400 400 fixed same
O3 explore logs, dotted structured-metadata filter 3h 0.27s† 400 400 fixed same
O3 explore logs, dotted structured-metadata filter 6h 0.22s† 400 400 fixed same
O3 explore logs, dotted structured-metadata filter 24h 0.21s 400 400 fixed same
O4 explore log volume (Grafana form), dotted structured-metadata filter 1h 2.54s† 400 400 fixed same
O4 explore log volume (Grafana form), dotted structured-metadata filter 3h 7.55s† 400 400 fixed same
O4 explore log volume (Grafana form), dotted structured-metadata filter 6h 502 400 400 same same
O4 explore log volume (Grafana form), dotted structured-metadata filter 24h 502 400 400 same same
  • † = identical answers for windows a minute apart (cache or stale view), timing not like-for-like. O2 differs rows: main and branch return identical series and sums, Loki lags at the live edge (it answers 0.00-0.02s, from its results cache). Verdict: 16 shape×range - 6 fixed, 4 faster, 6 same; 0 new differences from Loki.
  • No-degradation control set (control, 37 shape×range): 3 faster, 34 same, 0 slower; 0 new differences from Loki (31 pre-existing, also on main). A first run of the previous head flagged topk rate by app 24h 0.17s→0.23s; a 10-run recheck gave main 0.33s vs branch 0.31s (noise).
  • Drilldown breakdown set (drilldown-breakdown-exact, label and json field breakdowns 1h-7d, from fix: Logs Drilldown label and field breakdowns answer exactly like Loki #622): 16 of 16 same as main; 0 new differences from Loki. The 7d pod and batch_id rows fail on both main and branch (502 manual metric response exceeds 67108864 bytes, Loki also fails or returns nothing at 7d): pre-existing, not this change.
  • Earlier numbers on the previous head that this change does not touch (kept for reference): cold volume with the underscore filter 0.06s/0.25s/0.40s/1.48s vs Loki 0.83s/6.61s/8.78s/33.6s (1h/3h/6h/24h); pushdown is stats_query_range x4 + query x2 per volume, no raw rows.

benchstat (-count=10, previous head; the code it covers is unchanged):

  • Allocations are identical on Proxy_QueryRange_*, Proxy_LabelValues_Scale_*, NoCache_QueryRange_LogQuery and ValidateLogQL/*.
  • DottedNameError costs ~0.7–1.0 µs and 1 alloc per query (cached validation is unaffected).
  • ValidateLogQL ns/op noise (±40%) is on unchanged code.

Backend-safety load (previous head; 24 long-range requests at concurrency 8, /labels probe every second):

  • branch: 200×20, 400×4 (dotted); probe 200 throughout.
  • main: 200×20, 502×4 (the dotted volume hit the row limit), 28 s max.
  • VictoriaLogs RestartCount 2→2 both times.

Local e2e on an isolated compose project (-p lcmode, ports +10000, fresh stack per group, final rebased head, VictoriaLogs restarts 0 in every run, torn down):

  • Go groups: core, drilldown, otel-edge, tail-multitenancy, semantics all pass, including TestCompat_OptionMatrixAgainstStack, TestCompat_SanitizedJSONKeyFilterMatchesLoki and TestCompat_SanitizedJSONKeyGroupingMatchesLoki.
  • Playwright: datasource, explore-core, explore-tail, drilldown-core (1 flaky: patterns are visible in drilldown for autodetected datasource showed Patterns0 on the first attempt, passed on retry, same on both full runs), drilldown-mt, explore-ops, explore-mt, explore-regression (41 passed, 10 skipped), comprehensive all pass.

Re-run: python3 bench/ab/perf_matrix.py --set loki-profile-otel-metadata|control --runs 5 --target main=… --target branch=… --target loki=http://127.0.0.1:13101 --out … then report.py summarize … --save bench/ab/results/.

Sanitized JSON keys: filters, grouping and unwrap

Loki's json/logfmt parsers sanitize http.method to http_method; VictoriaLogs' unpack_json keeps the original key. So | json | http_method="GET" returned no lines, and grouping by or unwrapping such a name aggregated a field that does not exist (one unlabelled series, NaN). detected_fields of this PR names dotted JSON keys by the sanitized label with the key in jsonPath, and Logs Drilldown builds | json label="[\"key\"]" from it, so all of these had to give Loki's series before merging.

  • Filters (internal/translator/parsed_key_variants.go): after a json/logfmt parser a label filter accepts every original key that sanitizes to the name (each _ read as _ or ., plus the all-hyphen spelling, up to 6 underscores); =/=~/comparisons hold when any spelling matches, !=/!~/empty checks when every spelling does. Mapped labels, captures, label_format targets and json expressions keep one key.
  • Grouping and unwrap, per stats clause (each operand of a binary expression on its own): a json expression adds | copy "x.y" as a; a sanitized by()/unwrap label adds, right before the stats, one | format if ("x.y":*) "<x.y>" as label keep_original_fields per spelling (a stored or stream label wins, as in Loki); a parser-derived unwrap label also adds | filter label:* (Loki yields no sample for a line without it). VictoriaLogs stats aggregate a real field, so range queries stay pushed down, no raw rows. An instant bare sum(sum_over_time(... | unwrap x)) collapses its streams into one series like Loki's sum (count, avg, max, topk keep one value per stream).
  • Kept exact: the single-field count breakdown | json | f!="" ... by (f) (what fix: Logs Drilldown label and field breakdowns answer exactly like Loki #622's field-breakdown rewrite recognises) and label!="" keep the exact key; Drilldown names a dotted key through the expression form.
  • Tests: unit (translator table incl. binary expression, label_format, detected_level, quoted literal, Drilldown shape; proxy upstream query; instant sum), TestCompat_SanitizedJSONKeyFilterMatchesLoki and TestCompat_SanitizedJSONKeyGroupingMatchesLoki against real Loki (range and instant, plain and Drilldown-tagged, three proxies, the fields-page sequence detected_fields -> breakdown by name and jsonPath, unwrap), registry cases profiles/sanitized-json-key-filter and profiles/sanitized-json-key-grouping.
  • Open, registered and unchanged: a label-filter regexp is not anchored like Loki's (| msg=~"a" matches "cat"; profiles/label-filter-regexp-anchoring, separate from fix(logql): anchor label-matcher regexps to the whole label value #572); a json-expression label is always present in Loki, so a line without the path forms {a=""} where the proxy returns {} (only without a following a!="").

Fresh-context reviews (Opus), each finding checked: first review fixed logfmt detected_fields keys, ip() field quoting, dotted-edge candidates, pattern/label_format names, /label/service_name/values outside the response cap, the e2e peer ring. Second review (grouping/unwrap) found and I fixed: instant outer operators collapsed wrongly (now sum of additive only), a splice into the detected_level parser pipes, an unwrap filter leaking across a binary expression, an unwrap filter dropping lines before a later label_format, pipes inside a quoted literal, overwriting a stored label (now keep_original_fields); the resolution moved from a regex over the output to the stats clause. Left as is: error-body message on VL-translated error envelopes (rare path); | unwrap x on a non-sanitized, non-expression label still yields a NaN series for lines without it.

Gates

  • go build, go vet ./... (+ -tags=e2e), gofmt, golangci-lint (0 issues), go test -count=1 ./....
  • go run ./cmd/configdoc -check, validate_helm_flags.sh, check_conformance.py ok, docker compose config valid.
  • Changelog gate ok. The released part of CHANGELOG is byte-identical to main.

Breaking

  • Loki profile (default): dotted names in LogQL are now Loki's 400.
  • Label endpoints ignore limit/offset/search unless -label-values-indexed-cache=true.
  • Label values responses over 64 MiB now fail with Loki's ResourceExhausted 500.

@szibis
szibis force-pushed the feat/loki-compat-test-mode branch from c77e3e2 to ea8fc01 Compare September 24, 2026 10:24
@github-actions github-actions Bot added size/XL Extra large change scope/proxy Proxy core scope/middleware Middleware scope/helm Helm chart scope/docs Documentation scope/cli CLI flags scope/tests Tests bugfix Bug fix labels Sep 24, 2026
@github-actions

github-actions Bot commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

🟢 Performance A/B: faster, no regression

48 shape×range — 2 ✅ fixed · 46 ⚪ same · results vs Loki: 40 same, 0 new difference(s), 8 pre-existing

shape range base p50 PR p50 change Loki p50 PR ÷ Loki result vs Loki
✅ O3 explore logs, dotted structured-metadata filter 1h 264 ms 400 ✅ fixed 400 — ✅ same
✅ O4 explore log volume (Grafana form), dotted structured-metadata filter 1h 3.78 s 400 ✅ fixed 400 — ✅ same
46 unchanged shape×range (within noise)
shape range base p50 PR p50 change Loki p50 PR ÷ Loki result vs Loki
⚪ by service_name 1h 27 ms 29 ms ⚪ +7% 32 ms 0.91× ✅ same
⚪ bytes_over_time by app 1h 52 ms 51 ms ⚪ -2% 72 ms 0.71× ✅ same
⚪ detected_level volume 1h 12 ms 14 ms ⚪ +17% 31 ms 0.45× ✅ same
⚪ drilldown detected_level 1h 61 ms 57 ms ⚪ -7% 20 ms 2.85× ✅ same
⚪ drilldown level volume 1h 11 ms 11 ms ⚪ +0% 21 ms 0.52× ✅ same
⚪ drilldown service landing 1h 24 ms 24 ms ⚪ +0% 21 ms 1.14× ✅ same
⚪ instant json filtered instant 307 ms 304 ms ⚪ -1% 60 ms 5.07× ✅ same
⚪ instant json volume instant 346 ms 342 ms ⚪ -1% 80 ms 4.28× ✅ same
⚪ instant ungrouped rate instant 7 ms 5 ms ⚪ -29% 61 ms 0.08× ✅ same
⚪ json rate by level (no filter, no drop) 1h 400 400 ⚪ +0% 400 — ✅ same
⚪ json volume no filters 1h 1 ms 1 ms ⚪ +0% 2 ms 0.50× ✅ same
⚪ logfmt count by level, filter, drop 1h 265 ms 270 ms ⚪ +2% 43 ms 6.28× ✅ sum ±0.17%
⚪ logfmt volume 1h 227 ms 220 ms ⚪ -3% 46 ms 4.78× ✅ same
⚪ logs plain 1h 46 ms 44 ms ⚪ -4% 30 ms 1.47× ➖ differs (as base)
⚪ logs | json 1h 71 ms 70 ms ⚪ -1% 48 ms 1.46× ➖ differs (as base)
⚪ logs | logfmt 1h 127 ms 117 ms ⚪ -8% 39 ms 3.00× ➖ differs (as base)
⚪ plain volume 1h 12 ms 13 ms ⚪ +8% 32 ms 0.41× ✅ same
⚪ stream-label count by app 1h 22 ms 18 ms ⚪ -18% 32 ms 0.56× ✅ same
⚪ topk rate by app 1h 33 ms 34 ms ⚪ +3% 70 ms 0.49× ➖ differs (as base)
⚪ ungrouped rate 1h 9 ms 9 ms ⚪ +0% 69 ms 0.13× ✅ same
⚪ DB1 label breakdown pod 1h 67 ms 56 ms ⚪ -16% 95 ms 0.59× ✅ same
⚪ DB2 label breakdown namespace 1h 12 ms 14 ms ⚪ +17% 27 ms 0.52× ✅ same
⚪ DB3 json field breakdown user_id 1h 33 ms 36 ms ⚪ +9% 21 ms 1.71× ✅ same
⚪ DB4 json field breakdown batch_id 1h 70 ms 70 ms ⚪ +0% 47 ms 1.49× ✅ same
⚪ A explore volume, 2 filters, drop 1h 210 ms 191 ms ⚪ -9% 101 ms 1.89× ✅ same
⚪ D volume, no filters 1h 150 ms 153 ms ⚪ +2% 38 ms 4.03× ✅ same
⚪ E field breakdown pipeline (explore) 1h 133 ms 137 ms ⚪ +3% 29 ms 4.72× ✅ same
⚪ G field breakdown pipeline (drilldown) 1h 34 ms 33 ms ⚪ -3% 20 ms 1.65× ✅ same
⚪ I grafana volume of B 1h 138 ms 144 ms ⚪ +4% 30 ms 4.80× ✅ same
⚪ O1 explore logs, underscore structured-metadata filter 1h 252 ms 247 ms ⚪ -2% 292 ms 0.85× ✅ same
⚪ O2 explore log volume (Grafana form), underscore structured-metadata filter 1h 141 ms 134 ms ⚪ -5% 30 ms 4.47× ✅ same
⚪ drilldown detected fields 1h 21 ms 21 ms ⚪ +0% 14 ms 1.50× ➖ differs (as base)
⚪ drilldown detected labels 1h 20 ms 19 ms ⚪ -5% 2 ms 9.50× ➖ differs (as base)
⚪ drilldown landing volume 1h 43 ms 42 ms ⚪ -2% 22 ms 1.91× ➖ differs (as base)
⚪ explore label values env 1h 9 ms 7 ms ⚪ -22% 11 ms 0.64× ✅ same
⚪ explore label values service_name 1h 6 ms 4 ms ⚪ -33% 13 ms 0.31× ✅ same
⚪ explore labels (label browser) 1h 6 ms 6 ms ⚪ +0% 7 ms 0.86× ✅ same
⚪ explore labels with selector 1h 6 ms 6 ms ⚪ +0% 12 ms 0.50× ✅ same
⚪ refresh label values env 1h 18 ms 16 ms ⚪ -11% 10 ms 1.60× ✅ same
⚪ refresh label values service_name 1h 9 ms 8 ms ⚪ -11% 12 ms 0.67× ✅ same
⚪ refresh labels (label browser) 1h 11 ms 13 ms ⚪ +18% 7 ms 1.86× ✅ same
⚪ refresh labels with selector 1h 12 ms 14 ms ⚪ +17% 12 ms 1.17× ✅ same
⚪ DL1 drilldown field breakdown json-expr, level filter 1h 364 ms 364 ms ⚪ +0% 19 ms 19× ✅ same
⚪ DL3 drilldown logfmt field breakdown, level filter 1h 471 ms 451 ms ⚪ -4% 84 ms 5.37× ➖ differs (as base)
⚪ DL4 instant count, level filter, json+logfmt, 1h instant 376 ms 374 ms ⚪ -1% 978 ms 0.38× ✅ sum ±0.18%
⚪ DL6 explore volume, level filter before json 1h 385 ms 380 ms ⚪ -1% 32 ms 12× ✅ sum ±0.17%
Cold (first-run) timings — what a user opening a panel sees
shape range base cold PR cold Loki cold PR ÷ Loki cold
O3 explore logs, dotted structured-metadata filter 1h 255 ms 400 400 —
O4 explore log volume (Grafana form), dotted structured-metadata filter 1h 3.84 s 400 400 —
by service_name 1h 29 ms 29 ms 573 ms 0.05×
bytes_over_time by app 1h 54 ms 52 ms 617 ms 0.08×
detected_level volume 1h 14 ms 12 ms 548 ms 0.02×
drilldown detected_level 1h 60 ms 58 ms 533 ms 0.11×
drilldown level volume 1h 11 ms 11 ms 503 ms 0.02×
drilldown service landing 1h 23 ms 27 ms 596 ms 0.05×
instant json filtered instant 319 ms 313 ms 63 ms 4.97×
instant json volume instant 364 ms 351 ms 82 ms 4.28×
instant ungrouped rate instant 5 ms 6 ms 61 ms 0.10×
json rate by level (no filter, no drop) 1h 400 400 400 —
json volume no filters 1h 137 ms 137 ms 2 ms 68.50×
logfmt count by level, filter, drop 1h 272 ms 266 ms 916 ms 0.29×
logfmt volume 1h 161 ms 157 ms 1.00 s 0.16×
logs plain 1h 75 ms 52 ms 34 ms 1.53×
logs | json 1h 77 ms 71 ms 54 ms 1.31×
logs | logfmt 1h 126 ms 117 ms 41 ms 2.85×
plain volume 1h 12 ms 12 ms 617 ms 0.02×
stream-label count by app 1h 21 ms 21 ms 587 ms 0.04×
topk rate by app 1h 33 ms 32 ms 616 ms 0.05×
ungrouped rate 1h 9 ms 10 ms 597 ms 0.02×
DB1 label breakdown pod 1h 78 ms 58 ms 667 ms 0.09×
DB2 label breakdown namespace 1h 12 ms 12 ms 503 ms 0.02×
DB3 json field breakdown user_id 1h 34 ms 36 ms 561 ms 0.06×
DB4 json field breakdown batch_id 1h 72 ms 69 ms 568 ms 0.12×
A explore volume, 2 filters, drop 1h 1.12 s 1.19 s 940 ms 1.26×
D volume, no filters 1h 147 ms 157 ms 1.49 s 0.11×
E field breakdown pipeline (explore) 1h 165 ms 156 ms 465 ms 0.34×
G field breakdown pipeline (drilldown) 1h 40 ms 33 ms 527 ms 0.06×
I grafana volume of B 1h 160 ms 162 ms 475 ms 0.34×
O1 explore logs, underscore structured-metadata filter 1h 275 ms 260 ms 350 ms 0.74×
O2 explore log volume (Grafana form), underscore structured-metadata filter 1h 199 ms 196 ms 468 ms 0.42×
drilldown detected fields 1h 21 ms 20 ms 14 ms 1.43×
drilldown detected labels 1h 20 ms 24 ms 3 ms 8.00×
drilldown landing volume 1h 42 ms 40 ms 25 ms 1.60×
explore label values env 1h 84 ms 65 ms 10 ms 6.50×
explore label values service_name 1h 29 ms 31 ms 12 ms 2.58×
explore labels (label browser) 1h 59 ms 45 ms 8 ms 5.62×
explore labels with selector 1h 49 ms 48 ms 13 ms 3.69×
refresh label values env 1h 1 ms 1 ms 11 ms 0.09×
refresh label values service_name 1h 1 ms 1 ms 12 ms 0.08×
refresh labels (label browser) 1h 1 ms 1 ms 7 ms 0.14×
refresh labels with selector 1h 1 ms 1 ms 14 ms 0.07×
DL1 drilldown field breakdown json-expr, level filter 1h 363 ms 371 ms 413 ms 0.90×
DL3 drilldown logfmt field breakdown, level filter 1h 483 ms 479 ms 837 ms 0.57×
DL4 instant count, level filter, json+logfmt, 1h instant 379 ms 393 ms 976 ms 0.40×
DL6 explore volume, level filter before json 1h 771 ms 741 ms 594 ms 1.25×
Why these shapes
  • internal/translator/translator.go → 24: control/bytes_over_time by app, 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, control/logs | json …
  • internal/proxy/drilldown.go → 22: 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, control/logs | logfmt, drilldown-breakdown-exact/DB3 json field breakdown user_id …
  • cmd/proxy/main.go → 20: control/by service_name, control/bytes_over_time by app, control/detected_level volume, control/drilldown detected_level, control/drilldown level volume, control/drilldown service landing, control/instant json filtered, control/instant json volume …
  • internal/proxy/http_utils.go → 19: 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, loki-profile-otel-metadata/O1 explore logs, underscore structured-metadata filter, loki-profile-otel-metadata/O2 explore log volume (Grafana form), underscore structured-metadata filter, loki-profile-otel-metadata/O3 explore logs, dotted structured-metadata filter, loki-profile-otel-metadata/O4 explore log volume (Grafana form), dotted structured-metadata filter …
  • internal/proxy/range_metric_compat.go → 12: 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/backend.go → 11: metadata-inventory/drilldown detected fields, metadata-inventory/drilldown detected labels, metadata-inventory/drilldown landing volume, metadata-inventory/explore label values env, metadata-inventory/explore label values service_name, metadata-inventory/explore labels (label browser), metadata-inventory/explore labels with selector, metadata-inventory/refresh label values env …
  • internal/proxy/metadata_inventory.go → 11: metadata-inventory/drilldown detected fields, metadata-inventory/drilldown detected labels, metadata-inventory/drilldown landing volume, metadata-inventory/explore label values env, metadata-inventory/explore label values service_name, metadata-inventory/explore labels (label browser), metadata-inventory/explore labels with selector, metadata-inventory/refresh label values env …
  • 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/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
  • internal/config/registry.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
  • internal/logql/dotted_names.go → 4: loki-profile-otel-metadata/O1 explore logs, underscore structured-metadata filter, loki-profile-otel-metadata/O2 explore log volume (Grafana form), underscore structured-metadata filter, loki-profile-otel-metadata/O3 explore logs, dotted structured-metadata filter, loki-profile-otel-metadata/O4 explore log volume (Grafana form), dotted structured-metadata filter
  • internal/proxy/backend_response_guard.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
  • internal/proxy/execution_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
  • internal/proxy/label_handlers.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
  • internal/proxy/loki_profile.go → 4: loki-profile-otel-metadata/O1 explore logs, underscore structured-metadata filter, loki-profile-otel-metadata/O2 explore log volume (Grafana form), underscore structured-metadata filter, loki-profile-otel-metadata/O3 explore logs, dotted structured-metadata filter, loki-profile-otel-metadata/O4 explore log volume (Grafana form), dotted structured-metadata filter
  • shape added or edited → 4: loki-profile-otel-metadata/O1 explore logs, underscore structured-metadata filter, loki-profile-otel-metadata/O2 explore log volume (Grafana form), underscore structured-metadata filter, loki-profile-otel-metadata/O3 explore logs, dotted structured-metadata filter, loki-profile-otel-metadata/O4 explore log volume (Grafana form), dotted structured-metadata filter

base 5135704b0b → PR 5784083b61 · 4 runs (first is cold), warm p50 · ranges 1h · noise ±30% and 100 ms · VictoriaLogs restarts 0→0 · 5.6 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 Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

PR Quality Report

Compared against base branch main.

Coverage and tests

Signal Base PR Delta
Test count 7660 8185 525
Coverage 90.4% 90.6% +0.2% (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 3223.0 ns/op 3371.0 ns/op +4.6% (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 3817.0 ns/op 4014.0 ns/op +5.2% (stable)
QueryRange cache-bypass memory 1299.0 B/op 1300.0 B/op +0.1% (stable)
QueryRange cache-bypass allocations 27.0 allocs/op 27.0 allocs/op 0.0% (stable)
Labels cache-hit CPU cost 828.9 ns/op 858.1 ns/op +3.5% (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 1364.0 ns/op 1455.0 ns/op +6.7% (stable)
Labels cache-bypass memory 392.0 B/op 392.0 B/op 0.0% (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.

@szibis
szibis force-pushed the feat/loki-compat-test-mode branch from ea8fc01 to ba497e0 Compare September 24, 2026 10:38
@github-actions github-actions Bot added size/XL Extra large change scope/translator LogQL translator and removed size/XL Extra large change labels Sep 24, 2026
@szibis
szibis force-pushed the feat/loki-compat-test-mode branch from 62e9ecf to d14a807 Compare September 30, 2026 11:20
@github-actions github-actions Bot added size/XL Extra large change and removed size/XL Extra large change labels Sep 30, 2026
@szibis
szibis force-pushed the feat/loki-compat-test-mode branch from d14a807 to bbfd677 Compare September 30, 2026 13:38
@github-actions github-actions Bot added size/XL Extra large change and removed size/XL Extra large change labels Sep 30, 2026
…tack tests it by default

- Dotted LogQL names (k8s.namespace.name) are Loki's exact 400 parse error
  (line, column, expected tokens) in the default underscores+translated
  profile, on query/query_range/tail and every selector parameter; hybrid,
  native and passthrough keep accepting them.
- The label endpoints ignore limit/offset/search like Loki unless
  -label-values-indexed-cache opts into the browse window.
- -label-values-max-response-bytes bounds label values reads with Loki's
  ResourceExhausted error; a VictoriaLogs response aborted after its headers
  is a 504/502, not a raw transport error.
- Error bodies carry "message", so Grafana shows Loki's error text.
- -labels-cache-warm turns off the label keep-warm scans.
- e2e stack: Grafana Explore and Logs Drilldown datasources run the
  Loki-compatible profile, the hybrid profile is a named datasource,
  only three variants warm, the peer ring is behind the "peers" profile.
- ".5" right after a Loki keyword (offset.5h, > bool.5) is a number, not a
  dotted name.
- The aborted-response error unwraps its transport cause, so deadline
  retries and graceful timeouts still see context.DeadlineExceeded.
- The label values cap also bounds background refreshes, and coalesced
  reads with different caps no longer share a flight.
- Docs name the entry points and the GET scope of the abort guard;
  open case for detected_fields offering dotted JSON-body keys; A/B
  control results saved.
…Loki-matching rejections

A candidate that now fails exactly as Loki does counts as fixed in the A/B
report; the Loki-compatible profile behaviour names its implementation so
the per-PR shape selection reaches its shapes.
…trix tested

-logql-dotted-names (auto|reject|accept) and -label-browse-extensions
(auto|on|off) decouple dotted-name handling and the label browse window from
the metadata mode; -error-response-message-field restores the previous error
body. With dotted names rejected, detected_fields names a dotted JSON key by
Loki's sanitized label with the key in jsonPath. The translated-metadata
e2e variant is restored next to the new OTel hybrid variant, the option
matrix is documented and walked by unit and e2e tests, and the @regression
series-cap spec reads the published limit.
…lects

Loki's json and logfmt parsers sanitize each key into a label name (http.method becomes http_method) while unpack_json keeps the original key, so | json | http_method="GET" matched nothing when the name was not a stored VictoriaLogs field. A label filter after a json or logfmt parser now accepts every original key that sanitizes to its name.
…, service_name values cap, peer ring

Existence checks keep their single key for the Drilldown fast paths; candidate keys with adjacent or edge dots are not offered; pattern captures and label_format destinations are query-local; field names in ip() filters are quoted; detected_fields sanitizes logfmt keys; service_name values honour the response cap and report a failed read; the e2e stack starts its peer ring again.
…abel gives Loki's series

A json expression label gets its value copied from the original key. A sanitized by() or unwrap label of a json or logfmt query is copied, right before the stats clause, from each original spelling, so VictoriaLogs stats aggregate a real field (pushdown for range queries). A parser-derived unwrap label drops the lines without it. A bare instant sum over an unwrap collapses its streams. The label-filter regexp anchoring is registered as an open item.
…ead; the label-values guard tests count bucket calls
@szibis
szibis force-pushed the feat/loki-compat-test-mode branch from bbfd677 to 9c62082 Compare September 30, 2026 13:43
@github-actions github-actions Bot added size/XL Extra large change and removed size/XL Extra large change labels Sep 30, 2026
szibis and others added 2 commits September 30, 2026 16:02
…r decoding

decodeCompressedHTTPResponse set ContentLength and Uncompressed on a
gzip or zstd response after the transport had delivered it, while the
transport's read loop reads ContentLength from another goroutine once the
body is closed. A failed bucket of a label listing cancels its siblings,
which made that a data race under -race. The response keeps the
transport's values; the Content-Encoding and Content-Length headers are
still dropped.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WqewtSrSe2W95zXFBXkam9
…sting

The cap was checked against each VictoriaLogs bucket response, so a
listing whose every bucket was under the cap but whose union was over it
answered 200, and buckets another request had filled without a cap
bypassed it. With the inventory on, the merged listing is now measured as
VictoriaLogs would have sent it for one call over the whole range and
fails with Loki's ResourceExhausted 500 above the cap, agreeing with the
inventory-off path at the boundary. The inventory's singleflight key now
carries the cap, so an uncapped request never inherits a capped leader's
failure.

The guard tests run with the inventory off again and assert the exact call
count; new tests cover the inventory (a poisoned sealed bucket is never
cached or merged into a partial answer). The e2e compose restores
-metadata-default-lookback=1h on the translated-metadata proxy, with a
guard test that no proxy service passes 0.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WqewtSrSe2W95zXFBXkam9
@github-actions github-actions Bot added size/XL Extra large change and removed size/XL Extra large change labels Sep 30, 2026
@szibis
szibis merged commit 0529913 into main Sep 30, 2026
59 checks passed
@szibis
szibis deleted the feat/loki-compat-test-mode branch September 30, 2026 14:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugfix Bug fix scope/cli CLI flags scope/docs Documentation scope/helm Helm chart scope/middleware Middleware 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