Skip to content

test: differential parity run against Loki and a ranked gap inventory - #657

Merged
szibis merged 1 commit into
mainfrom
test/parity-gap-inventory
Oct 5, 2026
Merged

szibis merged 1 commit into
mainfrom
test/parity-gap-inventory

Conversation

@szibis

@szibis szibis commented Oct 5, 2026

Copy link
Copy Markdown
Collaborator

What

A reusable differential runner (bench/parity) that compares the proxy with Loki v3.7.7 on identical data, and a registry-backed, ranked list of every known difference (conformance/reports/parity-gaps.md). Discovery and registration only: no proxy code change.

  • bench/parity/corpus.py: corpus from Loki v3.7.7's own pkg/logql test suites (selectors moved onto the seeded data, sharding/template fixtures dropped, ranges clamped to the seeded hour), the repo's parity cases (test/e2e-compat, query-semantics-matrix.json, bench/ab/shapes.json, registry case requests), a seeded sweep generated from Loki's own answers (labels/values, every service's detected fields with the stages Explore and Drilldown build, index/stats, volume, volume_range, detected_*, patterns, format_query, tail), and the Grafana Explore + Drilldown requests captured by bench/visual (replayed through Grafana against both datasources).
  • bench/parity/run.py: proves both sides healthy before and after (equal line counts per 10-minute slice, Loki out of its blank window, Loki and VictoriaLogs restart counts known and unchanged, no VictoriaLogs 5xx / fallback-after-5xx in the proxy log), runs query endpoints with and without categorize-labels, diffs semantically (differ.py: entry order vs direction, duplicate label sets, scalars as numbers), reconfirms up to three samples of every difference signature, clusters by endpoint + query shape (operation, parser, stages, line/label/pattern filter) + kind (cluster.py). --keep-bodies / --rediff re-apply diff rules to a recorded run offline.
  • bench/parity/publish.py records a run in conformance/registry/generated/parity-discovery.json; conformance/scripts/parity_gaps.py ranks the registry's gap: cases into conformance/reports/parity-gaps.md. The conformance gate fails on an unclaimed or doubly claimed cluster, a recorded deviation that names no documented/owner-kept/upstream case, or status: fixed without fixed_by (a fix PR retires a gap without a rerun).
  • bench/visual/stack.py --overlay; bench/parity/docker-compose.parity.yml raises Loki's gRPC message limits (8 MiB in the e2e config dropped per-stream metric answers between querier and frontend).

