Loki Compatibility
This track measures how closely the proxy behaves like Loki on Loki-facing APIs and LogQL behavior.
Scope​
/labels,/label/<name>/values,/series,/query,/query_range- LogQL parser, filter, metric, and OTel label compatibility
- Synthetic compatibility labels the proxy must expose to Loki clients, such as
service_name - Manifest-driven query semantics parity against real Loki in query-semantics-matrix.json
- Tracked operation inventory in query-semantics-operations.json
CI And Score​
- Workflow:
compat-loki.yaml - Score test:
TestLokiTrackScore - Required PR gate:
loki-pinned, which runs theTestQuerySemantics*inventory + matrix suite - Runtime matrix: real Loki images
- Support window: current Loki minor family plus one minor family behind
The Loki matrix is a moving window. When a new Loki minor becomes current, the matrix advances to that family and the immediately previous minor family. Older minors drop out of support.
Version Matrix​
| Loki version | Coverage path | Version-specific focus |
|---|---|---|
3.7.1 | PR and main CI pinned runtime | Primary supported reference; LogQL validation error format, logfmt type inference, bare | unwrap drop, metric label pollution, nested JSON field exclusion |
3.7.0 | Scheduled and manual matrix | detected_level metric grouping, OTel label parity |
3.6.10 | Scheduled and manual matrix | Range query matrix shape, detected fields stability |
3.6.9 | Scheduled and manual matrix | Range query matrix shape, detected fields stability |
3.6.8 | Scheduled and manual matrix | Range query matrix shape, detected fields stability |
3.6.7 | Scheduled and manual matrix | Range query matrix shape, detected fields stability |
3.6.6 | Scheduled and manual matrix | Range query matrix shape, detected fields stability |
3.6.5 | Scheduled and manual matrix | Range query matrix shape, detected fields stability |
3.6.4 | Scheduled and manual matrix | Range query matrix shape, detected fields stability |
3.6.3 | Scheduled and manual matrix | Range query matrix shape, detected fields stability |
3.6.2 | Scheduled and manual matrix | Range query matrix shape, detected fields stability |
3.6.1 | Scheduled and manual matrix | Range query matrix shape, detected fields stability |
3.6.0 | Scheduled and manual matrix | Range query matrix shape, detected fields stability |
Proxy Behavior Parity — Release Notes​
v1.16.0 — LogQL Syntax Validation​
The proxy now validates LogQL before attempting translation and returns HTTP 400 with a plain-text parse error body matching Loki's exact error format. Before this release, invalid queries returned HTTP 200 or an opaque proxy error instead.
Shapes that now return HTTP 400:
| Shape | Example |
|---|---|
| Binary operation on log/pipeline expressions | {app="x"} == {app="y"}, count_over_time({app="x"}[5m]) + {app="y"} |
| Malformed stream selector | {app=}, unclosed {app="x" |
| Empty query or missing stream selector | bare ` |
The -patterns-enabled flag defaults to true and controls whether pattern-match line filters (|> / !>) are accepted.
v1.17.0 — detected_fields Type Inference​
Logfmt fields now report type=int or type=float for numeric values instead of always reporting type=string. For example, status=400 is inferred as int and latency_ms=300 as int. This matters because Grafana's Drilldown plugin filters unwrap field candidates by type=int or type=float; logfmt numeric fields were invisible in the unwrap dropdown before this fix.
v1.17.0 — Bare | unwrap Silent Drop​
A bare | unwrap with no field name (emitted by the Grafana query builder while a user is actively selecting a field) is now silently stripped from the pipeline instead of producing a VictoriaLogs parse error. This is an intermediate query state that Loki itself handles gracefully.
v1.17.1 — Metric Label Pollution Fix​
When detected_level is synthesized from the level label in metric aggregation results (for example sum by (detected_level)), the raw level label is now removed from the result series. Before this fix sum by (detected_level) returned a series with both detected_level and level set to "error"; after it returns only detected_level="error", matching Loki's output.
v1.17.1 — Nested JSON Objects Excluded From detected_fields​
Nested JSON objects (for example a field whose value is itself a JSON object such as service.name = "api-gateway" encoded as an object) are now skipped during the detected_fields body scan. Before this fix these appeared as filterable fields with a raw JSON string as their value, which broke the Grafana Drilldown field breakdown panel.
v1.20.0 — detected_level Inference and Bare Label Matchers​
detected_level is now synthesised from JSON/logfmt level, severity, and lvl field variants, matching Loki's level inference rules. Bare label matchers (e.g. {app="api"} without a filter pipeline) now preserve the stream label set exactly as Loki does.
v1.21.x — label_replace, label_join, group, count_values, Stream Ordering​
label_replace() and label_join() metric transformations, the group() aggregation, and count_values() now produce output that matches Loki's label semantics. Stream result ordering for log queries is aligned with Loki's timestamp-descending default.
count_values() returns HTTP 400 with message: "count_values is not supported: it groups by metric values which VictoriaLogs cannot compute; use count() grouped by an existing label instead"
v1.29.x — error Opt-In and Cold Storage Routing​
The __error__ / | drop __error__ mechanism for opting parser-stage metric queries into the VL native stats fast path is now precisely gated: only queries where | drop __error__ is the sole post-parser stage qualify, preventing false positives from queries that mention __error__ in label values.
v1.30.0 — detected_fields Hybrid OTel/Non-OTel​
service_name alias now appears correctly in detected_fields for hybrid datasets that mix OTel service.name stream labels with pre-normalised service_name labels. The previous guard blocked the alias whenever any entry carried a literal service_name stream key.
v1.31.0 — Bare Parser-Stage Metric Label Scoping​
rate(| json) and similar bare parser-stage metrics no longer include parsed JSON field names (e.g. path, method, status) as metric series labels. Only stream labels appear in the output, matching Loki's behaviour where parsed fields become metric dimensions only when explicitly named in an outer by (...) aggregation.
v1.31.2 — Parser-Stage Guard Removed From stats Fast Path​
rate, bytes_rate, count_over_time, and bytes_over_time queries containing | json or | logfmt parser stages now use VL native stats_query_range for tumbling windows (range == step). These queries were previously forced onto the raw log-fetch slow path regardless of window type.
v3.7.1 — drop Matcher Semantics, Structured Metadata Classification, Parity Expansion​
| drop field=value (matcher form) now correctly implements conditional semantics: VL | delete is issued for the field, and the proxy post-processes each log entry to restore the field value when it does not match the predicate. Previously the value predicate was ignored and the field was always removed.
structuredMetadata vs parsedFields classification in push/detected_fields paths is now correct: the proxy compares candidate field names against _msg JSON content to determine whether a field originates from structured metadata or was parsed from the log body.
The -emit-structured-metadata flag (default: true) controls whether log entries are returned as Loki 3-tuples [timestamp, line, {metadata}] or 2-tuples [timestamp, line]. Grafana Loki datasource requires 3-tuple format; only disable if your client cannot handle structured metadata.
The LogQL exhaustive parity machine covers 555+ cases, with all 14 previously tracked proxy_bug and proxy_strict KnownGaps resolved. Default label-style is now underscores and metadata-field-mode is now translated.
Edge Cases Covered​
- JSON and logfmt parser chains followed by field filters
detected_levelgrouped metric queries used by Grafana log volume panels- OTel dotted and underscore label parity through the underscore proxy
- Series and label-value parity for labels synthesized by the proxy
- parser-stage range metrics where labelset parity must match Loki under compatibility execution
rate_counter(... | unwrap ...)reset-aware semantics and error-class parity- logfmt numeric field type inference for unwrap dropdown visibility (v1.17.0)
- bare
| unwrapintermediate-state handling (v1.17.0) - metric label pollution when synthesizing
detected_level(v1.17.1) - nested JSON object exclusion from detected_fields body scan (v1.17.1)
label_replace/label_join/group/count_valuesmetric transformation parity (v1.21.x)service_namealias in detected_fields for hybrid OTel/non-OTel datasets (v1.30.0)- bare parser-stage metric label scoping (v1.31.0)
- parser-stage tumbling-window fast path via VL native stats (v1.31.2)
| drop field=valuematcher-form conditional semantics via proxy post-processing (v3.7.1)- structuredMetadata vs parsedFields classification fix using
_msgJSON content comparison (v3.7.1)
Query Families In The Loki Semantics Matrix​
The Loki semantics matrix focuses on query combinations where the proxy should match Loki as closely as possible on:
- HTTP status
- payload
status errorTyperesultType- line-count parity for log streams
- series-count parity for vectors and matrices
- exact metric-label-set parity for label-sensitive metric families such as bare parser metrics
The tracked operation inventory in query-semantics-operations.json is machine-checked in CI. Every matrix case must belong to at least one inventory operation, and every inventory operation must reference live matrix cases.
Covered valid families:
| Family | Representative cases |
|---|---|
| Stream selectors | exact match, multi-label match, regex, negative match |
| Line filters | ` |
| Parser pipelines | json, logfmt, regexp, pattern, parser plus exact/regex/numeric field filter, label_format |
| Metric range queries | count_over_time, rate, bytes_over_time, bytes_rate, absent_over_time, grouped range aggregations, parser-inside-range filters, bare unwrap range functions |
| Aggregations | sum by(...), without(...), topk(...), bottomk(...), sort(...), sort_desc(...) |
| Binary operations | scalar comparisons/math, bool comparisons, and vector-to-vector operations such as /, and, or, and unless over valid metric expressions |
Detailed operation inventory:
| Category | Operations currently enforced in CI |
|---|---|
| Selectors | exact selectors, multi-label regex selectors, negative-regex selectors |
| Line filters | ` |
| Parsers | json, logfmt, regexp, pattern plus parsed/extracted field filters |
| Formatting | line_format, label_format, keep, drop |
| Metric functions | count_over_time, rate, bytes_over_time, bytes_rate, absent_over_time, sum/avg/max/min/first/last/stddev/stdvar_over_time with unwrap, quantile_over_time with unwrap |
| Aggregations | sum by(...), sum without(...), topk, bottomk, sort, sort_desc |
| Binary operators | scalar math/comparison, scalar bool comparison, vector arithmetic, on(...), group_left(...), logical and, or, unless |
| Invalid shapes | log-query aggregation misuse, missing metric range, malformed selector/parser syntax, invalid log-query binary ops; all now return HTTP 400 with Loki-matching parse error text |
Explicit invalid families:
| Family | Representative cases |
|---|---|
| Metric aggregation over a log query | sum by(job) ({selector}) |
| Post-aggregation over a log query | topk(2, {selector}), sort({selector}) |
| Missing range on a metric function | rate({selector}) |
| Malformed selector / syntax | broken braces ({app=}), unclosed selector ({app="x"), parser syntax errors |
| Invalid binary shape | log-query to scalar/vector binary expressions ({app="x"} == {app="y"}, count_over_time(...) + {selector}) |
These are intentionally called out because they are easy to regress while changing translation, shaping, or query planning.
What Stays Outside Loki Parity​
Some important compatibility behavior is still tested, but it is not part of the strict Loki parity matrix:
- synthetic
service_namerecovery when the backend only has structured metadata - Drilldown helper endpoints like detected labels, detected fields, field values, volume, and patterns
- stale-on-error helper behavior under VictoriaLogs failures
Those cases live in the proxy contract suite because Loki itself is not the source of truth for them.
Parity Rule​
Valid Loki behavior is not an allowed exclusion category.
If a query shape works in real Loki and the proxy does not match it, that is treated as a parity bug and should be fixed or tracked with an explicit regression case. The bare parser-metric and bare unwrap metric shapes now live inside the required matrix for that reason.
Detailed Edge Cases Now Gated​
The required matrix is intentionally not limited to happy-path selectors. It now includes:
- parser-derived metric labelsets, not just result counts
- bare
unwraprange functions where Loki keeps parsed labels but not the unwrap target field itself patternparser extraction semantics- set-style binary operators such as
orandunless boolcomparison semantics on metric expressions- parser-stage metric compatibility path selection for
queryandquery_rangewhen backend stats semantics diverge rate_counterparity for parser and non-parser query forms- metric aggregations that do not group by service labels must not receive synthetic
service_name="unknown_service"in query/query_range responses - invalid log/metric shape rejections that must fail with the same class of error as Loki
offsetmodifier -- time-shifting parity (fully implemented since v1.32.0)unpackparser -- translates tounpack_json, e2e parity testedunwrap duration()/unwrap bytes()conversion modifiers -- proxy-side conversion paritylabel_replace()-- proxy-side post-processing implementation, parity tested|>/!>pattern match line filter -- Loki 3.7+ support, parity tested
When a new LogQL family is implemented or fixed in the proxy, the expectation is to add:
- a runtime matrix case in
query-semantics-matrix.json - an inventory entry in
query-semantics-operations.json - a local or unit regression if the fix needed proxy-side translation or shaping changes