Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed

- **A zero divisor of `/` and `%` is NaN, like Loki.** `sum(rate({app="api"}[5m])) / 0`,
`5 / <a series whose value is 0>`, `-1 / 0` and `1 / 0` answered `+Inf` or `-Inf`
(IEEE division), and the fallback sample arithmetic used when a query is not
parsed as a binary expression answered `0` for `/` and `%`. Loki routes every binary operation through
`syntax.MergeBinOp`, which returns NaN for a zero right operand of `/` and `%`
whatever the numerator is, for a literal fold (`1 / 0`), a vector against a
literal and two vectors alike. The proxy now returns NaN in every sample
arithmetic path; comparisons (with and without `bool`), `^`, NaN and infinite
operands already matched Loki and are pinned against a direct port of
`MergeBinOp` by an operator by operand edge table. Aggregations over a
division by zero are not changed here. (#745)

## [2.11.2] - 2026-10-12

### CI
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
id: semantics/binary-division-by-zero-nan
title: A zero divisor of `/` and `%` is NaN, like Loki (fixed)
proves:
- loki_api_v1_query_range
- loki_api_v1_query
consumer: explore
why: |
Loki guards a zero right operand of `/` and `%` with NaN in the one function
every binary operation goes through, syntax.MergeBinOp
(pkg/logql/syntax/ast.go:1978-2005): `if right.F == 0 { res.F = math.NaN() }`.
It applies to a literal fold (reduceBinOp, ast.go:1918-1935, so `1 / 0` is NaN),
to a vector against a literal (LiteralStepEvaluator.Next,
pkg/logql/evaluator.go:1179) and to two vectors (vectorBinop, evaluator.go:1020),
whatever the numerator is: `sum(rate(..)) / 0`, `-1 / 0` and `Inf / 0` are all
NaN, never +Inf or -Inf. The proxy computed IEEE a/b, so a nonzero numerator
over a zero divisor came back as "+Inf" or "-Inf" (only 0/0 was NaN), and the
legacy sample arithmetic used when a query is not parsed as a binary
expression answered 0 for both operators. `%` by zero was already NaN through
math.Mod on the main path. The comparison operators, `^`, NaN operands and
infinite operands already matched MergeBinOp; the edge table in
TestBinarySampleValueMatchesLokiMergeBinOp pins every operator against a
direct port of it, with and without bool.

min and max across series skip a NaN unless every value is NaN: Loki's
VectorAggEvaluator replaces a NaN accumulator (pkg/logql/evaluator.go:525-535),
so `max(x / 0)` over mixed values is the largest real value. Residual, not
fixed here: the one proxy fold that lets NaN win is withoutMerge.combine
(math.Min / math.Max in internal/proxy/vector_matching.go), fixed by #734
(semantics/non-sum-vector-aggregation-unreduced). With this change no other
min/max fold receives a NaN: on origin/main an aggregation over a binary
expression drops the binary operand (`max(v / 2)` and `max(v / 0)` send VL the
same query without the division), which #741 fixes; the folds #741 adds must
apply Loki's NaN rule.
request:
method: GET
path: /loki/api/v1/query_range
params:
query: 'sum(count_over_time({app="payment-service"}[5m])) / 0'
start: 'now-1h'
end: 'now'
step: '60'
headers:
X-Scope-OrgID: "0"
compare:
mode: observed
fields: [series, values]
expect: |
One unlabelled series whose every value is "NaN".
gap:
status: fixed
fixed_by: "#745"
impact: api-only
effort: S
area: 'internal/proxy binary_matching.go (binarySampleValue), metric_binary.go (applyOp)'
loki: 'pkg/logql/syntax/ast.go:1944-2125 MergeBinOp (the / and % guards at :1978-2005), reduceBinOp :1918; pkg/logql/evaluator.go:1020 vectorBinop, :1179 LiteralStepEvaluator.Next'
proxy: 'a zero divisor of / and % is NaN in every sample-arithmetic path'
planned_test: 'TestCompat_BinaryDivisionByZeroIsNaN (test/e2e-compat, group core); TestBinarySampleValueMatchesLokiMergeBinOp, TestBinaryScalarFoldMatchesLokiReduceBinOp, TestBinaryMatchersMatchLokiMergeBinOp, TestApplyOpDivisionAndModuloByZeroAreNaN, TestBinaryVectorDivisionByZeroHandlers and TestBinaryConstantVectorDivisionByZeroInstant (internal/proxy)'
note: 'Measured on origin/main b20a550f. The e2e is written and not yet run. min/max NaN folding: residual in #734 (withoutMerge.combine) and #741 (aggregation over binary).'
clusters: []
20 changes: 17 additions & 3 deletions conformance/registry/generated/wiring.json
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@
"internal/proxy/tenant_query_limits_test.go",
"internal/proxy/tumbling_axis_parity_test.go",
"internal/proxy/unwrap_conversion_error_test.go",
"test/e2e-compat/binary_division_zero_test.go",
"test/e2e-compat/extracted_suffix_collision_test.go",
"test/e2e-compat/parenthesized_range_or_filters_test.go",
"test/e2e-compat/parser_capture_identity_test.go",
Expand All @@ -210,6 +211,7 @@
"internal/proxy/unwrap_conversion_error_test.go",
"internal/translator/extracted_suffix_test.go",
"internal/translator/regexp_escaping_test.go",
"test/e2e-compat/binary_division_zero_test.go",
"test/e2e-compat/drilldown_field_breakdown_test.go",
"test/e2e-compat/extracted_suffix_collision_test.go",
"test/e2e-compat/heavy_query_protection_test.go",
Expand Down Expand Up @@ -361,6 +363,10 @@
"semantics/backend-timeout-argument": [
"internal/proxy/backend_heavy_test.go"
],
"semantics/binary-division-by-zero-nan": [
"internal/proxy/binary_division_zero_test.go",
"test/e2e-compat/binary_division_zero_test.go"
],
"semantics/client-cancellation-propagates": [
"internal/proxy/backend_heavy_test.go"
],
Expand Down Expand Up @@ -541,7 +547,7 @@
"test/e2e-compat/unwrap_ungrouped_sum_test.go"
]
},
"registry_items": 299,
"registry_items": 300,
"routes_without_registry_item": [],
"test_to_items": {
"cmd/proxy/main_test.go": [
Expand Down Expand Up @@ -592,6 +598,9 @@
"operator-configurable-limits",
"semantics/backend-aborted-response"
],
"internal/proxy/binary_division_zero_test.go": [
"semantics/binary-division-by-zero-nan"
],
"internal/proxy/compat_cache_freshness_test.go": [
"semantics/metadata-answers-include-last-24h-like-loki"
],
Expand Down Expand Up @@ -895,6 +904,11 @@
"test/e2e-compat/bad_request_status_parity_test.go": [
"loki-compatible-profile"
],
"test/e2e-compat/binary_division_zero_test.go": [
"loki_api_v1_query",
"loki_api_v1_query_range",
"semantics/binary-division-by-zero-nan"
],
"test/e2e-compat/compat_option_matrix_test.go": [
"loki-compatible-profile",
"profiles/detected-fields-dotted-json-keys",
Expand Down Expand Up @@ -1064,7 +1078,7 @@
"profiles/structured-metadata-keys-per-profile"
]
},
"tests_with_markers": 96,
"tests_with_markers": 98,
"unknown_ids": {},
"unwired_items": [
"api_prom_label",
Expand Down Expand Up @@ -1172,5 +1186,5 @@
"versions/capabilities-floor-vs-matrix",
"versions/rejected-stats-query-wording-vl-1.47"
],
"wired_items": 106
"wired_items": 107
}
1 change: 1 addition & 0 deletions conformance/reports/parity-gaps.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,4 +114,5 @@ Last run: 2026-10-05T20:16:15Z on `fcaccfcc`, window [1791220800, 1791224400].
| `semantics/unwrap-rate-ungrouped-sum-per-stream` — An ungrouped sum over rate with unwrap is one series on query_range, like Loki (fixed) | #716 | 0 | 0 |
| `severity/otlp-severity-before-vl-1.50` — OTLP rows stored by VictoriaLogs before v1.50 (one `severity` field, no severity_number) get detected_level "Unspecified" / "Info2" (fixed) | #666 | 0 | 0 |
| `versions/rejected-stats-query-wording-vl-1.47` — The proxy did not recognise VictoriaLogs v1.47 parse errors, so a rejected query was not Loki's 400 (fixed) | #659 | 0 | 0 |
| `semantics/binary-division-by-zero-nan` — A zero divisor of `/` and `%` is NaN, like Loki (fixed) | #745 | 0 | 0 |
| `versions/capabilities-floor-vs-matrix` — logsql.Capabilities treated VictoriaLogs below v1.40 as unsupported while the matrix claimed v1.30 (fixed) | #659 | 0 | 0 |
Loading
Loading