Recorded run: main at fcaccfc (before #652 rewrote the tail and log-row label conversion)

Isolated Colima stack, Loki v3.7.7, VictoriaLogs v1.52.0, proxy built from fcaccfc. #5 (non-sum aggregation) and #16 (tail history) were verified still present on c4ebcd9 (after #652); the log-row label cases carry a note to recheck on current code before fixing.

corpus source requests same differ both empty blocked
grafana (Explore + Drilldown captures) 159 125 34 0 0
loki-tests (Loki v3.7.7 pkg/logql) 3570 448 1850 1248 24
repo (parity cases, shapes, registry) 2124 912 935 260 17
seeded (generated from Loki's answers) 5033 2492 2424 104 13

Both sides healthy: 291,425 lines on Loki and VictoriaLogs in 6/6 equal 10-minute slices, 15/15 services in Loki's range-metric path, Loki and VictoriaLogs restart counts 0 before and after every pass (full pass, repo rerun, confirmation pass). Proxy log over the repo rerun and the confirmation pass: 0 VictoriaLogs 5xx, 0 fallback/partial after a 5xx; 277 VictoriaLogs 4xx and 2 fallbacks after a 4xx (queries the proxy translated wrongly, i.e. the measured differences); the full pass's log was replaced when the proxies restarted. 12 differences did not reproduce on a second request and are blocked, not counted. 600 gap signatures + 5 recorded-deviation clusters, all claimed by exactly one of 66 registry cases (47 open, 4 owner-kept, 13 documented, 2 upstream). The entry-order and duplicate-series checks were added after the run; the run stored facets, not bodies, so they apply from the next run (--keep-bodies); no scalar/string difference was recorded, so the numeric scalar comparison changes nothing in it.

Ranked differences (top 30; full list in conformance/reports/parity-gaps.md)

# case difference status impact corpus queries clusters effort area
1 semantics/extracted-suffix-collision A parsed key named like a stream label is renamed with _extracted in query results open explore-visible 749 76 M internal/proxy stage-field exposure (parsed-label naming)
2 semantics/unwrap-sample-validity unwrap emits samples Loki skips: a missing label gives "" values, a unit string is passed through open explore-visible 295 64 M internal/proxy range-metric unwrap evaluation (stats pushdown over a missing/non-numeric field)
3 profiles/structured-metadata-label-collision Structured metadata named like a stream label gets Loki's _extracted suffix open explore-visible 293 20 M internal/proxy structured-metadata naming in log responses (collision with derived service_name)
4 profiles/keep-removes-derived-service-name | keep removes service_name the proxy derived for a stream without one open explore-visible 117 5 S internal/proxy service_name derivation after keep/drop
5 semantics/non-sum-vector-aggregation-unreduced max, min, count and avg over a range function return one series per stream on query_range open explore-visible 92 24 M internal/translator seriesLevelOuterAggregation (range path) + internal/proxy range stats folding
6 semantics/parenthesized-log-range A parenthesized log expression under a range function fails translation open explore-visible 77 41 S internal/logql parser / internal/translator (LogRangeExpr with parenthesized left side)
7 semantics/line-filter-or-alternatives Line filters with or alternatives are rejected open explore-visible 42 6 S internal/logql parser (line filter alternatives) + translator
8 severity/detected-level-stage-side-effects keep/drop detected_level and a detected_level filter change other labels open explore-visible 37 3 S internal/proxy/detected_level.go (level/detected_level coupling in keep/drop/filter)
9 semantics/logfmt-existence-filter-ungrouped-sum An ungrouped sum over | logfmt | key!="" returns no series open explore-visible 36 1 S internal/proxy/ordered_json_metric.go setStatsPushdown / pushdownLabelFilters (logfmt, ungrouped)
10 parsed-identity/bare-parser-metric-duplicates-extracted A bare | json metric labels each series with every parsed key twice, once with _extracted open explore-visible 34 14 S internal/proxy exact parser series identity (bare parser metric labels)
11 semantics/topk-bottomk-range-series topk/bottomk on query_range return k series overall, not the per-step top k open explore-visible 29 7 M internal/proxy topk/bottomk range ranking (cases limits/top-n-ranking-not-silent)
12 profiles/keep-structured-metadata-key | keep of a structured-metadata key drops it open explore-visible 24 1 S internal/proxy keep stage over structured metadata (Loki-compatible profile names)
13 semantics/label-filter-expression-syntax Comma-joined and parenthesized label-filter expressions are rejected open explore-visible 24 11 M internal/logql parser (label filter grammar) + translator filter emission
14 profiles/json-extraction-list-exposes-only-listed Extraction lists (| json msg="msg", | logfmt op) expose level from the line open explore-visible 18 1 S internal/proxy stage-field exposure (extraction lists)
15 semantics/first-last-over-time-translation first_over_time and last_over_time with offset or a line filter fail in VictoriaLogs open explore-visible 16 10 S internal/translator first/last_over_time emission (row_min/row_max with offset and filters)
16 semantics/tail-history-start-limit Native tail ignores start and limit: Grafana Live shows no history where Loki shows its last 100 lines open explore-visible 15 1 M internal/proxy/tail.go openNativeTailStream (history replay before the live stream)
17 semantics/binary-ignoring-matching + ignoring(label) between two sums joins the wrong series open explore-visible 10 2 S internal/proxy/vector_matching.go (ignoring with a label absent on both sides)
18 semantics/parse-error-labels-on-log-lines A line a parser rejects carries error and error_details in Loki's log answer open explore-visible 10 2 M internal/proxy log-row assembly (stage-field exposure) + translator parse-error predicate
19 semantics/absent-over-time-semantics absent_over_time answers when data exists and stays empty when it is absent, after a parser open explore-visible 8 6 S internal/translator absent_over_time (parser pipelines)
20 semantics/stddev-stdvar-grouped-values stddev and stdvar by a label compute different values open explore-visible 7 5 S internal/translator stddev/stdvar outer aggregation (by-group fold)
21 semantics/label-replace-new-label label_replace does not add a new destination label open explore-visible 5 1 S internal/proxy label_replace post-processing
22 quality/label-values-chunk-window Loki lists label values of streams whose chunks overlap the window; the proxy only of streams with lines in it open explore-visible 2 2 M internal/proxy label values window (chunk-overlap semantics)
23 semantics/pattern-line-filter An empty pattern line filter (|> "", !> "") selects the opposite lines open explore-visible 2 2 S internal/translator pattern line filter
24 semantics/log-entries-differ-same-count A log query returns the same number of lines with different entries open explore-visible 1 1 S to be isolated (log query paging / numeric label filters)
25 profiles/label-filter-regexp-anchoring A label-filter regexp after a parser is not anchored the way Loki's is open explore-visible — — S internal/translator label-filter regexp emission (anchor ^(?:re)$); external PR #572 open
26 quality/label-cache-aged-labels-kept Labels that age out of the window are never dropped from the merged label cache open explore-visible — — S internal/proxy/label_metadata.go:1259 mergeLabelsIntoCache
27 semantics/unwrap-conversion-error An unwrap over non-numeric values fails in Loki with a 400 SampleExtractionErr; the proxy answers 200 open explore-visible — — M internal/proxy range metric unwrap path (one-lookup probe like pipeline-error-text-parity)
28 semantics/unwrap-range-last-point A grouped unwrap range metric misses the sample at end open explore-visible — — S internal/proxy range-metric unwrap path (end bucket)
29 drilldown/detected-field-values detected_field values differ: none for service_name, other values for numeric and id fields open drilldown-visible 33 3 M internal/proxy detected_field values (sampling window and stream-label fields)
30 profiles/json-number-label-text JSON numbers become label values in a different text form ("14.0" vs "14") open drilldown-visible 12 6 M internal/proxy JSON number rendering of unpacked values (VictoriaLogs unpack_json normalises)

Visual / A-B proof

Not applicable: no proxy code change (tooling, tests and registry only).

Gates

  • python3 scripts/ci/check_conformance.py: ok (parity registration: 66 cases, 605 clusters, 0 problems)
  • python3 scripts/ci/check_changelog_pr.py --base origin/main --head HEAD: ok
  • python3 bench/ab/selection.py --check, python3 bench/visual/plan.py --check: ok
  • python3 -m unittest discover -s bench/parity/tests (44, wired into ci.yaml), bench/visual/tests (66), bench/ab/tests (29): ok

bench/parity: a differential runner for the proxy against Loki on an
isolated stack with identical data. It builds a corpus from Loki v3.7.7's
own LogQL test suites (selectors moved onto the seeded data), the
repository's parity cases, a sweep generated from what Loki reports for the
window (labels, values, detected fields and the stages Explore and Logs
Drilldown build on them, the metadata endpoints, patterns, format_query,
tail) and the Grafana Explore and Logs Drilldown requests captured by
bench/visual, replayed through Grafana. It proves both sides healthy and
equal before and after (per-slice line counts, Loki's blank window, known
and unchanged Loki and VictoriaLogs restart counts, no VictoriaLogs failure
in the proxy log), diffs every answer semantically with and without
categorize-labels, reconfirms samples of every difference, and clusters the
rest by endpoint, query shape and kind of difference. --keep-bodies and
--rediff let a later diff rule be applied to a recorded run offline.

The run on main at fcaccfc is recorded in
conformance/registry/generated/parity-discovery.json; every cluster is
claimed by exactly one registry case with a gap block (Loki reference,
proxy behaviour, impact, effort, code area, planned test), recorded
deviations by the case id the diff names. conformance/scripts/parity_gaps.py
ranks them into conformance/reports/parity-gaps.md and fails the gate on an
unclaimed or doubly claimed cluster, a deviation that names no recorded
case, or `status: fixed` without `fixed_by`.

bench/visual/stack.py takes --overlay for a further compose file; the
parity overlay raises Loki's gRPC message limits so per-stream metric
answers are not dropped between Loki's querier and frontend.
@github-actions github-actions Bot added size/XL Extra large change scope/ci CI/CD scope/docs Documentation labels Oct 5, 2026
Comment thread bench/parity/cluster.py
a = ap.parse_args()
rows = load_rows(os.path.join(a.run_dir, "results.jsonl"))
health_path = os.path.join(a.run_dir, "health.json")
health = json.load(open(health_path)) if os.path.exists(health_path) else {}
Comment thread bench/parity/corpus.py
for raw in sorted(extract_strings(glob.glob(os.path.join(ROOT, "test", "e2e-compat", "*_test.go")))):
add("repo", raw, "test/e2e-compat")
matrix = os.path.join(ROOT, "test", "e2e-compat", "query-semantics-matrix.json")
for case in json.load(open(matrix))["cases"]:
Comment thread bench/parity/corpus.py
matrix = os.path.join(ROOT, "test", "e2e-compat", "query-semantics-matrix.json")
for case in json.load(open(matrix))["cases"]:
add("repo", case["query"], f"query-semantics-matrix:{case['id']}")
shapes = json.load(open(os.path.join(ROOT, "bench", "ab", "shapes.json")))
Comment thread bench/parity/corpus.py
if shape.get("query"):
add("repo", shape["query"], f"shapes:{name}:{shape['name']}")
for path in sorted(glob.glob(os.path.join(ROOT, "conformance", "registry", "cases", "**", "*.yaml"), recursive=True)):
text = open(path).read()
Comment thread bench/parity/corpus.py
for directory in capture_dirs:
for path in sorted(glob.glob(os.path.join(directory, "**", "*.json"), recursive=True)):
try:
doc = json.load(open(path))
Comment thread bench/parity/publish.py
ap.add_argument("--ref", required=True)
ap.add_argument("--out", default=OUT)
a = ap.parse_args()
data = json.load(open(os.path.join(a.run_dir, "clusters.json")))
Comment thread bench/parity/publish.py
ap.add_argument("--out", default=OUT)
a = ap.parse_args()
data = json.load(open(os.path.join(a.run_dir, "clusters.json")))
health = json.load(open(os.path.join(a.run_dir, "health.json")))
Comment thread bench/parity/publish.py
# Runs merged into this one (an earlier pass, a rerun of one source) keep their own proof.
passes = {}
for path in sorted(glob.glob(os.path.join(a.run_dir, "health-*.json"))):
h = json.load(open(path))
Comment thread bench/parity/run.py
"fallback_after_4xx": 0, "partial_after_5xx": 0, "lines": 0}
if not path or not os.path.exists(path):
return {}
for line in open(path, errors="ignore"):
Comment thread bench/parity/run.py
help="confirm the differences of an existing RUN_DIR/results.jsonl instead of running the corpus")
a = ap.parse_args()
if a.state:
state = json.load(open(a.state))
@github-actions

github-actions Bot commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

PR Quality Report

Compared against base branch main.

Coverage and tests

Signal Base PR Delta
Test count 8274 8274 0
Coverage 90.6% 90.6% 0.0% (stable)

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

Performance smoke was skipped for this PR because no perf-sensitive paths changed.

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 smoke was intentionally skipped because no perf-sensitive paths changed in this PR.
  • 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 5, 2026

Copy link
Copy Markdown
Contributor

✅ Visual smoke: passed with warnings

Base 11db66713c vs PR 3ceff444e8, Grafana through the proxy builds and Loki on one fresh stack; run.
Core set: 5 capture(s) at 1h. Detailed set: 0 entries (nothing visual touched). 5 capture(s), 15m/1h/6h, 24h and 7d stay out of PR runs.

page range set base = PR (data) vs Loki (PR) pixel diff result
dd-fields 1h core ✅ identical identical (5 explained, 1 history-dependent) 0.11% ⚠️ history-dependent difference, not gated (1): resource /api/datasources/uid/*/res
dd-landing 1h core ✅ identical identical (8 explained) 0.13% ✅
dd-service-logs 1h core ✅ identical identical (4 explained, 1 history-dependent) 0.11% ⚠️ history-dependent difference, not gated (1): resource /api/datasources/uid/*/res
explore-logs-plain 1h core ✅ identical identical (2 explained) 0.07% ✅
explore-metric-plain 1h core ✅ identical identical (1 explained) 0.04% ✅

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

dd-fields-1h: history-dependent difference, not gated (1): resource /api/datasources/uid/*/resources/patterns?end=2026-10-05T21%3A27%3
  • history-dependent difference, not gated (1): resource /api/datasources/uid/*/resources/patterns?end=2026-10-05T21%3A27%3A00.000Z&query=%7Bservic: body: content differs (rows 6 vs 6)
  • vs Loki, explained: query ('{service_name="otel-collector"} | json | logfmt | drop __error__, __error_details__: ('logsPanelQuery', '', '{}'): content differs (rows 1000 vs 1000) -- explained: Loki's _extracted suffix for a key named like a stream label (structured metadata: open, owner decision pending, profiles/structured-metadata-label-collision; detected_fields: documented deviation)
  • vs Loki, explained: query ('sum by (service_name_extracted) (count_over_time({service_name="otel-collector"} | servi: request only on one side (right) -- explained: Loki's _extracted suffix for a key named like a stream label (structured metadata: open, owner decision pending, profiles/structured-metadata-label-collision; detected_fields: documented deviation)
  • vs Loki, explained: resource /api/datasources/uid/*/resources/detected_fields?end=2026-10-05T21%3A27%3A00.000Z&query=%7: body: content differs (rows 13 vs 15) -- explained: Loki's _extracted suffix for a key named like a stream label (structured metadata: open, owner decision pending, profiles/structured-metadata-label-collision; detected_fields: documented deviation); the proxy also returns data, status; Loki lists fields in no fixed order
  • vs Loki, explained: resource /api/datasources/uid/*/resources/detected_labels?end=2026-10-05T21%3A27%3A00.000Z&query=%7: body: content differs (rows 9 vs 9) -- explained: sampled cardinality: Loki counts every stream its ingesters hold, not the requested window (pkg/ingester/instance.go LabelsWithValues), the proxy the window's last 5 minutes (metadataMaxFieldNamesWindow); the label names match; the proxy also returns data, limit, status; Loki lists labels in no fixed order

dd-fields-1h

dd-service-logs-1h: history-dependent difference, not gated (1): resource /api/datasources/uid/*/resources/patterns?end=2026-10-05T21%3A27%3
  • history-dependent difference, not gated (1): resource /api/datasources/uid/*/resources/patterns?end=2026-10-05T21%3A27%3A00.000Z&query=%7Bservic: body: content differs (rows 6 vs 6)
  • vs Loki, explained: query ('{service_name="otel-collector"} | json | logfmt | drop __error__, __error_details__: ('logsPanelQuery', '', '{}'): content differs (rows 1000 vs 1000) -- explained: Loki's _extracted suffix for a key named like a stream label (structured metadata: open, owner decision pending, profiles/structured-metadata-label-collision; detected_fields: documented deviation)
  • vs Loki, explained: resource /api/datasources/uid/*/resources/detected_fields?end=2026-10-05T21%3A27%3A00.000Z&query=%7: body: content differs (rows 13 vs 15) -- explained: Loki's _extracted suffix for a key named like a stream label (structured metadata: open, owner decision pending, profiles/structured-metadata-label-collision; detected_fields: documented deviation); the proxy also returns data, status; Loki lists fields in no fixed order
  • vs Loki, explained: resource /api/datasources/uid/*/resources/detected_labels?end=2026-10-05T21%3A27%3A00.000Z&query=%7: body: content differs (rows 9 vs 9) -- explained: sampled cardinality: Loki counts every stream its ingesters hold, not the requested window (pkg/ingester/instance.go LabelsWithValues), the proxy the window's last 5 minutes (metadataMaxFieldNamesWindow); the label names match; the proxy also returns data, limit, status; Loki lists labels in no fixed order
  • vs Loki, explained: resource /api/datasources/uid/*/resources/drilldown-limits: body: content differs (rows 0 vs 0) -- explained: configuration probe: each backend publishes its own limits, version and settings (the proxy's pattern persistence)

dd-service-logs-1h

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

dd-landing-1h

dd-landing-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-657.

@szibis
szibis merged commit 2fc2247 into main Oct 5, 2026
62 checks passed
@szibis
szibis deleted the test/parity-gap-inventory branch October 5, 2026 21:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope/ci CI/CD scope/docs Documentation size/XL Extra large change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant