Compare commits

..

3 Commits

Author SHA1 Message Date
Pandey
53ab4546bc chore(deps): bump clickhouse-sql-parser to v0.5.5 (#12454)
Some checks are pending
build-staging / prepare (push) Waiting to run
build-staging / js-build (push) Blocked by required conditions
build-staging / go-build (push) Blocked by required conditions
build-staging / staging (push) Blocked by required conditions
cacheci / tests (push) Waiting to run
Release Drafter / update_release_draft (push) Waiting to run
Bumps `clickhouse-sql-parser` to v0.5.5, fixes the false rejection that
was left over once it landed, and closes three holes in the same
validator that the first two changes brought to light.

## The bump

**Reserved keywords as expression operands**
([#305](https://github.com/AfterShip/clickhouse-sql-parser/pull/305)).
`interval` was fixed in v0.5.4, but the same defect affected 36 other
keywords once the column appeared as an operand rather than bare.
Sweeping 94 candidates against ClickHouse 26.8.1.337, only `on` still
rejects — and ClickHouse runs that too. This one was live: `sum(limit)`
on a metric label.

**Panic on an unparseable `DEFAULT` expression**
([#306](https://github.com/AfterShip/clickhouse-sql-parser/pull/306)).
Both known cases return a parse error now instead of dereferencing nil.
The `recover` in `ErrIfStatementIsNotValid` stays — it guards the next
one of these, not these two.

[#307](https://github.com/AfterShip/clickhouse-sql-parser/pull/307) also
allows `CAST` in a table function's argument list.

## Table functions are only table functions in a table position

The parser types a call inside a table function's argument list as a
`TableFunctionExpr` as well, so the generator allow list only ever
cleared a generator whose argument was a literal. Every real dashboard
computes its row count — `numbers(greatest(1, intDiv(end_ns - start_ns,
step_ns) + 1))` — and every one was refused, on `intDiv` rather than on
`numbers`.

`TableExpr.Expr` is the only table position a SELECT can reach, so the
allow list asks that instead. Of the four places the parser builds a
`TableFunctionExpr`, two are `CREATE TABLE` paths rejected as
not-a-SELECT before the walk starts, one is `parseTableArgPrimaryExpr`,
and one is the `FROM`/`JOIN` path that wraps into a `TableExpr`.

## Three holes that were already open

Skipping argument position is only safe if nothing there can read, and
that turned out not to be true — not because of this change, but
independently of it.

**Reading functions.** `file` is both a table function and a scalar
function, and the validator never inspected scalar calls at all. On
`main` today, `SELECT file('/etc/passwd')` is accepted and returns the
file. A numeric wrapper passes ClickHouse's type check, so the row count
alone is an oracle: `numbers(length(file(x)))` yields one row per byte.
The same applies to the 42 dictionary accessors, which can be backed by
HTTP, ODBC or another database, to `catboostEvaluate`, and to the
introspection functions. All are now refused by name wherever they
appear, under `clickhouse_sql_reading_function`.

**`x IN db.table`.** ClickHouse reads this as `x IN (SELECT * FROM
db.table)`, and a qualified name on the right of `IN` parses as a
`Path`, not a `TableIdentifier` — so `SELECT * FROM t WHERE a IN
system.users` bypassed the internal-database rule entirely. Now checked,
including the `GLOBAL IN` and `NOT IN` forms.

**Quoted generator names.** The allow list matched on the formatted
name, which carries the quoting, so ``SELECT * FROM `numbers`(31)`` was
refused. It now reads the identifier the way the internal-database
branch already did.

## Effect

Replaying 72 distinct shapes of production `clickhouse_sql` that the
validator currently rejects: **64 pass, up from 59 on v0.5.4**. Two came
from the bump, three from the table-position change, and those three are
379 of the 1390 sampled occurrences. The three new rules add no false
positives to the corpus.

Of the eight left, four are correct rejections (`system` reads, `SHOW
TABLES`), one is a dashboard variable rendering as the literal `<no
value>`, one is SQL ClickHouse also rejects, and two are an open
upstream gap.

## Tests

`TestErrIfStatementIsNotValid_ShouldPassButFails` is back, holding what
remains: three forms of a parenthesised left operand of a set operator,
and `on` as a column name. It also stopped panicking — `errors.Asc`
dereferences the error it is given, so a case starting to pass took the
suite out with a SIGSEGV instead of reporting. Both refusal tables now
share one harness, bounded by the same timeout the passing table uses.

Known gap: no input is currently known to panic the parser, so the
`recover` has no test exercising it.
2026-08-07 10:39:21 +00:00
Srikanth Chekuri
5c0dfe2ad1 feat(promql): transpile allowlisted query shapes to ClickHouse grid statements (#12325)
> **Stack** (review in order; each PR's diff is against its
predecessor):
> 1. #12323 `v2-read-path` — v2 native read path (leaf package)
> 2. #12324 `v2-wiring` — wiring, shadow/pin rollout machinery, dual-leg
conformance
> 3. #12325 `v2-transpiler` — PromQL→ClickHouse transpiler +
classification golden
> 4. #12093 `issue-4293` — the /prometheus API move (breaking slice,
last)

### What

The performance half of the v2 provider: an allowlist compiler
(`classify`/`rewrite`) that evaluates proven PromQL shapes entirely
inside
ClickHouse on the `timeSeries*ToGrid` aggregate functions (CH ≥ 25.6),
so one
row per output series comes back instead of every raw sample. Everything
not
provably equivalent falls back to the engine over the PR-1 querier; a
transpilable subtree under a non-transpilable node runs hybrid (subtree
materialized as synthetic series, engine on top). `TryExecuteRange`
slots
into the PR-2 serve/shadow paths (until now engine-only) through the new
`prometheus.RangeExecutor` capability interface — the provider stays
unexported and pkg/querier keeps holding `prometheus.Prometheus`;
providers
without the capability (v1) simply never transpile. The capability folds
into the main interface once v1 is removed.

Highlights (docs/contributing/prometheus.md carries the full correctness
story):

- Range functions map to verified grid aggregates; `increase` is
  `rate × range` exactly (same extrapolated delta, factor algebra).
- Instant selectors reproduce stale-marker shadowing with a
three-aggregate
  compare — skipping stale rows in WHERE would resurrect the sample the
  marker buried.
- `*_over_time` at range = k·step aggregates whole step buckets
  (`groupArrayInsertAt` + slide) — no per-window fan-out, no prefix-sum
  differencing.
- **Window-sliver filtering** (the headline perf commit, folded here):
when
the window is narrower than the step, only window/step of the timeline
can
influence any grid point; a lattice predicate in WHERE cuts the
aggregate's
  input by the coverage ratio — measured 74s/28GiB → 16s/4.3GiB on a
  36k-series 1w rate, and a 2.67B-sample case that exceeded 150GiB now
completes in 19s/17GiB. Over sliver-filtered rows the last-style gates
lift
(instant selectors and `last_over_time` transpile at window < step), and
  disjoint-window `*_over_time` forms drop the divisibility gate.
- Scalar-op pipelines apply in Go, slot by slot — same float64 ops, same
  order the AST dictates.

Two guards land with it:

- **Classification golden** (`classification_golden_test.go` +
  `testdata/classification_golden.json`): freezes the route
(full/hybrid(n)/fallback + reason) of every conformance-corpus
expression,
one line each — 317 expressions: 132 full, 39 hybrid, 146 fallback. The
test also requires each expression to route the same on every corpus
grid;
if a classifier change ever makes the route grid-dependent, the test
fails
  and the key must grow. Routing is its own
correctness surface — silently falling back costs the pushdown, silently
  transpiling an unproven shape risks wrong numbers; both now show up in
review as a golden diff, with the corpus suite's v2 leg judging the
numbers.
- **Workload coverage reporter** (`TestClassifyCorpus`, env-gated):
classifies
a JSON-lines corpus of real dashboard/alert queries and buckets
fallbacks
  by reason, to steer future allowlist work.

**What the dual-leg suite caught on its first transpiled run** (evidence
the
PR-2 guard works, worth stating in review):

- The classifier read a duration expression's offset (`x offset step()`)
as
  zero and transpiled it — offset expressions parse *without* the
experimental-parser flag, so they reach production. 20 corpus cases
served
  silently wrong numbers. Fixed by refusing `OriginalOffsetExpr` /
`RangeExpr` / `StepExpr` at classification (engine evaluates them
exactly);
  regression cases added, golden regenerated (30 routings flipped to
  fallback).
- Name-drop assembly treated temporally-disjoint same-labelset twins as
  separate series: `-{job="api"}` spanning `http_requests`/`http_errors`
  returned a 400 the engine would not raise, and hybrid
  `-metric_a or -metric_b` returned duplicate `{}` series. The engine's
  actual rule is: assemble the matrix by labelset, merging elements that
  never share an evaluation timestamp; error only on a same-timestamp
  conflict. Both the full-plan path (`mergeSameLabelsetSeries`) and the
hybrid post-strip path (`mergeMatrixByLabelset`) now reproduce it, with
  unit tests pinning the corpus scenarios.
- 12 remaining divergences, all one class, recorded in
`known_divergences_v2.json` with causes: the engine aggregates with
Kahan
compensated summation (sum, sum_over_time) and an overflow-free
incremental
mean (avg); ClickHouse's `sumForEach`/`avgForEach`/`arraySum` are naive,
so
±1e100 cancellation returns 0/residue and near-max-float64 `avg`
overflows
to ±Inf. Burn-down note: `sumKahanForEach` for the cancellation class;
the
overflow class needs an incremental-mean aggregate ClickHouse doesn't
have.

### Alternatives considered and discarded

- **General PromQL→SQL translation.** An allowlist inverts the failure
mode:
an overlooked construct becomes a fallback instead of a wrong number.
Every
shape on the list was validated slot-for-slot against the vendored
engine
  on live data before entering it.
- **ClickHouse's own PromQL dialect** (ClickHouse#57545,
`dialect='promql'`).
  Emits the same grid functions, but currently covers only
rate/irate/delta/idelta/last_over_time, has no fallback engine, and ties
us
to their TimeSeries table engine. We use the same primitives with our
own
  classifier and our own exactness gates.
- **Prefix-sum differencing for `*_over_time` windows.**
Large-minus-large
  cancellation drifts past the shadow tolerance on counter-sized values;
direct per-slot combination of at most W bucket partials adds the way
the
  engine adds.
- **Fanning each sample into every window that covers it.** Multiplies
rows
by W — billions of rows for a long range over a short step; the bucketed
  form's row count is series × buckets, the size of the output.
- **Handling staleness by filtering stale rows in WHERE (instant
units).**
Resurrects the older real sample the marker was written to bury; hence
the
  last-overall vs last-non-stale timestamp comparison.
- **Transpiling @-modifier and default-resolution subqueries.** Their
evaluation grid depends on server runtime settings the transpiler cannot
  see; they stay on the (exact) engine path.

### Test plan

- `go test ./pkg/prometheus/clickhouseprometheusv2` — transpiler unit
tests
  (SQL forms, classification, scalar ops, subquery grids), golden.
- `pytest integration/tests/promqlconformance/` — the v2 leg now
exercises
transpiled serving for every routable corpus case; ledger unchanged
(empty).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Pandey <vibhupandey28@gmail.com>
2026-08-07 10:07:32 +00:00
Pandey
bb3f5818c1 fix(sentry): stop reporting self-healed chunk load failures (#12440)
Some checks failed
build-staging / js-build (push) Has been cancelled
build-staging / prepare (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
cacheci / tests (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
A tab that outlives a deploy requests hashed assets the new build no
longer has. `lazyRetry` already recovers from this by reloading once, so
the resulting errors are noise — they spike on every deploy and each one
burns a Session Replay (`replaysOnErrorSampleRate: 1.0`).

### Sentry `ignoreErrors`

Filters the whole class. Four patterns because the same failure is
worded differently per source:

| Pattern | Source |
|---|---|
| `Unable to preload CSS for` | Vite's own thrown `Error`, identical
everywhere |
| `Failed to fetch dynamically imported module` | Chromium |
| `error loading dynamically imported module` | Firefox |
| `Importing a module script failed` | Safari |

`ignoreErrors` is applied as an event processor (`@sentry/core`
`eventFilters.js`), so the event is dropped before transport. Replay's
error flush hooks `afterSendEvent`, which never fires for a dropped
event — so this stops the replay burn too, not just the issue count.

Trade-off, stated plainly: stale-asset failures now produce no Sentry
signal at all, including the case where the reload doesn't fix it. A
genuinely broken deploy has to be caught from asset 404 rates rather
than from Sentry.

### `lazyRetry`

Behaviour is unchanged. One guard added: `setSessionStorageApi` returns
`false` when sessionStorage is blocked (iframe, storage disabled), and
the retry flag can't persist. The reload was previously issued anyway,
so every failed import reloaded forever with no way out. It now reloads
only when the flag was actually written.
2026-08-06 22:31:33 +00:00
202 changed files with 4891 additions and 10218 deletions

View File

@@ -53,21 +53,6 @@ jobs:
with:
PRIMUS_REF: main
GO_VERSION: 1.24
semconv-generated:
if: |
github.event_name == 'merge_group' ||
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
(github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe-to-test'))
runs-on: ubuntu-latest
steps:
- name: self-checkout
uses: actions/checkout@v4
- name: go-install
uses: actions/setup-go@v5
with:
go-version: "1.24"
- name: check-semconv-generated-files-and-product-literals
run: make semconv-check
build:
if: |
github.event_name == 'merge_group' ||

View File

@@ -220,25 +220,6 @@ py-test-teardown: ## Tear down the shared SigNoz backend
py-test: ## Runs integration tests
@cd tests && uv run pytest --basetemp=./tmp/ -vv --capture=no integration/tests/
.PHONY: py-test-semconv-phase1
py-test-semconv-phase1: py-test-setup ## Rebuild the shared stack and run the semantic-convention Phase 1 matrix
@cd tests && uv run pytest --basetemp=./tmp/ -vv --reuse --capture=no integration/tests/queriertraces/13_semconv_evolution.py
.PHONY: py-test-semconv-phase2
py-test-semconv-phase2: py-test-setup ## Rebuild the shared stack and run the Phase 1-2 cross-signal matrices
@cd tests && uv run pytest --basetemp=./tmp/ -vv --reuse --capture=no integration/tests/queriertraces/13_semconv_evolution.py integration/tests/queriersemconv/02_cross_signal.py
.PHONY: py-test-semconv-phase3
py-test-semconv-phase3: py-test-setup ## Rebuild the shared stack and run the Phase 1-3 compatibility and migration-report matrices
@cd tests && uv run pytest --basetemp=./tmp/ -vv --reuse --capture=no integration/tests/queriertraces/13_semconv_evolution.py integration/tests/queriersemconv/02_cross_signal.py
.PHONY: py-test-semconv-phase4
py-test-semconv-phase4: py-test-setup ## Rebuild the shared stack and run all semantic-convention compatibility matrices
@cd tests && uv run pytest --basetemp=./tmp/ -vv --reuse --capture=no integration/tests/queriertraces/13_semconv_evolution.py integration/tests/queriersemconv/02_cross_signal.py integration/tests/queriersemconv/04_phase4_families.py
.PHONY: py-test-semconv
py-test-semconv: py-test-semconv-phase4 ## Run the complete semantic-convention evolution closure gate
.PHONY: py-clean
py-clean: ## Clear all pycache and pytest cache from tests directory recursively
@echo ">> cleaning python cache files from tests directory"
@@ -252,14 +233,6 @@ py-clean: ## Clear all pycache and pytest cache from tests directory recursively
##############################################################
# generate commands
##############################################################
.PHONY: semconv-generate
semconv-generate: ## Regenerate semantic-convention families for Go and TypeScript
@go run ./scripts/semconv
.PHONY: semconv-check
semconv-check: ## Verify generated semantic-convention files and reject old-name product literals
@go run ./scripts/semconv -check -lint
.PHONY: gen-mocks
gen-mocks:
@echo ">> Generating mocks"

View File

@@ -6401,8 +6401,6 @@ components:
$ref: '#/components/schemas/TelemetrytypesFieldContext'
fieldDataType:
$ref: '#/components/schemas/TelemetrytypesFieldDataType'
fieldResolution:
$ref: '#/components/schemas/TelemetrytypesFieldResolution'
meta:
properties:
unit:
@@ -6447,10 +6445,6 @@ components:
rowsScanned:
minimum: 0
type: integer
semconvResolutions:
items:
$ref: '#/components/schemas/Querybuildertypesv5SemconvResolution'
type: array
stepIntervals:
additionalProperties:
minimum: 0
@@ -6517,8 +6511,6 @@ components:
$ref: '#/components/schemas/TelemetrytypesFieldContext'
fieldDataType:
$ref: '#/components/schemas/TelemetrytypesFieldDataType'
fieldResolution:
$ref: '#/components/schemas/TelemetrytypesFieldResolution'
name:
type: string
signal:
@@ -6590,8 +6582,6 @@ components:
$ref: '#/components/schemas/TelemetrytypesFieldContext'
fieldDataType:
$ref: '#/components/schemas/TelemetrytypesFieldDataType'
fieldResolution:
$ref: '#/components/schemas/TelemetrytypesFieldResolution'
name:
type: string
signal:
@@ -7159,20 +7149,6 @@ components:
stepInterval:
$ref: '#/components/schemas/Querybuildertypesv5Step'
type: object
Querybuildertypesv5SemconvResolution:
properties:
current:
type: string
kind:
type: string
members:
items:
type: string
nullable: true
type: array
requested:
type: string
type: object
Querybuildertypesv5Step:
description: Step interval. Accepts a Go duration string (e.g., "60s", "1m",
"1h") or a number representing seconds (e.g., 60).
@@ -8578,11 +8554,6 @@ components:
- number
- ""
type: string
TelemetrytypesFieldResolution:
enum:
- exact
- ""
type: string
TelemetrytypesGettableFieldKeys:
properties:
complete:
@@ -8608,42 +8579,6 @@ components:
- values
- complete
type: object
TelemetrytypesGettableSemconvMigrationReport:
properties:
endUnixMilli:
format: int64
type: integer
entries:
items:
$ref: '#/components/schemas/TelemetrytypesSemconvMigrationReportEntry'
nullable: true
type: array
startUnixMilli:
format: int64
type: integer
required:
- entries
type: object
TelemetrytypesSemconvMigrationReportEntry:
properties:
current:
type: string
lastSeenUnixMilli:
format: int64
type: integer
old:
type: string
resourceSets:
minimum: 0
type: integer
services:
items:
type: string
nullable: true
type: array
signal:
type: string
type: object
TelemetrytypesSignal:
enum:
- traces
@@ -8664,8 +8599,6 @@ components:
$ref: '#/components/schemas/TelemetrytypesFieldContext'
fieldDataType:
$ref: '#/components/schemas/TelemetrytypesFieldDataType'
fieldResolution:
$ref: '#/components/schemas/TelemetrytypesFieldResolution'
name:
type: string
signal:
@@ -11162,64 +11095,6 @@ paths:
summary: Get field keys
tags:
- fields
/api/v1/fields/semconv-migration:
get:
deprecated: false
description: Returns services that still emit old semantic-convention names
without the current family name
operationId: GetSemconvMigrationReport
parameters:
- in: query
name: startUnixMilli
schema:
format: int64
type: integer
- in: query
name: endUnixMilli
schema:
format: int64
type: integer
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/TelemetrytypesGettableSemconvMigrationReport'
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- VIEWER
- tokenizer:
- VIEWER
summary: Get semantic-convention migration report
tags:
- fields
/api/v1/fields/values:
get:
deprecated: false
@@ -18805,8 +18680,8 @@ paths:
alert: Payments-api error log rate above 1%
alertType: LOGS_BASED_ALERT
annotations:
description: Error log rate in {{$deployment.environment.name}}
is {{$value}}%
description: Error log rate in {{$deployment.environment}} is
{{$value}}%
summary: Payments-api error rate above {{$threshold}}%
condition:
compositeQuery:
@@ -18822,7 +18697,7 @@ paths:
groupBy:
- fieldContext: resource
fieldDataType: string
name: deployment.environment.name
name: deployment.environment
name: A
signal: logs
stepInterval: 60
@@ -18836,14 +18711,14 @@ paths:
groupBy:
- fieldContext: resource
fieldDataType: string
name: deployment.environment.name
name: deployment.environment
name: B
signal: logs
stepInterval: 60
type: builder_query
- spec:
expression: (A / B) * 100
legend: '{{deployment.environment.name}}'
legend: '{{deployment.environment}}'
name: F1
type: builder_formula
queryType: builder
@@ -18869,7 +18744,7 @@ paths:
team: payments
notificationSettings:
groupBy:
- deployment.environment.name
- deployment.environment
renotify:
alertStates:
- firing
@@ -18888,7 +18763,7 @@ paths:
alertType: LOGS_BASED_ALERT
annotations:
description: '{{$k8s.pod.name}} emitted {{$value}} panic log(s)
in {{$deployment.environment.name}}.'
in {{$deployment.environment}}.'
summary: Payments service panic
condition:
compositeQuery:
@@ -18906,8 +18781,8 @@ paths:
name: k8s.pod.name
- fieldContext: resource
fieldDataType: string
name: deployment.environment.name
legend: '{{k8s.pod.name}} ({{deployment.environment.name}})'
name: deployment.environment
legend: '{{k8s.pod.name}} ({{deployment.environment}})'
name: A
signal: logs
stepInterval: 60
@@ -18936,7 +18811,7 @@ paths:
notificationSettings:
groupBy:
- k8s.pod.name
- deployment.environment.name
- deployment.environment
renotify:
alertStates:
- firing
@@ -19006,9 +18881,8 @@ paths:
version: v5
metric_promql:
description: PromQL expression instead of the builder. Dotted OTEL
resource attributes are quoted ("deployment.environment.name").
Useful for queries that combine series with group_right or other
Prom operators.
resource attributes are quoted ("deployment.environment"). Useful
for queries that combine series with group_right or other Prom operators.
summary: Metric threshold PromQL rule
value:
alert: Kafka consumer group lag above 1000
@@ -19024,9 +18898,9 @@ paths:
- spec:
legend: '{{topic}}/{{partition}} ({{group}})'
name: A
query: (max by(topic, partition, "deployment.environment.name")(kafka_log_end_offset)
- on(topic, partition, "deployment.environment.name")
group_right max by(group, topic, partition, "deployment.environment.name")(kafka_consumer_committed_offset))
query: (max by(topic, partition, "deployment.environment")(kafka_log_end_offset)
- on(topic, partition, "deployment.environment") group_right
max by(group, topic, partition, "deployment.environment")(kafka_consumer_committed_offset))
> 0
type: promql
queryType: promql
@@ -19162,7 +19036,7 @@ paths:
alertType: METRIC_BASED_ALERT
annotations:
description: Pod {{$k8s.pod.name}} CPU is at {{$value}} of request
in {{$deployment.environment.name}}.
in {{$deployment.environment}}.
summary: Pod CPU above {{$threshold}} of request
condition:
compositeQuery:
@@ -19181,8 +19055,8 @@ paths:
name: k8s.pod.name
- fieldContext: resource
fieldDataType: string
name: deployment.environment.name
legend: '{{k8s.pod.name}} ({{deployment.environment.name}})'
name: deployment.environment
legend: '{{k8s.pod.name}} ({{deployment.environment}})'
name: A
signal: metrics
stepInterval: 60
@@ -19213,7 +19087,7 @@ paths:
notificationSettings:
groupBy:
- k8s.pod.name
- deployment.environment.name
- deployment.environment
renotify:
alertStates:
- firing
@@ -19237,7 +19111,7 @@ paths:
alert: API 5xx error rate above 1%
alertType: TRACES_BASED_ALERT
annotations:
description: '{{$service.name}} 5xx rate in {{$deployment.environment.name}}
description: '{{$service.name}} 5xx rate in {{$deployment.environment}}
is {{$value}}%.'
summary: API service error rate elevated
condition:
@@ -19249,7 +19123,7 @@ paths:
- expression: count()
disabled: true
filter:
expression: service.name CONTAINS 'api' AND http.response.status_code
expression: service.name CONTAINS 'api' AND http.status_code
>= 500
groupBy:
- fieldContext: resource
@@ -19257,7 +19131,7 @@ paths:
name: service.name
- fieldContext: resource
fieldDataType: string
name: deployment.environment.name
name: deployment.environment
name: A
signal: traces
stepInterval: 60
@@ -19274,14 +19148,14 @@ paths:
name: service.name
- fieldContext: resource
fieldDataType: string
name: deployment.environment.name
name: deployment.environment
name: B
signal: traces
stepInterval: 60
type: builder_query
- spec:
expression: (A / B) * 100
legend: '{{service.name}} ({{deployment.environment.name}})'
legend: '{{service.name}} ({{deployment.environment}})'
name: F1
type: builder_formula
queryType: builder
@@ -19309,7 +19183,7 @@ paths:
notificationSettings:
groupBy:
- service.name
- deployment.environment.name
- deployment.environment
newGroupEvalDelay: 2m
renotify:
alertStates:
@@ -19755,8 +19629,8 @@ paths:
alert: Payments-api error log rate above 1%
alertType: LOGS_BASED_ALERT
annotations:
description: Error log rate in {{$deployment.environment.name}}
is {{$value}}%
description: Error log rate in {{$deployment.environment}} is
{{$value}}%
summary: Payments-api error rate above {{$threshold}}%
condition:
compositeQuery:
@@ -19772,7 +19646,7 @@ paths:
groupBy:
- fieldContext: resource
fieldDataType: string
name: deployment.environment.name
name: deployment.environment
name: A
signal: logs
stepInterval: 60
@@ -19786,14 +19660,14 @@ paths:
groupBy:
- fieldContext: resource
fieldDataType: string
name: deployment.environment.name
name: deployment.environment
name: B
signal: logs
stepInterval: 60
type: builder_query
- spec:
expression: (A / B) * 100
legend: '{{deployment.environment.name}}'
legend: '{{deployment.environment}}'
name: F1
type: builder_formula
queryType: builder
@@ -19819,7 +19693,7 @@ paths:
team: payments
notificationSettings:
groupBy:
- deployment.environment.name
- deployment.environment
renotify:
alertStates:
- firing
@@ -19838,7 +19712,7 @@ paths:
alertType: LOGS_BASED_ALERT
annotations:
description: '{{$k8s.pod.name}} emitted {{$value}} panic log(s)
in {{$deployment.environment.name}}.'
in {{$deployment.environment}}.'
summary: Payments service panic
condition:
compositeQuery:
@@ -19856,8 +19730,8 @@ paths:
name: k8s.pod.name
- fieldContext: resource
fieldDataType: string
name: deployment.environment.name
legend: '{{k8s.pod.name}} ({{deployment.environment.name}})'
name: deployment.environment
legend: '{{k8s.pod.name}} ({{deployment.environment}})'
name: A
signal: logs
stepInterval: 60
@@ -19886,7 +19760,7 @@ paths:
notificationSettings:
groupBy:
- k8s.pod.name
- deployment.environment.name
- deployment.environment
renotify:
alertStates:
- firing
@@ -19956,9 +19830,8 @@ paths:
version: v5
metric_promql:
description: PromQL expression instead of the builder. Dotted OTEL
resource attributes are quoted ("deployment.environment.name").
Useful for queries that combine series with group_right or other
Prom operators.
resource attributes are quoted ("deployment.environment"). Useful
for queries that combine series with group_right or other Prom operators.
summary: Metric threshold PromQL rule
value:
alert: Kafka consumer group lag above 1000
@@ -19974,9 +19847,9 @@ paths:
- spec:
legend: '{{topic}}/{{partition}} ({{group}})'
name: A
query: (max by(topic, partition, "deployment.environment.name")(kafka_log_end_offset)
- on(topic, partition, "deployment.environment.name")
group_right max by(group, topic, partition, "deployment.environment.name")(kafka_consumer_committed_offset))
query: (max by(topic, partition, "deployment.environment")(kafka_log_end_offset)
- on(topic, partition, "deployment.environment") group_right
max by(group, topic, partition, "deployment.environment")(kafka_consumer_committed_offset))
> 0
type: promql
queryType: promql
@@ -20112,7 +19985,7 @@ paths:
alertType: METRIC_BASED_ALERT
annotations:
description: Pod {{$k8s.pod.name}} CPU is at {{$value}} of request
in {{$deployment.environment.name}}.
in {{$deployment.environment}}.
summary: Pod CPU above {{$threshold}} of request
condition:
compositeQuery:
@@ -20131,8 +20004,8 @@ paths:
name: k8s.pod.name
- fieldContext: resource
fieldDataType: string
name: deployment.environment.name
legend: '{{k8s.pod.name}} ({{deployment.environment.name}})'
name: deployment.environment
legend: '{{k8s.pod.name}} ({{deployment.environment}})'
name: A
signal: metrics
stepInterval: 60
@@ -20163,7 +20036,7 @@ paths:
notificationSettings:
groupBy:
- k8s.pod.name
- deployment.environment.name
- deployment.environment
renotify:
alertStates:
- firing
@@ -20187,7 +20060,7 @@ paths:
alert: API 5xx error rate above 1%
alertType: TRACES_BASED_ALERT
annotations:
description: '{{$service.name}} 5xx rate in {{$deployment.environment.name}}
description: '{{$service.name}} 5xx rate in {{$deployment.environment}}
is {{$value}}%.'
summary: API service error rate elevated
condition:
@@ -20199,7 +20072,7 @@ paths:
- expression: count()
disabled: true
filter:
expression: service.name CONTAINS 'api' AND http.response.status_code
expression: service.name CONTAINS 'api' AND http.status_code
>= 500
groupBy:
- fieldContext: resource
@@ -20207,7 +20080,7 @@ paths:
name: service.name
- fieldContext: resource
fieldDataType: string
name: deployment.environment.name
name: deployment.environment
name: A
signal: traces
stepInterval: 60
@@ -20224,14 +20097,14 @@ paths:
name: service.name
- fieldContext: resource
fieldDataType: string
name: deployment.environment.name
name: deployment.environment
name: B
signal: traces
stepInterval: 60
type: builder_query
- spec:
expression: (A / B) * 100
legend: '{{service.name}} ({{deployment.environment.name}})'
legend: '{{service.name}} ({{deployment.environment}})'
name: F1
type: builder_formula
queryType: builder
@@ -20259,7 +20132,7 @@ paths:
notificationSettings:
groupBy:
- service.name
- deployment.environment.name
- deployment.environment
newGroupEvalDelay: 2m
renotify:
alertStates:
@@ -20608,8 +20481,8 @@ paths:
alert: Payments-api error log rate above 1%
alertType: LOGS_BASED_ALERT
annotations:
description: Error log rate in {{$deployment.environment.name}}
is {{$value}}%
description: Error log rate in {{$deployment.environment}} is
{{$value}}%
summary: Payments-api error rate above {{$threshold}}%
condition:
compositeQuery:
@@ -20625,7 +20498,7 @@ paths:
groupBy:
- fieldContext: resource
fieldDataType: string
name: deployment.environment.name
name: deployment.environment
name: A
signal: logs
stepInterval: 60
@@ -20639,14 +20512,14 @@ paths:
groupBy:
- fieldContext: resource
fieldDataType: string
name: deployment.environment.name
name: deployment.environment
name: B
signal: logs
stepInterval: 60
type: builder_query
- spec:
expression: (A / B) * 100
legend: '{{deployment.environment.name}}'
legend: '{{deployment.environment}}'
name: F1
type: builder_formula
queryType: builder
@@ -20672,7 +20545,7 @@ paths:
team: payments
notificationSettings:
groupBy:
- deployment.environment.name
- deployment.environment
renotify:
alertStates:
- firing
@@ -20691,7 +20564,7 @@ paths:
alertType: LOGS_BASED_ALERT
annotations:
description: '{{$k8s.pod.name}} emitted {{$value}} panic log(s)
in {{$deployment.environment.name}}.'
in {{$deployment.environment}}.'
summary: Payments service panic
condition:
compositeQuery:
@@ -20709,8 +20582,8 @@ paths:
name: k8s.pod.name
- fieldContext: resource
fieldDataType: string
name: deployment.environment.name
legend: '{{k8s.pod.name}} ({{deployment.environment.name}})'
name: deployment.environment
legend: '{{k8s.pod.name}} ({{deployment.environment}})'
name: A
signal: logs
stepInterval: 60
@@ -20739,7 +20612,7 @@ paths:
notificationSettings:
groupBy:
- k8s.pod.name
- deployment.environment.name
- deployment.environment
renotify:
alertStates:
- firing
@@ -20809,9 +20682,8 @@ paths:
version: v5
metric_promql:
description: PromQL expression instead of the builder. Dotted OTEL
resource attributes are quoted ("deployment.environment.name").
Useful for queries that combine series with group_right or other
Prom operators.
resource attributes are quoted ("deployment.environment"). Useful
for queries that combine series with group_right or other Prom operators.
summary: Metric threshold PromQL rule
value:
alert: Kafka consumer group lag above 1000
@@ -20827,9 +20699,9 @@ paths:
- spec:
legend: '{{topic}}/{{partition}} ({{group}})'
name: A
query: (max by(topic, partition, "deployment.environment.name")(kafka_log_end_offset)
- on(topic, partition, "deployment.environment.name")
group_right max by(group, topic, partition, "deployment.environment.name")(kafka_consumer_committed_offset))
query: (max by(topic, partition, "deployment.environment")(kafka_log_end_offset)
- on(topic, partition, "deployment.environment") group_right
max by(group, topic, partition, "deployment.environment")(kafka_consumer_committed_offset))
> 0
type: promql
queryType: promql
@@ -20965,7 +20837,7 @@ paths:
alertType: METRIC_BASED_ALERT
annotations:
description: Pod {{$k8s.pod.name}} CPU is at {{$value}} of request
in {{$deployment.environment.name}}.
in {{$deployment.environment}}.
summary: Pod CPU above {{$threshold}} of request
condition:
compositeQuery:
@@ -20984,8 +20856,8 @@ paths:
name: k8s.pod.name
- fieldContext: resource
fieldDataType: string
name: deployment.environment.name
legend: '{{k8s.pod.name}} ({{deployment.environment.name}})'
name: deployment.environment
legend: '{{k8s.pod.name}} ({{deployment.environment}})'
name: A
signal: metrics
stepInterval: 60
@@ -21016,7 +20888,7 @@ paths:
notificationSettings:
groupBy:
- k8s.pod.name
- deployment.environment.name
- deployment.environment
renotify:
alertStates:
- firing
@@ -21040,7 +20912,7 @@ paths:
alert: API 5xx error rate above 1%
alertType: TRACES_BASED_ALERT
annotations:
description: '{{$service.name}} 5xx rate in {{$deployment.environment.name}}
description: '{{$service.name}} 5xx rate in {{$deployment.environment}}
is {{$value}}%.'
summary: API service error rate elevated
condition:
@@ -21052,7 +20924,7 @@ paths:
- expression: count()
disabled: true
filter:
expression: service.name CONTAINS 'api' AND http.response.status_code
expression: service.name CONTAINS 'api' AND http.status_code
>= 500
groupBy:
- fieldContext: resource
@@ -21060,7 +20932,7 @@ paths:
name: service.name
- fieldContext: resource
fieldDataType: string
name: deployment.environment.name
name: deployment.environment
name: A
signal: traces
stepInterval: 60
@@ -21077,14 +20949,14 @@ paths:
name: service.name
- fieldContext: resource
fieldDataType: string
name: deployment.environment.name
name: deployment.environment
name: B
signal: traces
stepInterval: 60
type: builder_query
- spec:
expression: (A / B) * 100
legend: '{{service.name}} ({{deployment.environment.name}})'
legend: '{{service.name}} ({{deployment.environment}})'
name: F1
type: builder_formula
queryType: builder
@@ -21112,7 +20984,7 @@ paths:
notificationSettings:
groupBy:
- service.name
- deployment.environment.name
- deployment.environment
newGroupEvalDelay: 2m
renotify:
alertStates:
@@ -21964,8 +21836,8 @@ paths:
alert: Payments-api error log rate above 1%
alertType: LOGS_BASED_ALERT
annotations:
description: Error log rate in {{$deployment.environment.name}}
is {{$value}}%
description: Error log rate in {{$deployment.environment}} is
{{$value}}%
summary: Payments-api error rate above {{$threshold}}%
condition:
compositeQuery:
@@ -21981,7 +21853,7 @@ paths:
groupBy:
- fieldContext: resource
fieldDataType: string
name: deployment.environment.name
name: deployment.environment
name: A
signal: logs
stepInterval: 60
@@ -21995,14 +21867,14 @@ paths:
groupBy:
- fieldContext: resource
fieldDataType: string
name: deployment.environment.name
name: deployment.environment
name: B
signal: logs
stepInterval: 60
type: builder_query
- spec:
expression: (A / B) * 100
legend: '{{deployment.environment.name}}'
legend: '{{deployment.environment}}'
name: F1
type: builder_formula
queryType: builder
@@ -22028,7 +21900,7 @@ paths:
team: payments
notificationSettings:
groupBy:
- deployment.environment.name
- deployment.environment
renotify:
alertStates:
- firing
@@ -22047,7 +21919,7 @@ paths:
alertType: LOGS_BASED_ALERT
annotations:
description: '{{$k8s.pod.name}} emitted {{$value}} panic log(s)
in {{$deployment.environment.name}}.'
in {{$deployment.environment}}.'
summary: Payments service panic
condition:
compositeQuery:
@@ -22065,8 +21937,8 @@ paths:
name: k8s.pod.name
- fieldContext: resource
fieldDataType: string
name: deployment.environment.name
legend: '{{k8s.pod.name}} ({{deployment.environment.name}})'
name: deployment.environment
legend: '{{k8s.pod.name}} ({{deployment.environment}})'
name: A
signal: logs
stepInterval: 60
@@ -22095,7 +21967,7 @@ paths:
notificationSettings:
groupBy:
- k8s.pod.name
- deployment.environment.name
- deployment.environment
renotify:
alertStates:
- firing
@@ -22165,9 +22037,8 @@ paths:
version: v5
metric_promql:
description: PromQL expression instead of the builder. Dotted OTEL
resource attributes are quoted ("deployment.environment.name").
Useful for queries that combine series with group_right or other
Prom operators.
resource attributes are quoted ("deployment.environment"). Useful
for queries that combine series with group_right or other Prom operators.
summary: Metric threshold PromQL rule
value:
alert: Kafka consumer group lag above 1000
@@ -22183,9 +22054,9 @@ paths:
- spec:
legend: '{{topic}}/{{partition}} ({{group}})'
name: A
query: (max by(topic, partition, "deployment.environment.name")(kafka_log_end_offset)
- on(topic, partition, "deployment.environment.name")
group_right max by(group, topic, partition, "deployment.environment.name")(kafka_consumer_committed_offset))
query: (max by(topic, partition, "deployment.environment")(kafka_log_end_offset)
- on(topic, partition, "deployment.environment") group_right
max by(group, topic, partition, "deployment.environment")(kafka_consumer_committed_offset))
> 0
type: promql
queryType: promql
@@ -22321,7 +22192,7 @@ paths:
alertType: METRIC_BASED_ALERT
annotations:
description: Pod {{$k8s.pod.name}} CPU is at {{$value}} of request
in {{$deployment.environment.name}}.
in {{$deployment.environment}}.
summary: Pod CPU above {{$threshold}} of request
condition:
compositeQuery:
@@ -22340,8 +22211,8 @@ paths:
name: k8s.pod.name
- fieldContext: resource
fieldDataType: string
name: deployment.environment.name
legend: '{{k8s.pod.name}} ({{deployment.environment.name}})'
name: deployment.environment
legend: '{{k8s.pod.name}} ({{deployment.environment}})'
name: A
signal: metrics
stepInterval: 60
@@ -22372,7 +22243,7 @@ paths:
notificationSettings:
groupBy:
- k8s.pod.name
- deployment.environment.name
- deployment.environment
renotify:
alertStates:
- firing
@@ -22396,7 +22267,7 @@ paths:
alert: API 5xx error rate above 1%
alertType: TRACES_BASED_ALERT
annotations:
description: '{{$service.name}} 5xx rate in {{$deployment.environment.name}}
description: '{{$service.name}} 5xx rate in {{$deployment.environment}}
is {{$value}}%.'
summary: API service error rate elevated
condition:
@@ -22408,7 +22279,7 @@ paths:
- expression: count()
disabled: true
filter:
expression: service.name CONTAINS 'api' AND http.response.status_code
expression: service.name CONTAINS 'api' AND http.status_code
>= 500
groupBy:
- fieldContext: resource
@@ -22416,7 +22287,7 @@ paths:
name: service.name
- fieldContext: resource
fieldDataType: string
name: deployment.environment.name
name: deployment.environment
name: A
signal: traces
stepInterval: 60
@@ -22433,14 +22304,14 @@ paths:
name: service.name
- fieldContext: resource
fieldDataType: string
name: deployment.environment.name
name: deployment.environment
name: B
signal: traces
stepInterval: 60
type: builder_query
- spec:
expression: (A / B) * 100
legend: '{{service.name}} ({{deployment.environment.name}})'
legend: '{{service.name}} ({{deployment.environment}})'
name: F1
type: builder_formula
queryType: builder
@@ -22468,7 +22339,7 @@ paths:
notificationSettings:
groupBy:
- service.name
- deployment.environment.name
- deployment.environment
newGroupEvalDelay: 2m
renotify:
alertStates:

View File

@@ -1,123 +1,377 @@
# PromQL Serving — clickhouseprometheusv2
This document is the subsystem context for `pkg/prometheus/clickhouseprometheusv2`,
the second-generation ClickHouse-backed Prometheus provider. It explains why the
package exists, the correctness constraints that shaped it, and how each fetch
reduction is proven not to change results. Any change to the provider must keep
these invariants; if a change would violate one, it must be flagged and
discussed.
This document gives the context for `pkg/prometheus/clickhouseprometheusv2`.
This package is the second-generation ClickHouse-backed Prometheus provider.
The document tells you why the package exists. It tells you the correctness
rules that shaped it. It shows how we prove that each construct does not
change results. Keep these invariants when you change the provider. If your
change breaks an invariant, flag it and discuss it first.
---
## Why a second provider
The v1 provider (`pkg/prometheus/clickhouseprometheus`) serves the promql engine
through the remote-read protobuf adapter: every raw sample of a query's union
window is fetched, serialized, and handed to the engine. The cost is a function
of ingested data, not of the question asked — which is how a dashboard of PromQL
panels can take an instance down.
The v1 provider (`pkg/prometheus/clickhouseprometheus`) serves the promql
engine through the remote-read protobuf adapter. It fetches every raw sample
of a query's union window. It serializes all of them and gives them to the
engine. The cost follows the ingested data, not the question. This is how a
dashboard of PromQL panels can take an instance down.
In v2 the stock promql engine evaluates over a native `storage.Querier`: no
translation layer, per-selector fetch windows, and fetch reductions that are
provably invisible to the engine.
In v2, each query runs in one of two ways. The classifier decides per query:
**The core constraint: every reduction either preserves engine semantics exactly
or is not performed.** A PromQL result that differs from upstream Prometheus is
a lost user. The conformance suite
- **Transpiled**: ClickHouse evaluates the query. Only final (or near-final)
per-group grid arrays come back. The statements use the
`timeSeries*ToGrid` aggregate functions. The supported ClickHouse floor is
25.6 or later, so these functions are assumed available.
- **Engine**: the stock promql engine evaluates over this package's native
`storage.Querier`. Every shape that does not transpile takes this path.
**The core rule: a PromQL result that differs from upstream Prometheus is a
lost user. A construct that cannot reproduce engine semantics exactly falls
back. It does not approximate.** The conformance suite
(`tests/integration/tests/promqlconformance/`) replays Prometheus' own test
corpus against both providers and is the arbiter.
corpus against both providers. It is the arbiter. The classification golden
(`testdata/classification_golden.json`) freezes the route of each corpus
expression. The rest of this document is the PromQL-to-SQL story. That
mapping is where correctness is won or lost.
---
## The evaluation model the SQL must reproduce
A PromQL range query is an instant query evaluated at each grid point
`t_i = start + i*step`, for `i = 0..(end-start)/step`. At each `t_i`:
- An instant selector resolves to the latest sample in the left-open
lookback window `(t_i - lookback, t_i]`. If that latest sample is a stale
marker, the selector resolves to nothing. Older real samples in the window
do not change this.
- A range selector `[r]` collects every sample in `(t_i - r, t_i]`. Stale
markers are excluded.
- `offset d` shifts both windows to `(t_i - d - w, t_i - d]`.
The transpilation invariant follows from this model. Each transpiled
construct produces one array per output series. The array has exactly one
slot per grid point. Slot `i` holds the value at `t_i`. NULL means absent.
This makes composition correct, not only convenient. The engine evaluates
these operators independently per `t_i`. A representation that gets every
slot right gets the whole query right. Spatial aggregation over arrays is
sound because it combines values that belong to the same `t_i` by
construction. Scan time maps slot `i` back to `t_i = start + i*step`
(`toMatrix`). The sections below fill those slots with exactly the numbers
the engine computes. We validated each equivalence against the vendored
engine on live data before its shape entered the allowlist. An unproven
shape stays on the engine path.
## Classification: finding what a statement can answer
`classify` walks the parsed AST and looks for "core units". A core unit is a
maximal subtree of this shape:
[agg by/without (...)] [fn(] selector[range] [offset d] [)] [op scalar]...
`classifyCore` peels that chain from the outside in. It takes an optional
sum/min/max/avg/count aggregation. It then takes one allowlisted function or
a bare instant selector. It then takes the selector with its offset. On the
way out, it collects number-literal arithmetic, comparisons (including
`bool`), and unary minus into a scalar-op pipeline. A node qualifies only if
its type, arguments, and children are in the proven set. This is an
allowlist. An overlooked construct becomes a fallback, not a wrong number.
Three unit kinds come out. Each kind has its own SQL form:
- `unitRange`: rate, irate, increase, delta, idelta over a range selector.
- `unitInstant`: instant vector selection, bare or comparison-filtered.
- `unitOverTime`: avg/min/max/sum/count/last `_over_time`.
If the whole tree is one unit, the plan is "full". The statement's rows are
the query result. Otherwise, `rewrite` cuts out each maximal unit and puts a
synthetic selector `__signoz_transpiled_N__` in its place. The engine then
runs the rewritten expression over the units' materialized results. This is
a "hybrid" plan. `histogram_quantile`, `topk`, `or`/`and`/`unless`, and
vector matching keep exact engine semantics. Their expensive inputs were
aggregated server-side.
Classification refuses a shape when it cannot guarantee exact semantics
server-side:
- The `@` modifier, anywhere.
- Default-resolution subqueries. Their resolution is a server runtime
setting that the transpiler cannot see.
- Duration expressions (`offset step()`, `[range()]`, ...), anywhere. The
engine resolves them into the selector's static fields only at evaluation
time. At classification time those fields hold zero values. A transpile
would silently use the wrong offset or range.
- Steps or ranges that are not whole seconds. The grid functions take
whole-second parameters.
- Grouping by `__name__`, or matching on it, in hybrid plans. The synthetic
name would leak into results.
- Name-keeping units in hybrid plans. Bare and comparison-filtered instant
selectors and `last_over_time` keep their real `__name__` (`keepsName`).
Substitution would replace that name. These units transpile only as full
plans.
- Every function outside the allowlist: changes, resets,
quantile_over_time, absent, native-histogram functions, and more.
Units inside a fixed-resolution subquery evaluate on the subquery's own
grid, not the query grid. That grid is the set of epoch-aligned multiples of
the resolution strictly after `outerStart - offset - range`, ending at
`outer end - offset`. This is the exact derivation the engine uses. A grid
shifted by one step changes which samples every window sees.
## From one unit to one statement
`buildUnitSQL` renders each unit as one statement. For
`sum by (pod) (rate(m{job="api"}[5m]))` the skeleton is:
SELECT g0, sumForEach(grid) AS grid FROM (
SELECT any(series.g0) AS g0,
timeSeriesRateToGrid(<start>, <end>, <step>, <range>)(fromUnixTimestamp64Milli(unix_milli), value) AS grid
FROM signoz_metrics.distributed_samples_v4 AS points
INNER JOIN (
SELECT fingerprint, JSONExtractString(labels, 'pod') AS g0
FROM signoz_metrics.time_series_v4
WHERE <series predicates>
GROUP BY fingerprint, g0
) AS series ON points.fingerprint = series.fingerprint
WHERE metric_name = ? AND temporality IN ['Cumulative', 'Unspecified']
AND unix_milli > <start - range> AND unix_milli <= <end>
AND bitAnd(flags, 1) = 0
GROUP BY points.fingerprint
) GROUP BY g0
SETTINGS allow_experimental_ts_to_grid_aggregate_function = 1
Read it from the inside out.
**The time window** is the selector's semantics, verbatim. Strict `>` on the
lower bound and `<=` on the upper bound is the left-open `(t - w, t]` rule.
The offset shifts the whole window. `bitAnd(flags, 1) = 0` drops stale
markers. PromQL excludes them from range vectors.
**The inner GROUP BY** computes one grid array per series.
`timeSeriesRateToGrid(start, end, step, range)` is a parametric aggregate.
It takes (timestamp, value) pairs and produces `Array(Nullable(Float64))`
with one slot per grid point. It is correct because it implements the
engine's `extrapolatedRate`, decision for decision: counter resets, the
zero-point clamp, the extrapolation thresholds, the two-samples rule, and
the left-open window. We verified this: we fed identical samples to both and
compared slot for slot. The only observed difference is the last bit.
ClickHouse's C++ and Go round the same formula differently. That is the
floating-point floor, not a semantic gap. irate/delta/idelta map to their
own `timeSeries*ToGrid` functions, with the same verification. `increase`
has no function of its own. We emit
`arrayMap(x -> x * <range seconds>, <rate expr>)`. This is exact by
definition: `extrapolatedRate` computes the same extrapolated delta for both
and divides by the range only when `isRate`. The multiplication reverses it
exactly. The grid parameters render as literals, not bound args. They are
aggregate-function parameters. The experimental gate rides as a SETTINGS
clause on the statement itself, so telemetrystore hooks cannot remove it.
The group key is functionally dependent on the fingerprint: one fingerprint
is the hash of one labelset. So the inner query groups by the fingerprint
alone and reads the key columns with `any()`. This is exact, and it makes
the per-row hash key smaller.
**The join** gives each series its group key, in one of two forms.
`by (...)` extracts each listed label as a plain column
(`JSONExtractString(labels, 'pod') AS g0`) and groups on the columns. The
projection is a known short list, and the label names live in Go. To build,
sort, and stringify every label pair per row would be waste. This is correct
because column-tuple equality is label-set equality on the projection. An
extracted `''` means the label is absent. That is Prometheus semantics for
`by()` over missing labels. The empties are skipped when the columns turn
back into labels. `without` and no-aggregation project a label set that
varies per series. They get the canonical key: `toJSONString` of the sorted
[label, value] pairs that the unit projects. `without` excludes the listed
labels plus `__name__`. No-aggregation keeps everything; the name comes off
in Go, per the engine's name-dropping rules. Here the sort is load-bearing.
Stored JSON key order is not canonical across fingerprints. Two orderings of
the same labels must land in one group. Empty values are filtered for the
same absent-label reason. The same string parses back into the output label
set (`labelsFromGroupKey`).
**The outer GROUP BY** is the spatial aggregation. sum/min/max/avg/count
by/without become the `-ForEach` combinators. Element-wise aggregation over
grid arrays is the engine's per-`t_i` aggregation: slot `i` of every input
array refers to the same `t_i`. The combinators skip NULLs. That is the
engine aggregating only the series present at `t_i`. An index where every
series is absent stays NULL. Two edges need explicit handling. First,
`countForEach` wraps in a map of 0 back to NULL. A count over an all-absent
index is an absent point, not 0. Second, a unit without aggregation still
passes through `maxForEach`. That is the identity for the common
one-fingerprint group. It is a deterministic NULL-skipping merge when a
regex `__name__` selector collapses distinct metrics onto one projected
label set. One caveat is inherent: the summation order over series differs
from the engine's. Spatial aggregates can differ in the last ULP. Float
addition is not associative. No ordering reproduces the engine's result
bit-exactly from inside a GROUP BY.
## Instant selectors: staleness needs two aggregates
`unitInstant` uses window = lookback. It must reproduce the shadowing rule:
the point is absent when the latest in-window sample is a stale marker.
`timeSeriesLastToGrid` alone cannot express that. To skip stale rows in
WHERE would resurrect the older real sample that the marker buried. So stale
rows stay in the scan for this kind only. The grid expression compares three
aggregates per slot:
arrayMap((tall, tok, vok) -> if(tall IS NULL OR tok IS NULL OR tall != tok, NULL, vok),
timeSeriesLastToGrid(...)(ts, toFloat64(unix_milli)), -- last sample overall
timeSeriesLastToGridIf(...)(ts, toFloat64(unix_milli), bitAnd(flags, 1) = 0), -- last non-stale, its timestamp
timeSeriesLastToGridIf(...)(ts, value, bitAnd(flags, 1) = 0)) -- last non-stale, its value
This is correct by cases on a slot's window. No samples at all: both
timestamp aggregates are NULL, so the slot is NULL. That is absent, as the
engine says. Latest sample non-stale: it is the latest overall and the
latest non-stale. The timestamps agree. The slot takes its value. That is
the engine's pick. Latest sample stale: the last-overall timestamp is the
marker's. The last-non-stale timestamp is older, or NULL when the window
holds only markers. They disagree. The slot is NULL. The marker shadows,
exactly as the engine's rule says. Timestamps are unique per series (ingest
dedups). So timestamp equality identifies "the same sample" without
ambiguity. We probed the `-If` combinator against these experimental
aggregates before we trusted it.
## Windowed *_over_time: whole buckets instead of a grid function
avg/min/max/sum/count `_over_time` aggregate every raw sample in the window.
No `timeSeries*ToGrid` function computes them. (`last_over_time` is the
exception. The last sample of a range vector is exactly
`timeSeriesLastToGrid`. PromQL excludes stale markers from range vectors; we
exclude them in WHERE.) These shapes transpile only when the range is a
whole multiple of the step. Then the window needs no per-sample fan-out.
With `W = range/step`, the window `(t_k - range, t_k]` is exactly the union
of W step buckets. Both are left-open on the same boundaries. So bucket
membership fully determines window membership. Each sample lands in exactly
one bucket:
intDiv(unix_milli - <start> + <range> - 1, <step>)
This is `ceil((ts - start)/step)` shifted by W-1, so the earliest in-window
sample sits at 0. Slot k's window is buckets in `[k, k+W-1]`. The
alternative fans each sample into all W windows that cover it. That
multiplies rows by W. For a long range over a short step, that is a row
explosion measured in billions. The bucketed form's row count is
series × buckets: the size of the output, for any W.
Each series aggregates in one group. The `-Resample` combinator
(`sumResample`, `countResample`) holds the dense per-bucket partials inside
one group state: a bucket count, plus the function's value aggregate (sum
for sum/avg, min, max). An earlier form grouped by (series, bucket) and
assembled with `groupArrayInsertAt`. At scale that made 37M hash groups, and
per-thread partials scaled memory with the thread count. The slide then
combines each slot's at-most-W bucket partials by direct aggregation
(`arraySum(arraySlice(...))`). Window sums are added the way the engine adds
them. There is no prefix-sum differencing: its large-minus-large
cancellation would drift past the shadow tolerance on counter-sized values.
This is correct per slot because the bucket union is the exact window
multiset, and avg/min/max/sum/count are order-insensitive on a multiset
(sum/avg up to summation order; see the float caveat above). A slot with
zero window count is absent. min/max filter their slices on the bucket
counts. An empty bucket's default can never look like a value: a real sample
can legitimately be +Inf.
Two shapes fall back to the engine path, which is exact: a range that does
not divide the step, and a window wider than `maxWindowBuckets` buckets (the
slide costs W combines per slot). A range narrower than the step needs
neither gate: the windows are pairwise disjoint, one bucket per slot, no
slide. That form is exact only together with the window-sliver predicate
below.
## Scalar ops, full plans, hybrid plans
The scalar-op pipeline runs in Go on the returned arrays
(`applyScalarOps`), slot by slot. Arithmetic operators compute. Comparisons
filter: the slot keeps the vector-side value or becomes NULL. Under `bool`
they return 0/1. This is trivially correct. It is the same float64 operation
the engine applies, to the same slot value, in the same operator order the
AST dictates. Go instead of another SQL layer changes where, not what.
A full plan's arrays map straight to the result matrix. A hybrid plan
materializes each unit's arrays as synthetic series under its
`__signoz_transpiled_N__` name. The engine evaluates the rewritten
expression over a storage that serves synthetic names from memory and
everything else live. Substitution is sound because a unit's output is a
plain instant vector to the engine: same values at same timestamps, under a
different name. The name cannot matter. Plans that group by or match on
`__name__` were refused at classification. Name-keeping units are never
substituted. One subtlety makes it exact: we write stale markers at absent
grid points. Without them, the engine's lookback would resurrect a point
from up to `lookback` earlier. The marker encodes "absent here" the way the
engine itself encodes it. Units evaluate concurrently. Each unit is one
series lookup plus one grid statement. A step of 0 is an instant query: a
single evaluation at `end`.
A note on the window sliver: when the window is narrower than the step, the
grid windows cover only `window/step` of the timeline. A sample in a gap
belongs to no window. It cannot move any grid point, but the grid aggregate
would buffer it. A WHERE predicate keeps only the in-window rows:
`positiveModulo(selStart - unix_milli, step) < window`, with the scan capped
at the last grid point. The lattice anchors at the selector start, because
the end can sit off-lattice on unaligned grids. This cut a 36k-series
one-week rate from 74s/28GiB to 16s/4.3GiB on fleet data. Over slivered
rows, `timeSeriesLastToGrid`'s window widening is harmless, so instant
selectors and `last_over_time` transpile at window < step too.
## Series lookup
Matchers resolve to series once per selector (`selectSeries`) against the series
tables, which hold one row per (fingerprint, bucket) at 1h/6h/1d/1w
granularities. Table selection and window rounding delegate to the shared
metrics schema package (`pkg/telemetryschema/metricstelemetryschema`); the
window start rounds down to the bucket boundary so a window beginning mid-bucket
still matches the bucket's row.
Both paths resolve matchers the same way, once per selector
(`selectSeries`). The series tables hold one row per (fingerprint, bucket)
at 1h/6h/1d/1w granularities. The shared schema package
(`pkg/telemetryschema/metricstelemetryschema`) picks the table whose bucket
fits the window. It rounds the window start down to the bucket boundary, so
a window that begins mid-bucket still matches the bucket's row. How matchers
become SQL, and why regexes are anchored, is documented at
`applySeriesConditions`. Empty-valued labels come off at this boundary. An
empty value means "label absent" in Prometheus, but stored attribute JSON
can carry them.
How matchers become SQL is documented at `applySeriesConditions`. The rules that
carry semantics:
## The engine path
- `__name__` matchers (all four types) translate to the `metric_name` column.
- Every other matcher becomes a `JSONExtractString` condition on the labels
column. An equality matcher against `""` matches series *without* the label,
mirroring PromQL, because `JSONExtractString` returns `""` for missing keys.
- Regexes are anchored (`^(?:...)$`) before they reach `match()`: PromQL
matchers match the whole value, ClickHouse `match()` searches for a
substring.
- The series-lookup upper bound is inclusive (`unix_milli <= end`) because the
exporter floors registration rows to the bucket start: a series first
registered in the bucket beginning exactly at `end` would otherwise be
invisible while its samples are in range.
Empty-valued labels come off at this boundary: an empty value means "label
absent" in Prometheus, but stored attribute JSON can carry them.
---
## Sample fetch
Samples are fetched per selector using the engine's per-selector hints, not the
query-wide union window — `foo / foo offset 1d` reads two narrow windows
instead of the widest one twice.
**Last-sample-per-step reduction.** Instant selectors of subquery-free queries
fetch only the last sample per step bucket. The engine resolves an instant
selector at each grid timestamp `t` to the latest sample in the left-open
lookback window `(t lookback, t]`. Buckets anchor at the selector's first
evaluation timestamp — recovered from the hints as
`hints.Start + lookback 1ms`, the inverse of how the engine derives
`hints.Start` — so bucket boundaries coincide with evaluation timestamps, and a
non-final sample of a bucket can never be the latest sample in
`(t lookback, t]` for any grid `t`. Real timestamps are preserved, so the
engine's own lookback and staleness handling stay exact.
Range selectors always fetch raw — every sample feeds the range function. The
subquery-free proof travels in the context as `prometheus.QueryTraits`, because
subquery selectors evaluate at the subquery's step while the hints carry the
top-level step; call sites that do not attach traits get the conservative raw
fetch.
**Row assembly** maps stale flags to the engine's `StaleNaN` and merges series
with identical label sets (`sortAndMerge`) — the engine assumes storages never
emit duplicates. Duplicate timestamps pass through as stored: uniqueness is
ingest's job, and v1 feeds them to the engine as-is over the same data.
**The fingerprint filter is a shard-local semi-join.** The samples query
restricts to the matched series by re-running the series predicates as an
`IN (SELECT fingerprint FROM <local series table> ...)` subquery, not a GLOBAL
broadcast of the matched set. ClickHouse materializes the subquery's set per
shard before the scan, so it still engages the fingerprint primary-key column.
Because the subquery re-executes the predicates after the lookup ran, it can
match series registered in between; sample rows whose fingerprint the lookup
never saw are skipped — the lookup is the read snapshot.
---
Queries that do not transpile run in the stock engine over this package's
`storage.Querier`. This is still not the v1 path. Samples are fetched per
selector with the engine's per-selector hints, not the query-wide union
window. So `foo / foo offset 1d` reads two narrow windows, not the widest
one twice. Instant selectors of subquery-free queries fetch only the last
sample per step bucket (`lastSamplePerStep`). Buckets anchor at the
selector's first evaluation timestamp. The code recovers it from the hints
as `hints.Start + lookback - 1ms`, the inverse of how the engine derives
`hints.Start`. Bucket boundaries then coincide with evaluation timestamps.
A non-final sample of a bucket can never be the latest sample in
`(t - lookback, t]` for any grid `t`. Real timestamps are preserved, so the
engine's own lookback and staleness handling stay exact. Range selectors
always fetch raw: every sample feeds the range function. The subquery-free
proof travels in the context as `prometheus.QueryTraits`. Subquery selectors
evaluate at the subquery's step, while the hints carry the top-level step.
Row assembly maps stale flags to the engine's StaleNaN. It merges series
with identical label sets (`sortAndMerge`): the engine assumes storages
never emit duplicates.
## Sharding
`samples_v4` and `time_series_v4` (and all their rollups) shard on the same key
`cityHash64(env, temporality, metric_name, fingerprint)` — so a series'
samples and catalog rows live on the same shard. The semi-join above exploits
that: each shard filters by its own series rows, which are exactly the series
of that shard's samples.
The temporality filter on every samples statement
(`temporality IN ['Cumulative', 'Unspecified']`) is a semantic no-op — the
matched fingerprints already come from those temporalities — that engages the
leading samples primary-key column.
Delta-temporality series stay invisible to PromQL exactly as they are in v1:
the rollout gate is parity with v1, and a Delta stream fed to `rate()`
`samples_v4` and `time_series_v4` (and all their rollups) shard on the same
key: `cityHash64(env, temporality, metric_name, fingerprint)`. So a series'
samples and catalog rows live on the same shard. The transpiled statement
exploits that. The distributed samples table at the top-level FROM makes
ClickHouse rewrite the whole inner query per shard. The join against the
shard-local series table and the per-series grid aggregation run next to
the data. The initiator only merges aggregate states and applies the
spatial `-ForEach` step. This is the same layout as the telemetrymetrics
statement builder. The group-key join alone restricts the transpiled scan
to the matched series. The engine path's samples fetch restricts by the
same predicates as a shard-local semi-join, not a GLOBAL broadcast of the
matched set. The temporality filter on every samples statement is a
semantic no-op: the matched fingerprints already come from those
temporalities. It engages the leading samples primary-key column.
Delta-temporality series stay invisible to PromQL here, exactly as in v1.
The rollout gate is parity with v1. To make Delta visible is its own change
with its own semantics to design. A Delta stream fed to `rate()`
as-if-cumulative would be wrong, not just new.
---
## Observability
Every statement carries a `log_comment` with
`code.namespace=clickhouse-prometheus-v2` and `code.function.name` naming the
call site, so this provider's work is attributable in `system.query_log`.
`code.namespace=clickhouse-prometheus-v2` and `code.function.name` naming
the call site (`selectSeries`, `selectSamples`, `transpiledUnit`,
`LabelValues`, `LabelNames`). This provider's work is attributable in
`system.query_log` without guessing from query text.

View File

@@ -176,7 +176,7 @@ func (m *module) Create(ctx context.Context, orgID valuer.UUID, userEmail string
if err := m.checkAccess(ctx, orgID); err != nil {
return nil, err
}
if err := metricreductionrule.ValidatePostableReductionRule(req); err != nil {
if err := req.Validate(); err != nil {
return nil, err
}
if err := m.validateMetricForReduction(ctx, orgID, req.MetricName); err != nil {
@@ -218,7 +218,7 @@ func (m *module) UpdateByID(ctx context.Context, orgID valuer.UUID, userEmail st
if err != nil {
return nil, err
}
if err := metricreductionrule.ValidateUpdatableReductionRule(req); err != nil {
if err := req.Validate(); err != nil {
return nil, err
}
@@ -543,7 +543,7 @@ func resolveDroppedKept(matchType metricreductionruletypes.MatchType, ruleLabels
}
for _, k := range keys {
if metricreductionrule.IsProtectedLabel(k) {
if metricreductionruletypes.IsProtectedLabel(k) {
kept = append(kept, k)
continue
}

View File

@@ -354,6 +354,16 @@ function App(): JSX.Element {
tunnel: window.signozBootData.settings.sentry.tunnel,
environment: process.env.ENVIRONMENT,
release: process.env.VERSION,
// A tab that outlived a deploy requests hashed assets the new build no longer
// has. `lazyRetry` recovers by reloading once, so this class is not worth
// reporting. The stylesheet message is Vite's own; the module ones are the
// same failure worded differently by Chromium, Firefox and Safari.
ignoreErrors: [
/Unable to preload CSS for/,
/Failed to fetch dynamically imported module/,
/error loading dynamically imported module/,
/Importing a module script failed/,
],
integrations: [
// Kept for the `transaction` tag used in routing, even though
// tracing is disabled. Ref: https://github.com/SigNoz/platform-pod/issues/2393#issuecomment-4603658055

View File

@@ -19,8 +19,6 @@ import type {
GetFieldsKeysParams,
GetFieldsValues200,
GetFieldsValuesParams,
GetSemconvMigrationReport200,
GetSemconvMigrationReportParams,
RenderErrorResponseDTO,
} from '../sigNoz.schemas';
@@ -122,108 +120,6 @@ export const invalidateGetFieldsKeys = async (
return queryClient;
};
/**
* Returns services that still emit old semantic-convention names without the current family name
* @summary Get semantic-convention migration report
*/
export const getSemconvMigrationReport = (
params?: GetSemconvMigrationReportParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetSemconvMigrationReport200>({
url: `/api/v1/fields/semconv-migration`,
method: 'GET',
params,
signal,
});
};
export const getGetSemconvMigrationReportQueryKey = (
params?: GetSemconvMigrationReportParams,
) => {
return [
`/api/v1/fields/semconv-migration`,
...(params ? [params] : []),
] as const;
};
export const getGetSemconvMigrationReportQueryOptions = <
TData = Awaited<ReturnType<typeof getSemconvMigrationReport>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
params?: GetSemconvMigrationReportParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getSemconvMigrationReport>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetSemconvMigrationReportQueryKey(params);
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getSemconvMigrationReport>>
> = ({ signal }) => getSemconvMigrationReport(params, signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof getSemconvMigrationReport>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetSemconvMigrationReportQueryResult = NonNullable<
Awaited<ReturnType<typeof getSemconvMigrationReport>>
>;
export type GetSemconvMigrationReportQueryError =
ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get semantic-convention migration report
*/
export function useGetSemconvMigrationReport<
TData = Awaited<ReturnType<typeof getSemconvMigrationReport>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
params?: GetSemconvMigrationReportParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getSemconvMigrationReport>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetSemconvMigrationReportQueryOptions(params, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary Get semantic-convention migration report
*/
export const invalidateGetSemconvMigrationReport = async (
queryClient: QueryClient,
params?: GetSemconvMigrationReportParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetSemconvMigrationReportQueryKey(params) },
options,
);
return queryClient;
};
/**
* This endpoint returns field values
* @summary Get field values

View File

@@ -3482,10 +3482,6 @@ export enum TelemetrytypesFieldDataTypeDTO {
number = 'number',
'' = '',
}
export enum TelemetrytypesFieldResolutionDTO {
exact = 'exact',
'' = '',
}
export enum TelemetrytypesSignalDTO {
traces = 'traces',
logs = 'logs',
@@ -3499,7 +3495,6 @@ export interface Querybuildertypesv5GroupByKeyDTO {
description?: string;
fieldContext?: TelemetrytypesFieldContextDTO;
fieldDataType?: TelemetrytypesFieldDataTypeDTO;
fieldResolution?: TelemetrytypesFieldResolutionDTO;
/**
* @type string
*/
@@ -3540,7 +3535,6 @@ export interface Querybuildertypesv5OrderByKeyDTO {
description?: string;
fieldContext?: TelemetrytypesFieldContextDTO;
fieldDataType?: TelemetrytypesFieldDataTypeDTO;
fieldResolution?: TelemetrytypesFieldResolutionDTO;
/**
* @type string
*/
@@ -3594,7 +3588,6 @@ export interface TelemetrytypesTelemetryFieldKeyDTO {
description?: string;
fieldContext?: TelemetrytypesFieldContextDTO;
fieldDataType?: TelemetrytypesFieldDataTypeDTO;
fieldResolution?: TelemetrytypesFieldResolutionDTO;
/**
* @type string
*/
@@ -7943,7 +7936,6 @@ export interface Querybuildertypesv5ColumnDescriptorDTO {
description?: string;
fieldContext?: TelemetrytypesFieldContextDTO;
fieldDataType?: TelemetrytypesFieldDataTypeDTO;
fieldResolution?: TelemetrytypesFieldResolutionDTO;
/**
* @type object
*/
@@ -7967,25 +7959,6 @@ export type Querybuildertypesv5ExecStatsDTOStepIntervals = {
[key: string]: number;
};
export interface Querybuildertypesv5SemconvResolutionDTO {
/**
* @type string
*/
current?: string;
/**
* @type string
*/
kind?: string;
/**
* @type array,null
*/
members?: string[] | null;
/**
* @type string
*/
requested?: string;
}
/**
* Execution statistics for the query, including rows scanned, bytes scanned, and duration.
*/
@@ -8005,10 +7978,6 @@ export interface Querybuildertypesv5ExecStatsDTO {
* @minimum 0
*/
rowsScanned?: number;
/**
* @type array
*/
semconvResolutions?: Querybuildertypesv5SemconvResolutionDTO[];
/**
* @type object
*/
@@ -9741,52 +9710,6 @@ export interface TelemetrytypesGettableFieldValuesDTO {
values: TelemetrytypesTelemetryFieldValuesDTO;
}
export interface TelemetrytypesSemconvMigrationReportEntryDTO {
/**
* @type string
*/
current?: string;
/**
* @type integer
* @format int64
*/
lastSeenUnixMilli?: number;
/**
* @type string
*/
old?: string;
/**
* @type integer
* @minimum 0
*/
resourceSets?: number;
/**
* @type array,null
*/
services?: string[] | null;
/**
* @type string
*/
signal?: string;
}
export interface TelemetrytypesGettableSemconvMigrationReportDTO {
/**
* @type integer
* @format int64
*/
endUnixMilli?: number;
/**
* @type array,null
*/
entries: TelemetrytypesSemconvMigrationReportEntryDTO[] | null;
/**
* @type integer
* @format int64
*/
startUnixMilli?: number;
}
export interface TypesChangePasswordRequestDTO {
/**
* @type string
@@ -10553,29 +10476,6 @@ export type GetFieldsKeys200 = {
status: string;
};
export type GetSemconvMigrationReportParams = {
/**
* @type integer
* @format int64
* @description undefined
*/
startUnixMilli?: number;
/**
* @type integer
* @format int64
* @description undefined
*/
endUnixMilli?: number;
};
export type GetSemconvMigrationReport200 = {
data: TelemetrytypesGettableSemconvMigrationReportDTO;
/**
* @type string
*/
status: string;
};
export type GetFieldsValuesParams = {
/**
* @description undefined

View File

@@ -1,9 +0,0 @@
import axios from 'api';
import { SemconvMigrationReport } from 'types/api/semconvMigration';
async function getSemconvMigrationReport(): Promise<SemconvMigrationReport> {
const response = await axios.get('/fields/semconv-migration');
return response.data.data;
}
export default getSemconvMigrationReport;

View File

@@ -64,12 +64,12 @@ export const COMMON_FILTERS = {
SERVER_SPANS: "kind_string = 'Server'",
CLIENT_SPANS: "kind_string = 'Client'",
INTERNAL_SPANS: "kind_string = 'Internal'",
ERROR_SPANS: 'http.response.status_code >= 400',
SUCCESS_SPANS: 'http.response.status_code < 400',
ERROR_SPANS: 'http.status_code >= 400',
SUCCESS_SPANS: 'http.status_code < 400',
// Common service filters
EXCLUDE_HEALTH_CHECKS: "http.route != '/health' AND http.route != '/ping'",
HTTP_REQUESTS: "http.request.method != ''",
HTTP_REQUESTS: "http.method != ''",
// Log filters
ERROR_LOGS: "severity_text = 'ERROR'",
@@ -87,7 +87,7 @@ export const COMMON_GROUP_BY_FIELDS = {
fieldContext: 'resource' as const,
},
HTTP_METHOD: {
name: 'http.request.method',
name: 'http.method',
fieldDataType: 'string' as const,
fieldContext: 'attribute' as const,
},
@@ -97,7 +97,7 @@ export const COMMON_GROUP_BY_FIELDS = {
fieldContext: 'attribute' as const,
},
HTTP_STATUS_CODE: {
name: 'http.response.status_code',
name: 'http.status_code',
fieldDataType: 'int64' as const,
fieldContext: 'attribute' as const,
},

View File

@@ -145,7 +145,7 @@ function CeleryOverviewConfigOptions(): JSX.Element {
{
placeholder: 'Destination',
queryParam: QueryParams.destination,
filterType: ['messaging.destination.name'],
filterType: ['messaging.destination.name', 'messaging.destination'],
},
{
placeholder: 'Kind',

View File

@@ -1746,7 +1746,7 @@ QuerySearch.defaultProps = {
signalSource: '',
hardcodedAttributeKeys: undefined,
placeholder:
"Enter your filter query (e.g., http.response.status_code >= 500 AND service.name = 'frontend')",
"Enter your filter query (e.g., http.status_code >= 500 AND service.name = 'frontend')",
showFilterSuggestionsWithoutMetric: false,
initialExpression: undefined,
};

View File

@@ -152,18 +152,15 @@ describe('QuerySearch (Integration with Real CodeMirror)', () => {
/>,
);
// Wait for this mount's debounced call. A debounce from the preceding
// real-CodeMirror test can finish after mockClear(), so do not assume the
// first or last recorded call belongs to this render.
await waitFor(
() =>
expect(
mockedGetKeysOnMount.mock.calls.some(
([args]) => args.signal === DataSource.LOGS && args.searchText === '',
),
).toBe(true),
{ timeout: 2000 },
);
// Wait for debounced API call (300ms debounce + some buffer)
await waitFor(() => expect(mockedGetKeysOnMount).toHaveBeenCalled(), {
timeout: 2000,
});
const lastArgs = mockedGetKeysOnMount.mock.calls[
mockedGetKeysOnMount.mock.calls.length - 1
]?.[0] as { signal: unknown; searchText: string };
expect(lastArgs).toMatchObject({ signal: DataSource.LOGS, searchText: '' });
});
it('calls provided onRun on Mod-Enter', async () => {

View File

@@ -995,16 +995,6 @@ describe('removeKeysFromExpression', () => {
expect(result).toBe("status = 'success'");
});
it('should remove a comparison that uses the exact field wrapper', () => {
const expression =
"exact(resource.deployment.environment) EXISTS AND service.name = 'api-gateway'";
const result = removeKeysFromExpression(expression, [
'resource.deployment.environment',
]);
expect(result).toBe("service.name = 'api-gateway'");
});
it('should remove multiple keys from expression', () => {
const expression =
"service.name = 'api-gateway' AND status = 'success' AND region = 'us-east-1'";

View File

@@ -652,16 +652,7 @@ export const removeKeysFromExpression = (
}
function visitComparison(ctx: ComparisonContext): string | null {
const field = ctx.field();
// The runtime returns null for the inactive field alternative even though
// the generated TypeScript signature is non-nullable.
const exactCall = field.exactCall() as unknown as ReturnType<
typeof field.exactCall
> | null;
const keyText = (exactCall ? exactCall.key() : field.key())
.getText()
.trim()
.toLowerCase();
const keyText = ctx.key().getText().trim().toLowerCase();
if (!keysSet.has(keyText)) {
return src(ctx);

View File

@@ -5,7 +5,7 @@ import { ArrowUpRight } from '@signozhq/icons';
const QUICK_FILTER_DOC_PATHS: Record<string, string> = {
severity_text: 'severity-text',
'deployment.environment.name': 'environment',
'deployment.environment': 'environment',
'service.name': 'service-name',
'host.name': 'hostname',
'k8s.cluster.name': 'k8s-cluster-name',

View File

@@ -1,32 +0,0 @@
import { Alert } from 'antd';
import { findOldSemconvNames } from 'utils/semconv';
interface SemconvEditorWarningProps {
value: unknown;
editor: string;
}
function SemconvEditorWarning({
value,
editor,
}: SemconvEditorWarningProps): JSX.Element | null {
const text = typeof value === 'string' ? value : JSON.stringify(value ?? '');
const renames = findOldSemconvNames(text);
if (renames.length === 0) {
return null;
}
return (
<Alert
type="warning"
showIcon
data-testid="semconv-editor-warning"
message={`${editor} contains renamed OpenTelemetry fields`}
description={renames
.map(({ old, current }) => `${old}${current}`)
.join(', ')}
/>
);
}
export default SemconvEditorWarning;

View File

@@ -1,23 +0,0 @@
import { Badge } from '@signozhq/ui/badge';
import { getSemconvRename } from 'utils/semconv';
interface SemconvOldNameBadgeProps {
name: string;
}
function SemconvOldNameBadge({
name,
}: SemconvOldNameBadgeProps): JSX.Element | null {
const rename = getSemconvRename(name);
if (!rename || rename.family.kind !== 'attribute') {
return null;
}
return (
<Badge color="amber" variant="outline" data-testid="semconv-old-name-badge">
old name, renamed to {rename.current}
</Badge>
);
}
export default SemconvOldNameBadge;

View File

@@ -1,39 +0,0 @@
import { render, screen } from '@testing-library/react';
import SemconvEditorWarning from '../SemconvEditorWarning';
import SemconvOldNameBadge from '../SemconvOldNameBadge';
describe('semantic convention product hints', () => {
it('badges an old raw attribute with its current name', () => {
render(<SemconvOldNameBadge name="deployment.environment" />);
expect(screen.getByTestId('semconv-old-name-badge')).toHaveTextContent(
'old name, renamed to deployment.environment.name',
);
});
it('does not badge a current raw attribute', () => {
render(<SemconvOldNameBadge name="deployment.environment.name" />);
expect(
screen.queryByTestId('semconv-old-name-badge'),
).not.toBeInTheDocument();
});
it('shows an informational editor warning without disabling the editor', () => {
render(
<>
<input aria-label="query" defaultValue="db.system = 'postgresql'" />
<SemconvEditorWarning
value="db.system = 'postgresql'"
editor="ClickHouse SQL"
/>
</>,
);
expect(screen.getByLabelText('query')).not.toBeDisabled();
expect(screen.getByTestId('semconv-editor-warning')).toHaveTextContent(
'db.system → db.system.name',
);
});
});

View File

@@ -1,2 +0,0 @@
export { default as SemconvEditorWarning } from './SemconvEditorWarning';
export { default as SemconvOldNameBadge } from './SemconvOldNameBadge';

View File

@@ -1,235 +0,0 @@
// Code generated by scripts/semconv. DO NOT EDIT.
export type SemconvFamily = {
readonly current: string;
readonly old: readonly string[];
readonly kind: 'attribute' | 'metric';
readonly contexts: readonly string[];
readonly signals: readonly string[];
readonly applyToMetrics: readonly string[];
readonly valueMap: Readonly<Record<string, string>>;
};
export const SEMCONV_FAMILIES: readonly SemconvFamily[] = [
{
current: 'code.file.path',
old: ['code.filepath'],
kind: 'attribute',
contexts: [],
signals: [],
applyToMetrics: [],
valueMap: {},
},
{
current: 'code.function.name',
old: ['code.function'],
kind: 'attribute',
contexts: [],
signals: [],
applyToMetrics: [],
valueMap: {},
},
{
current: 'code.line.number',
old: ['code.lineno'],
kind: 'attribute',
contexts: [],
signals: [],
applyToMetrics: [],
valueMap: {},
},
{
current: 'container.cpu.usage',
old: ['container.cpu.utilization'],
kind: 'metric',
contexts: ['metric'],
signals: ['metrics'],
applyToMetrics: [],
valueMap: {},
},
{
current: 'container.runtime.name',
old: ['container.runtime'],
kind: 'attribute',
contexts: [],
signals: [],
applyToMetrics: [],
valueMap: {},
},
{
current: 'db.namespace',
old: [
'db.elasticsearch.cluster.name',
'db.name',
'db.cassandra.keyspace',
'db.hbase.namespace',
],
kind: 'attribute',
contexts: ['attribute'],
signals: ['traces'],
applyToMetrics: [],
valueMap: {},
},
{
current: 'db.operation.name',
old: ['db.operation'],
kind: 'attribute',
contexts: ['attribute'],
signals: ['traces'],
applyToMetrics: [],
valueMap: {},
},
{
current: 'db.query.text',
old: ['db.statement'],
kind: 'attribute',
contexts: ['attribute'],
signals: ['traces'],
applyToMetrics: [],
valueMap: {},
},
{
current: 'db.system.name',
old: ['db.system'],
kind: 'attribute',
contexts: ['attribute', 'resource'],
signals: ['logs', 'metrics', 'traces'],
applyToMetrics: [],
valueMap: {},
},
{
current: 'deployment.environment.name',
old: ['deployment.environment'],
kind: 'attribute',
contexts: ['attribute', 'resource'],
signals: ['logs', 'metrics', 'traces'],
applyToMetrics: [],
valueMap: {},
},
{
current: 'http.request.method',
old: ['http.method'],
kind: 'attribute',
contexts: ['attribute'],
signals: ['logs', 'traces'],
applyToMetrics: [],
valueMap: {},
},
{
current: 'http.response.status_code',
old: ['http.status_code'],
kind: 'attribute',
contexts: ['attribute'],
signals: ['logs', 'traces'],
applyToMetrics: [],
valueMap: {},
},
{
current: 'k8s.node.cpu.usage',
old: ['k8s.node.cpu.utilization'],
kind: 'metric',
contexts: ['metric'],
signals: ['metrics'],
applyToMetrics: [],
valueMap: {},
},
{
current: 'k8s.pod.cpu.usage',
old: ['k8s.pod.cpu.utilization'],
kind: 'metric',
contexts: ['metric'],
signals: ['metrics'],
applyToMetrics: [],
valueMap: {},
},
{
current: 'messaging.client.id',
old: [
'messaging.client_id',
'messaging.kafka.client_id',
'messaging.rocketmq.client_id',
],
kind: 'attribute',
contexts: ['attribute'],
signals: ['metrics', 'traces'],
applyToMetrics: [],
valueMap: {},
},
{
current: 'messaging.consumer.group.name',
old: [
'messaging.eventhubs.consumer.group',
'messaging.kafka.consumer.group',
'messaging.rocketmq.client_group',
'messaging.kafka.consumer_group',
],
kind: 'attribute',
contexts: [],
signals: [],
applyToMetrics: [],
valueMap: {},
},
{
current: 'messaging.destination.name',
old: ['messaging.destination'],
kind: 'attribute',
contexts: ['attribute'],
signals: ['traces'],
applyToMetrics: [],
valueMap: {},
},
{
current: 'messaging.operation.type',
old: ['messaging.operation'],
kind: 'attribute',
contexts: ['attribute'],
signals: ['traces'],
applyToMetrics: [],
valueMap: {},
},
{
current: 'rpc.system.name',
old: ['rpc.system'],
kind: 'attribute',
contexts: [],
signals: [],
applyToMetrics: [],
valueMap: {},
},
{
current: 'service.peer.name',
old: ['peer.service'],
kind: 'attribute',
contexts: [],
signals: [],
applyToMetrics: [],
valueMap: {},
},
{
current: 'url.full',
old: ['http.url'],
kind: 'attribute',
contexts: ['attribute'],
signals: ['logs', 'traces'],
applyToMetrics: [],
valueMap: {},
},
{
current: 'url.scheme',
old: ['http.scheme'],
kind: 'attribute',
contexts: ['attribute'],
signals: ['logs', 'traces'],
applyToMetrics: [],
valueMap: {},
},
{
current: 'user_agent.original',
old: ['browser.user_agent', 'http.user_agent'],
kind: 'attribute',
contexts: ['attribute'],
signals: ['logs', 'traces'],
applyToMetrics: [],
valueMap: {},
},
] as const;

View File

@@ -155,7 +155,7 @@ function DomainList(): JSX.Element {
dataSource={DataSource.TRACES}
queryData={query}
onChange={handleSearchChange}
placeholder="Enter your filter query (e.g., deployment.environment.name = 'otel-demo' AND service.name = 'frontend')"
placeholder="Enter your filter query (e.g., deployment.environment = 'otel-demo' AND service.name = 'frontend')"
hardcodedAttributeKeys={ApiMonitoringHardcodedAttributeKeys}
/>
</div>
@@ -180,8 +180,9 @@ function DomainList(): JSX.Element {
</div>
<div className="no-domain-subtitle">
Ensure all HTTP client spans are being sent with kind as{' '}
<span className="attribute">Client</span> and the URL set in the{' '}
<span className="attribute">url.full</span> attribute.
<span className="attribute">Client</span> and url set in{' '}
<span className="attribute">url.full</span> or{' '}
<span className="attribute">http.url</span> attribute.
</div>
<a
href={DOCLINKS.EXTERNAL_API_MONITORING}

View File

@@ -6,9 +6,9 @@ import { SPAN_ATTRIBUTES } from './Explorer/Domains/DomainDetails/constants';
export const ApiMonitoringHardcodedAttributeKeys: QueryKeyDataSuggestionsProps[] =
[
{
label: 'deployment.environment.name',
label: 'deployment.environment',
type: 'resource',
name: 'deployment.environment.name',
name: 'deployment.environment',
signal: 'traces',
fieldDataType: QUERY_BUILDER_KEY_TYPES.STRING,
},

View File

@@ -87,7 +87,7 @@ export const ApiMonitoringQuickFiltersConfig: IQuickFiltersConfig[] = [
title: 'Environment',
attributeKey: {
key: 'deployment.environment.name',
key: 'deployment.environment',
dataType: DataTypes.String,
type: 'resource',
},

View File

@@ -11,7 +11,6 @@ import {
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { FormatTimezoneAdjustedTimestamp } from 'hooks/useTimezoneFormatter/useTimezoneFormatter';
import { getSemconvMembers } from 'utils/semconv';
import styles from './traceListColumns.module.scss';
const keyToLabelMap: Record<string, string> = {
@@ -28,12 +27,7 @@ const keyToLabelMap: Record<string, string> = {
const keyAliases: Record<string, string[]> = {
serviceName: ['serviceName', 'service.name', 'service_name'],
durationNano: ['durationNano', 'duration.nano', 'duration_nano'],
httpMethod: [
'httpMethod',
...getSemconvMembers('http.request.method'),
'http_request_method',
'http_method',
],
httpMethod: ['httpMethod', 'http.method', 'http_method'],
responseStatusCode: [
'response_status_code',
'response.status.code',

View File

@@ -112,7 +112,7 @@ export const INFRA_MONITORING_ATTR_KEYS = {
K8S_OBJECT_NAME: 'k8s.object.name',
// Environment
DEPLOYMENT_ENVIRONMENT: 'deployment.environment.name',
DEPLOYMENT_ENVIRONMENT: 'deployment.environment',
// Host System
OS_TYPE: 'os.type',
@@ -733,7 +733,7 @@ export const ENTITY_FILTER_PLACEHOLDERS: Record<InfraMonitoringEntity, string> =
[InfraMonitoringEntity.NAMESPACES]:
"Enter your filter query (e.g., k8s.namespace.name = 'production' AND k8s.cluster.name = 'prod-cluster')",
[InfraMonitoringEntity.CLUSTERS]:
"Enter your filter query (e.g., k8s.cluster.name = 'prod-cluster' AND deployment.environment.name = 'production')",
"Enter your filter query (e.g., k8s.cluster.name = 'prod-cluster' AND deployment.environment = 'production')",
[InfraMonitoringEntity.DEPLOYMENTS]:
"Enter your filter query (e.g., k8s.deployment.name = 'api-server' AND k8s.namespace.name = 'production')",
[InfraMonitoringEntity.STATEFULSETS]:

View File

@@ -2,17 +2,6 @@
color: white;
}
.semconv-migration-report {
margin-top: 32px;
display: flex;
flex-direction: column;
gap: 12px;
.ant-table-wrapper {
margin-top: 4px;
}
}
.ingestion-key-container {
margin-top: 24px;
display: flex;

View File

@@ -5,8 +5,6 @@ import getIngestionData from 'api/settings/getIngestionData';
import { useAppContext } from 'providers/App/App';
import { IngestionDataType } from 'types/api/settings/ingestion';
import SemconvMigrationReport from './SemconvMigrationReport';
import './IngestionSettings.styles.scss';
export default function IngestionSettings(): JSX.Element {
@@ -86,7 +84,6 @@ export default function IngestionSettings(): JSX.Element {
dataSource={data}
bordered
/>
<SemconvMigrationReport />
</div>
);
}

View File

@@ -83,8 +83,6 @@ import { MeterAggregateOperator } from 'types/common/queryBuilder';
import { USER_ROLES } from 'types/roles';
import { getDaysUntilExpiry } from 'utils/timeUtils';
import SemconvMigrationReport from './SemconvMigrationReport';
import './IngestionSettings.styles.scss';
const { Option } = Select;
@@ -1707,7 +1705,6 @@ function MultiIngestionSettings(): JSX.Element {
}}
className="ingestion-keys-table"
/>
<SemconvMigrationReport />
</div>
{/* Delete Key Modal */}

View File

@@ -1,72 +0,0 @@
import { useQuery } from 'react-query';
import { Alert, Table, TableColumnsType } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import getSemconvMigrationReport from 'api/semconv/getMigrationReport';
import dayjs from 'dayjs';
import { SemconvMigrationReportEntry } from 'types/api/semconvMigration';
function SemconvMigrationReport(): JSX.Element {
const { data, isLoading, isError } = useQuery({
queryKey: ['semconv-migration-report'],
queryFn: getSemconvMigrationReport,
});
const columns: TableColumnsType<SemconvMigrationReportEntry> = [
{
title: 'Old name',
dataIndex: 'old',
key: 'old',
},
{
title: 'Current name',
dataIndex: 'current',
key: 'current',
},
{
title: 'Signal',
dataIndex: 'signal',
key: 'signal',
},
{
title: 'Services still sending only the old name',
dataIndex: 'services',
key: 'services',
render: (services: string[]): string => services.join(', '),
},
{
title: 'Last seen',
dataIndex: 'lastSeenUnixMilli',
key: 'lastSeenUnixMilli',
render: (value: number): string =>
dayjs(value).format('YYYY-MM-DD HH:mm:ss'),
},
];
return (
<section className="semconv-migration-report">
<Typography.Title level={4}>Semantic convention migration</Typography.Title>
<Typography.Text>
Services in this report sent an old OpenTelemetry field during the last 24
hours without sending its current replacement. Update their SDK or
instrumentation when practical; SigNoz queries remain backward compatible.
</Typography.Text>
{isError && (
<Alert
type="error"
showIcon
message="Could not load the semantic convention migration report"
/>
)}
<Table
loading={isLoading}
columns={columns}
dataSource={data?.entries ?? []}
rowKey={(entry): string => `${entry.current}-${entry.old}-${entry.signal}`}
pagination={false}
locale={{ emptyText: 'No old-only services found in the last 24 hours' }}
/>
</section>
);
}
export default SemconvMigrationReport;

View File

@@ -15,7 +15,7 @@ export const SAMPLE_SPAN_JSON = `{
},
"resource": {
"service.name": "llm-gateway",
"deployment.environment.name": "production"
"deployment.environment": "production"
}
}`;

View File

@@ -1120,7 +1120,7 @@
"plugin": {
"kind": "signoz/QueryVariable",
"spec": {
"queryValue": "SELECT DISTINCT resources_string['deployment.environment.name'] AS environment FROM signoz_traces.distributed_signoz_index_v3 WHERE mapContains(resources_string, 'deployment.environment.name') AND timestamp >= now() - INTERVAL 1 DAY"
"queryValue": "SELECT DISTINCT resources_string['deployment.environment'] AS environment FROM signoz_traces.distributed_signoz_index_v3 WHERE mapContains(resources_string, 'deployment.environment') AND timestamp >= now() - INTERVAL 1 DAY"
}
}
}

View File

@@ -1,7 +1,6 @@
import { Divider } from '@signozhq/ui/divider';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import { Typography } from '@signozhq/ui/typography';
import { SemconvOldNameBadge } from 'components/Semconv';
import { TagContainer, TagLabel, TagValue } from './FieldRenderer.styles';
import { FieldRendererProps } from './LogDetailedView.types';
@@ -29,7 +28,6 @@ function FieldRenderer({ field }: FieldRendererProps): JSX.Element {
<Typography.Text truncate={1} className="label">
{newField}{' '}
</Typography.Text>
<SemconvOldNameBadge name={newField} />
</TooltipSimple>
<div className="tags">
@@ -49,10 +47,7 @@ function FieldRenderer({ field }: FieldRendererProps): JSX.Element {
</div>
</>
) : (
<>
<span className="label">{field}</span>
<SemconvOldNameBadge name={field} />
</>
<span className="label">{field}</span>
)}
</span>
);

View File

@@ -164,9 +164,7 @@ describe('useInitialQuery - Priority-Based Resource Filtering', () => {
value: 'frontend-service',
}),
expect.objectContaining({
key: expect.objectContaining({
key: 'deployment.environment.name',
}),
key: expect.objectContaining({ key: 'deployment.environment' }),
value: 'production',
}),
expect.objectContaining({
@@ -288,9 +286,7 @@ describe('useInitialQuery - Priority-Based Resource Filtering', () => {
value: 'legacy-app',
}),
expect.objectContaining({
key: expect.objectContaining({
key: 'deployment.environment.name',
}),
key: expect.objectContaining({ key: 'deployment.environment' }),
value: 'production',
}),
expect.objectContaining({

View File

@@ -6,14 +6,13 @@ import {
TagFilterItem,
} from 'types/api/queryBuilder/queryBuilderData';
import { v4 as uuid } from 'uuid';
import { getSemconvRename } from 'utils/semconv';
const FALLBACK_STARTS_WITH_REGEX = /^(k8s|cloud|host|deployment)/; // regex to filter out resources that start with the specified keywords
const FALLBACK_CONTAINS_REGEX = /(env|service|file|container|tenant)/; // regex to filter out resources that contains the specified keywords
// Priority categories for filter selection
// Strategy:
// - Always include: service.name, deployment.environment.name, env, environment
// - Always include: service.name, deployment.environment, env, environment
// - Select ONE category only: stops at the first category with a matching attribute
// - Within category: picks the first available attribute by order
// - Order (highest to lowest priority): Kubernetes > Cloud > Host > Container
@@ -27,36 +26,27 @@ const PRIORITY_CATEGORIES = [
const SERVICE_AND_ENVIRONMENT_KEYS = [
'service.name',
'deployment.environment.name',
'deployment.environment',
'env',
'environment',
];
export const getFiltersFromResources = (
resources: ILog['resources_string'],
): TagFilterItem[] => {
const items = new Map<string, TagFilterItem>();
Object.keys(resources).forEach((key: string) => {
const currentKey = getSemconvRename(key)?.current ?? key;
): TagFilterItem[] =>
Object.keys(resources).map((key: string) => {
const resourceValue = resources[key] as string;
const item = {
return {
id: uuid(),
key: {
key: currentKey,
key,
dataType: DataTypes.String,
type: 'resource',
},
op: OPERATORS['='],
value: resourceValue,
};
// If raw data contains both names, retain the current value just like the
// backend's current-first resolver.
if (!items.has(currentKey) || key === currentKey) {
items.set(currentKey, item);
}
});
return Array.from(items.values());
};
export const isServiceOrEnvironmentAttribute = (key: string): boolean =>
SERVICE_AND_ENVIRONMENT_KEYS.includes(key);

View File

@@ -94,7 +94,7 @@ function DBCall(): JSX.Element {
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
const legend = dotMetricsEnabled ? '{{db.system.name}}' : '{{db_system_name}}';
const legend = dotMetricsEnabled ? '{{db.system}}' : '{{db_system}}';
const databaseCallsRPSWidget = useMemo(
() =>

View File

@@ -28,7 +28,7 @@ import { v4 as uuid } from 'uuid';
export const dbSystemTags: Tags[] = [
{
Key: 'db.system.name.(string)',
Key: 'db.system.(string)',
StringValues: [''],
NumberValues: [],
BoolValues: [],

View File

@@ -103,7 +103,7 @@ export enum WidgetKeys {
SignozExternalCallLatencySum = 'signoz_external_call_latency_sum',
Signoz_latency_bucket_norm = 'signoz_latency_bucket',
Signoz_latency_bucket = 'signoz_latency.bucket',
Db_system = 'db.system.name',
Db_system = 'db.system',
Db_system_norm = 'db_system',
}

View File

@@ -2,7 +2,6 @@ import { ChangeEvent, useCallback } from 'react';
import MEditor, { Monaco } from '@monaco-editor/react';
import { Color } from '@signozhq/design-tokens';
import { Input } from 'antd';
import { SemconvEditorWarning } from 'components/Semconv';
import { LEGEND } from 'constants/global';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useIsDarkMode } from 'hooks/useDarkMode';
@@ -119,7 +118,6 @@ function ClickHouseQueryBuilder({
theme={isDarkMode ? 'my-theme' : 'light'}
beforeMount={setEditorTheme}
/>
<SemconvEditorWarning value={queryData?.query} editor="ClickHouse SQL" />
<Input
onChange={handleUpdateInput}
name="legend"

View File

@@ -1,7 +1,6 @@
import { ChangeEvent, useCallback } from 'react';
import { Input } from 'antd';
import { LEGEND } from 'constants/global';
import { SemconvEditorWarning } from 'components/Semconv';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { IPromQLQuery } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
@@ -67,7 +66,6 @@ function PromQLQueryBuilder({
style={{ marginBottom: '0.5rem' }}
data-testid="promql-query-input"
/>
<SemconvEditorWarning value={queryData?.query} editor="PromQL" />
<Input
onChange={handleUpdateQuery}

View File

@@ -1,7 +1,6 @@
import { useTranslation } from 'react-i18next';
import { Form } from 'antd';
import { initialQueryBuilderFormValuesMap } from 'constants/queryBuilder';
import { SemconvEditorWarning } from 'components/Semconv';
import QueryBuilderSearchV2 from 'container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2';
import isEqual from 'lodash-es/isEqual';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
@@ -56,7 +55,6 @@ function TagFilterInputWithLogsResultPreview({
value={value}
onChange={onChange}
/>
<SemconvEditorWarning value={value} editor="Pipeline filter" />
<div className="pipeline-filter-input-preview-container">
<LogsFilterPreview filter={value} />
</div>

View File

@@ -424,7 +424,7 @@ describe('ResourceProvider', () => {
await waitFor(() => {
expect(result.current.queries).toHaveLength(1);
expect(result.current.queries[0]).toMatchObject({
tagKey: 'resource_deployment_environment_name',
tagKey: 'resource_deployment_environment',
operator: 'IN',
tagValue: ['production'],
});
@@ -435,7 +435,7 @@ describe('ResourceProvider', () => {
const seeded = [
{
id: 'env',
tagKey: 'resource_deployment_environment_name',
tagKey: 'resource_deployment_environment',
operator: 'IN',
tagValue: ['production'],
},
@@ -459,7 +459,7 @@ describe('ResourceProvider', () => {
await waitFor(() => {
const tagKeys = result.current.queries.map((q) => q.tagKey);
expect(tagKeys).not.toContain('resource_deployment_environment_name');
expect(tagKeys).not.toContain('resource_deployment_environment');
expect(tagKeys).toContain('resource_service_name');
});
});
@@ -468,7 +468,7 @@ describe('ResourceProvider', () => {
const seeded = [
{
id: 'env',
tagKey: 'resource_deployment_environment_name',
tagKey: 'resource_deployment_environment',
operator: 'IN',
tagValue: ['production'],
},
@@ -486,7 +486,7 @@ describe('ResourceProvider', () => {
await waitFor(() => {
const envQueries = result.current.queries.filter(
(q) => q.tagKey === 'resource_deployment_environment_name',
(q) => q.tagKey === 'resource_deployment_environment',
);
expect(envQueries).toHaveLength(1);
expect(envQueries[0].tagValue).toStrictEqual(['staging']);
@@ -518,7 +518,7 @@ describe('ResourceProvider', () => {
await waitFor(() => {
expect(result.current.queries[0].tagKey).toBe(
'resource_deployment.environment.name',
'resource_deployment.environment',
);
});
});

View File

@@ -6,13 +6,13 @@ import { mappingWithRoutesAndKeys } from '../utils';
describe('useResourceAttribute config', () => {
describe('whilelistedKeys', () => {
it('should include underscore-notation keys (DOT_METRICS_ENABLED=false)', () => {
expect(whilelistedKeys).toContain('resource_deployment_environment_name');
expect(whilelistedKeys).toContain('resource_deployment_environment');
expect(whilelistedKeys).toContain('resource_k8s_cluster_name');
expect(whilelistedKeys).toContain('resource_k8s_cluster_namespace');
});
it('should include dot-notation keys (DOT_METRICS_ENABLED=true)', () => {
expect(whilelistedKeys).toContain('resource_deployment.environment.name');
expect(whilelistedKeys).toContain('resource_deployment.environment');
expect(whilelistedKeys).toContain('resource_k8s.cluster.name');
expect(whilelistedKeys).toContain('resource_k8s.cluster.namespace');
});
@@ -21,8 +21,8 @@ describe('useResourceAttribute config', () => {
describe('mappingWithRoutesAndKeys', () => {
const dotNotationFilters = [
{
label: 'deployment.environment.name',
value: 'resource_deployment.environment.name',
label: 'deployment.environment',
value: 'resource_deployment.environment',
},
{ label: 'k8s.cluster.name', value: 'resource_k8s.cluster.name' },
{ label: 'k8s.cluster.namespace', value: 'resource_k8s.cluster.namespace' },
@@ -30,8 +30,8 @@ describe('useResourceAttribute config', () => {
const underscoreNotationFilters = [
{
label: 'deployment.environment.name',
value: 'resource_deployment_environment_name',
label: 'deployment.environment',
value: 'resource_deployment_environment',
},
{ label: 'k8s.cluster.name', value: 'resource_k8s_cluster_name' },
{ label: 'k8s.cluster.namespace', value: 'resource_k8s_cluster_namespace' },

View File

@@ -1,6 +1,6 @@
export const whilelistedKeys = [
'resource_deployment_environment_name',
'resource_deployment.environment.name',
'resource_deployment_environment',
'resource_deployment.environment',
'resource_k8s_cluster_name',
'resource_k8s.cluster.name',
'resource_k8s_cluster_namespace',

View File

@@ -148,9 +148,9 @@ export const getResourceDeploymentKeys = (
dotMetricsEnabled: boolean,
): string => {
if (dotMetricsEnabled) {
return 'resource_deployment.environment.name';
return 'resource_deployment.environment';
}
return 'resource_deployment_environment_name';
return 'resource_deployment_environment';
};
export const GetTagKeys = async (

View File

@@ -35,7 +35,13 @@ export default function CeleryOverviewDetails({
? undefined
: getFiltersFromKeyValue('messaging.system', value, 'tag');
case 'destination':
return getFiltersFromKeyValue('messaging.destination.name', value, 'tag');
return getFiltersFromKeyValue(
details.messaging_system === 'celery'
? 'messaging.destination'
: 'messaging.destination.name',
value,
'tag',
);
case 'kind_string':
return getFiltersFromKeyValue('kind_string', value, '');
default:

View File

@@ -233,7 +233,7 @@ describe('Logs Explorer Tests', () => {
);
const queries = queryAllByText(
"Enter your filter query (e.g., http.response.status_code >= 500 AND service.name = 'frontend')",
"Enter your filter query (e.g., http.status_code >= 500 AND service.name = 'frontend')",
);
expect(queries).toHaveLength(1);
});

View File

@@ -40,7 +40,7 @@ export const LogsQuickFiltersConfig: IQuickFiltersConfig[] = [
type: FiltersType.CHECKBOX,
title: 'Environment',
attributeKey: {
key: 'deployment.environment.name',
key: 'deployment.environment',
dataType: DataTypes.String,
type: 'resource',
},

View File

@@ -11,7 +11,6 @@ import { Skeleton } from 'antd';
import { DetailsHeader, DetailsPanelDrawer } from 'components/DetailsPanel';
import { HeaderAction } from 'components/DetailsPanel/DetailsHeader/DetailsHeader';
import { DetailsPanelState } from 'components/DetailsPanel/types';
import { SemconvOldNameBadge } from 'components/Semconv';
import { QueryParams } from 'constants/query';
import {
initialQueryBuilderFormValuesMap,
@@ -109,12 +108,6 @@ function SpanDetailsContent({
() => getSpanDisplayData(selectedSpan),
[selectedSpan],
);
const semconvLabelSuffix = useCallback(
(fieldKey: string): React.ReactNode => (
<SemconvOldNameBadge name={fieldKey} />
),
[],
);
// Map span attribute actions to PrettyView actions format.
// Use the last key in fieldKeyPath (the actual attribute key), not the full display path.
@@ -336,7 +329,6 @@ function SpanDetailsContent({
visibleActions: VISIBLE_ACTIONS,
pinnedFieldsValue,
onPinnedFieldsChange,
labelSuffixRenderer: semconvLabelSuffix,
}}
/>
</TabsContent>

View File

@@ -29,7 +29,7 @@ export const KEY_ATTRIBUTE_KEYS: Record<string, string[]> = {
traces: [
'service.name',
'service.namespace',
'deployment.environment.name',
'deployment.environment',
'timestamp',
'duration_nano',
'kind_string',

View File

@@ -359,7 +359,7 @@ function Filters({
onChange={handleExpressionChange}
onRun={handleRunQuery}
dataSource={DataSource.TRACES}
placeholder="Enter your filter query (e.g., http.response.status_code >= 500 AND service.name = 'frontend')"
placeholder="Enter your filter query (e.g., http.status_code >= 500 AND service.name = 'frontend')"
/>
</div>
</div>

View File

@@ -22,8 +22,8 @@ export const SPAN_CATEGORIES: readonly SpanCategory[] = [
// Map each category to the attribute key it filters on
const CATEGORY_KEYS: Record<Exclude<SpanCategory, 'All'>, string> = {
Database: 'db.system.name',
HTTP: 'http.request.method',
Database: 'db.system',
HTTP: 'http.method',
Functions: 'kind_string',
Jobs: 'messaging.system',
LLM: 'gen_ai.request.model',
@@ -34,8 +34,8 @@ const ALL_CATEGORY_KEYS = Object.values(CATEGORY_KEYS);
// The expression clause to add for each category
const CATEGORY_EXPRESSIONS: Record<Exclude<SpanCategory, 'All'>, string> = {
Database: 'db.system.name exists',
HTTP: 'http.request.method exists',
Database: 'db.system exists',
HTTP: 'http.method exists',
Functions: "kind_string = 'Internal'",
Jobs: 'messaging.system exists',
LLM: 'gen_ai.request.model exists',

View File

@@ -38,7 +38,7 @@ export function Section(props: SectionProps): JSX.Element {
'hasError',
'durationNano',
'serviceName',
'deployment.environment.name',
'deployment.environment',
]),
),
[selectedFilters],

View File

@@ -14,7 +14,7 @@ export const AllTraceFilterKeyValue: Record<string, string> = {
durationNano: 'Duration',
duration_nano: 'Duration',
durationNanoMax: 'Duration',
'deployment.environment.name': 'Environment',
'deployment.environment': 'Environment',
hasError: 'Status',
has_error: 'Status',
serviceName: 'Service Name',
@@ -208,11 +208,11 @@ export const traceFilterKeys: Record<AllTraceFilterKeys, BaseAutocompleteData> =
id: 'serviceName--string--tag--true',
},
'deployment.environment.name': {
key: 'deployment.environment.name',
'deployment.environment': {
key: 'deployment.environment',
dataType: DataTypes.String,
type: 'resource',
id: 'deployment.environment.name--string--resource--false',
id: 'deployment.environment--string--resource--false',
},
name: {
key: 'name',

View File

@@ -74,8 +74,7 @@ export function tracesRunQueryAction(
properties: {
key: {
type: 'string',
description:
'Attribute key, e.g. service.name, http.response.status_code',
description: 'Attribute key, e.g. service.name, http.status_code',
},
op: {
type: 'string',
@@ -144,7 +143,7 @@ export function tracesAddFilterAction(
properties: {
key: {
type: 'string',
description: 'Attribute key, e.g. service.name, http.response.status_code',
description: 'Attribute key, e.g. service.name, http.status_code',
},
op: {
type: 'string',

File diff suppressed because one or more lines are too long

View File

@@ -24,14 +24,12 @@ HASTOKEN=23
HAS=24
HASANY=25
HASALL=26
SEARCH=27
EXACT=28
BOOL=29
NUMBER=30
QUOTED_TEXT=31
KEY=32
WS=33
FREETEXT=34
BOOL=27
NUMBER=28
QUOTED_TEXT=29
KEY=30
WS=31
FREETEXT=32
'('=1
')'=2
'['=3

File diff suppressed because one or more lines are too long

View File

@@ -24,14 +24,12 @@ HASTOKEN=23
HAS=24
HASANY=25
HASALL=26
SEARCH=27
EXACT=28
BOOL=29
NUMBER=30
QUOTED_TEXT=31
KEY=32
WS=33
FREETEXT=34
BOOL=27
NUMBER=28
QUOTED_TEXT=29
KEY=30
WS=31
FREETEXT=32
'('=1
')'=2
'['=3

View File

@@ -1,4 +1,4 @@
// Generated from grammar/FilterQuery.g4 by ANTLR 4.13.2
// Generated from FilterQuery.g4 by ANTLR 4.13.1
// noinspection ES6UnusedImports,JSUnusedGlobalSymbols,JSUnusedLocalSymbols
import {
ATN,
@@ -38,14 +38,12 @@ export default class FilterQueryLexer extends Lexer {
public static readonly HAS = 24;
public static readonly HASANY = 25;
public static readonly HASALL = 26;
public static readonly SEARCH = 27;
public static readonly EXACT = 28;
public static readonly BOOL = 29;
public static readonly NUMBER = 30;
public static readonly QUOTED_TEXT = 31;
public static readonly KEY = 32;
public static readonly WS = 33;
public static readonly FREETEXT = 34;
public static readonly BOOL = 27;
public static readonly NUMBER = 28;
public static readonly QUOTED_TEXT = 29;
public static readonly KEY = 30;
public static readonly WS = 31;
public static readonly FREETEXT = 32;
public static readonly EOF = Token.EOF;
public static readonly channelNames: string[] = [ "DEFAULT_TOKEN_CHANNEL", "HIDDEN" ];
@@ -70,8 +68,7 @@ export default class FilterQueryLexer extends Lexer {
"AND", "OR",
"HASTOKEN",
"HAS", "HASANY",
"HASALL", "SEARCH",
"EXACT", "BOOL",
"HASALL", "BOOL",
"NUMBER", "QUOTED_TEXT",
"KEY", "WS",
"FREETEXT" ];
@@ -81,8 +78,8 @@ export default class FilterQueryLexer extends Lexer {
"LPAREN", "RPAREN", "LBRACK", "RBRACK", "COMMA", "EQUALS", "NOT_EQUALS",
"NEQ", "LT", "LE", "GT", "GE", "LIKE", "ILIKE", "BETWEEN", "EXISTS", "REGEXP",
"CONTAINS", "IN", "NOT", "AND", "OR", "HASTOKEN", "HAS", "HASANY", "HASALL",
"SEARCH", "EXACT", "BOOL", "SIGN", "NUMBER", "QUOTED_TEXT", "SEGMENT",
"EMPTY_BRACKS", "OLD_JSON_BRACKS", "KEY", "WS", "DIGIT", "FREETEXT",
"BOOL", "SIGN", "NUMBER", "QUOTED_TEXT", "SEGMENT", "EMPTY_BRACKS", "OLD_JSON_BRACKS",
"KEY", "WS", "DIGIT", "FREETEXT",
];
@@ -103,124 +100,119 @@ export default class FilterQueryLexer extends Lexer {
public get modeNames(): string[] { return FilterQueryLexer.modeNames; }
public static readonly _serializedATN: number[] = [4,0,34,337,6,-1,2,0,
public static readonly _serializedATN: number[] = [4,0,32,320,6,-1,2,0,
7,0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,5,2,6,7,6,2,7,7,7,2,8,7,8,2,9,
7,9,2,10,7,10,2,11,7,11,2,12,7,12,2,13,7,13,2,14,7,14,2,15,7,15,2,16,7,
16,2,17,7,17,2,18,7,18,2,19,7,19,2,20,7,20,2,21,7,21,2,22,7,22,2,23,7,23,
2,24,7,24,2,25,7,25,2,26,7,26,2,27,7,27,2,28,7,28,2,29,7,29,2,30,7,30,2,
31,7,31,2,32,7,32,2,33,7,33,2,34,7,34,2,35,7,35,2,36,7,36,2,37,7,37,2,38,
7,38,1,0,1,0,1,1,1,1,1,2,1,2,1,3,1,3,1,4,1,4,1,5,1,5,1,5,3,5,93,8,5,1,6,
1,6,1,6,1,7,1,7,1,7,1,8,1,8,1,9,1,9,1,9,1,10,1,10,1,11,1,11,1,11,1,12,1,
12,1,12,1,12,1,12,1,13,1,13,1,13,1,13,1,13,1,13,1,14,1,14,1,14,1,14,1,14,
1,14,1,14,1,14,1,15,1,15,1,15,1,15,1,15,1,15,3,15,136,8,15,1,16,1,16,1,
16,1,16,1,16,1,16,1,16,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,3,17,153,
8,17,1,18,1,18,1,18,1,19,1,19,1,19,1,19,1,20,1,20,1,20,1,20,1,21,1,21,1,
21,1,22,1,22,1,22,1,22,1,22,1,22,1,22,1,22,1,22,1,23,1,23,1,23,1,23,1,24,
1,24,1,24,1,24,1,24,1,24,1,24,1,25,1,25,1,25,1,25,1,25,1,25,1,25,1,26,1,
26,1,26,1,26,1,26,1,26,1,26,1,27,1,27,1,27,1,27,1,27,1,27,1,28,1,28,1,28,
1,28,1,28,1,28,1,28,1,28,1,28,3,28,218,8,28,1,29,1,29,1,30,3,30,223,8,30,
1,30,4,30,226,8,30,11,30,12,30,227,1,30,1,30,5,30,232,8,30,10,30,12,30,
235,9,30,3,30,237,8,30,1,30,1,30,3,30,241,8,30,1,30,4,30,244,8,30,11,30,
12,30,245,3,30,248,8,30,1,30,3,30,251,8,30,1,30,1,30,4,30,255,8,30,11,30,
12,30,256,1,30,1,30,3,30,261,8,30,1,30,4,30,264,8,30,11,30,12,30,265,3,
30,268,8,30,3,30,270,8,30,1,31,1,31,1,31,1,31,5,31,276,8,31,10,31,12,31,
279,9,31,1,31,1,31,1,31,1,31,1,31,5,31,286,8,31,10,31,12,31,289,9,31,1,
31,3,31,292,8,31,1,32,1,32,5,32,296,8,32,10,32,12,32,299,9,32,1,33,1,33,
1,33,1,34,1,34,1,34,1,34,1,35,1,35,1,35,1,35,1,35,1,35,1,35,4,35,315,8,
35,11,35,12,35,316,5,35,319,8,35,10,35,12,35,322,9,35,1,36,4,36,325,8,36,
11,36,12,36,326,1,36,1,36,1,37,1,37,1,38,4,38,334,8,38,11,38,12,38,335,
0,0,39,1,1,3,2,5,3,7,4,9,5,11,6,13,7,15,8,17,9,19,10,21,11,23,12,25,13,
27,14,29,15,31,16,33,17,35,18,37,19,39,20,41,21,43,22,45,23,47,24,49,25,
51,26,53,27,55,28,57,29,59,0,61,30,63,31,65,0,67,0,69,0,71,32,73,33,75,
0,77,34,1,0,29,2,0,76,76,108,108,2,0,73,73,105,105,2,0,75,75,107,107,2,
0,69,69,101,101,2,0,66,66,98,98,2,0,84,84,116,116,2,0,87,87,119,119,2,0,
78,78,110,110,2,0,88,88,120,120,2,0,83,83,115,115,2,0,82,82,114,114,2,0,
71,71,103,103,2,0,80,80,112,112,2,0,67,67,99,99,2,0,79,79,111,111,2,0,65,
65,97,97,2,0,68,68,100,100,2,0,72,72,104,104,2,0,89,89,121,121,2,0,85,85,
117,117,2,0,70,70,102,102,2,0,43,43,45,45,2,0,34,34,92,92,2,0,39,39,92,
92,4,0,35,36,64,90,95,95,97,123,7,0,35,36,45,45,47,58,64,90,95,95,97,123,
125,125,3,0,9,10,13,13,32,32,1,0,48,57,8,0,9,10,13,13,32,34,39,41,44,44,
60,62,91,91,93,93,361,0,1,1,0,0,0,0,3,1,0,0,0,0,5,1,0,0,0,0,7,1,0,0,0,0,
9,1,0,0,0,0,11,1,0,0,0,0,13,1,0,0,0,0,15,1,0,0,0,0,17,1,0,0,0,0,19,1,0,
0,0,0,21,1,0,0,0,0,23,1,0,0,0,0,25,1,0,0,0,0,27,1,0,0,0,0,29,1,0,0,0,0,
31,1,0,0,0,0,33,1,0,0,0,0,35,1,0,0,0,0,37,1,0,0,0,0,39,1,0,0,0,0,41,1,0,
0,0,0,43,1,0,0,0,0,45,1,0,0,0,0,47,1,0,0,0,0,49,1,0,0,0,0,51,1,0,0,0,0,
53,1,0,0,0,0,55,1,0,0,0,0,57,1,0,0,0,0,61,1,0,0,0,0,63,1,0,0,0,0,71,1,0,
0,0,0,73,1,0,0,0,0,77,1,0,0,0,1,79,1,0,0,0,3,81,1,0,0,0,5,83,1,0,0,0,7,
85,1,0,0,0,9,87,1,0,0,0,11,92,1,0,0,0,13,94,1,0,0,0,15,97,1,0,0,0,17,100,
1,0,0,0,19,102,1,0,0,0,21,105,1,0,0,0,23,107,1,0,0,0,25,110,1,0,0,0,27,
115,1,0,0,0,29,121,1,0,0,0,31,129,1,0,0,0,33,137,1,0,0,0,35,144,1,0,0,0,
37,154,1,0,0,0,39,157,1,0,0,0,41,161,1,0,0,0,43,165,1,0,0,0,45,168,1,0,
0,0,47,177,1,0,0,0,49,181,1,0,0,0,51,188,1,0,0,0,53,195,1,0,0,0,55,202,
1,0,0,0,57,217,1,0,0,0,59,219,1,0,0,0,61,269,1,0,0,0,63,291,1,0,0,0,65,
293,1,0,0,0,67,300,1,0,0,0,69,303,1,0,0,0,71,307,1,0,0,0,73,324,1,0,0,0,
75,330,1,0,0,0,77,333,1,0,0,0,79,80,5,40,0,0,80,2,1,0,0,0,81,82,5,41,0,
0,82,4,1,0,0,0,83,84,5,91,0,0,84,6,1,0,0,0,85,86,5,93,0,0,86,8,1,0,0,0,
87,88,5,44,0,0,88,10,1,0,0,0,89,93,5,61,0,0,90,91,5,61,0,0,91,93,5,61,0,
0,92,89,1,0,0,0,92,90,1,0,0,0,93,12,1,0,0,0,94,95,5,33,0,0,95,96,5,61,0,
0,96,14,1,0,0,0,97,98,5,60,0,0,98,99,5,62,0,0,99,16,1,0,0,0,100,101,5,60,
0,0,101,18,1,0,0,0,102,103,5,60,0,0,103,104,5,61,0,0,104,20,1,0,0,0,105,
106,5,62,0,0,106,22,1,0,0,0,107,108,5,62,0,0,108,109,5,61,0,0,109,24,1,
0,0,0,110,111,7,0,0,0,111,112,7,1,0,0,112,113,7,2,0,0,113,114,7,3,0,0,114,
26,1,0,0,0,115,116,7,1,0,0,116,117,7,0,0,0,117,118,7,1,0,0,118,119,7,2,
0,0,119,120,7,3,0,0,120,28,1,0,0,0,121,122,7,4,0,0,122,123,7,3,0,0,123,
124,7,5,0,0,124,125,7,6,0,0,125,126,7,3,0,0,126,127,7,3,0,0,127,128,7,7,
0,0,128,30,1,0,0,0,129,130,7,3,0,0,130,131,7,8,0,0,131,132,7,1,0,0,132,
133,7,9,0,0,133,135,7,5,0,0,134,136,7,9,0,0,135,134,1,0,0,0,135,136,1,0,
0,0,136,32,1,0,0,0,137,138,7,10,0,0,138,139,7,3,0,0,139,140,7,11,0,0,140,
141,7,3,0,0,141,142,7,8,0,0,142,143,7,12,0,0,143,34,1,0,0,0,144,145,7,13,
0,0,145,146,7,14,0,0,146,147,7,7,0,0,147,148,7,5,0,0,148,149,7,15,0,0,149,
150,7,1,0,0,150,152,7,7,0,0,151,153,7,9,0,0,152,151,1,0,0,0,152,153,1,0,
0,0,153,36,1,0,0,0,154,155,7,1,0,0,155,156,7,7,0,0,156,38,1,0,0,0,157,158,
7,7,0,0,158,159,7,14,0,0,159,160,7,5,0,0,160,40,1,0,0,0,161,162,7,15,0,
0,162,163,7,7,0,0,163,164,7,16,0,0,164,42,1,0,0,0,165,166,7,14,0,0,166,
167,7,10,0,0,167,44,1,0,0,0,168,169,7,17,0,0,169,170,7,15,0,0,170,171,7,
9,0,0,171,172,7,5,0,0,172,173,7,14,0,0,173,174,7,2,0,0,174,175,7,3,0,0,
175,176,7,7,0,0,176,46,1,0,0,0,177,178,7,17,0,0,178,179,7,15,0,0,179,180,
7,9,0,0,180,48,1,0,0,0,181,182,7,17,0,0,182,183,7,15,0,0,183,184,7,9,0,
0,184,185,7,15,0,0,185,186,7,7,0,0,186,187,7,18,0,0,187,50,1,0,0,0,188,
189,7,17,0,0,189,190,7,15,0,0,190,191,7,9,0,0,191,192,7,15,0,0,192,193,
7,0,0,0,193,194,7,0,0,0,194,52,1,0,0,0,195,196,7,9,0,0,196,197,7,3,0,0,
197,198,7,15,0,0,198,199,7,10,0,0,199,200,7,13,0,0,200,201,7,17,0,0,201,
54,1,0,0,0,202,203,7,3,0,0,203,204,7,8,0,0,204,205,7,15,0,0,205,206,7,13,
0,0,206,207,7,5,0,0,207,56,1,0,0,0,208,209,7,5,0,0,209,210,7,10,0,0,210,
211,7,19,0,0,211,218,7,3,0,0,212,213,7,20,0,0,213,214,7,15,0,0,214,215,
7,0,0,0,215,216,7,9,0,0,216,218,7,3,0,0,217,208,1,0,0,0,217,212,1,0,0,0,
218,58,1,0,0,0,219,220,7,21,0,0,220,60,1,0,0,0,221,223,3,59,29,0,222,221,
1,0,0,0,222,223,1,0,0,0,223,225,1,0,0,0,224,226,3,75,37,0,225,224,1,0,0,
0,226,227,1,0,0,0,227,225,1,0,0,0,227,228,1,0,0,0,228,236,1,0,0,0,229,233,
5,46,0,0,230,232,3,75,37,0,231,230,1,0,0,0,232,235,1,0,0,0,233,231,1,0,
0,0,233,234,1,0,0,0,234,237,1,0,0,0,235,233,1,0,0,0,236,229,1,0,0,0,236,
237,1,0,0,0,237,247,1,0,0,0,238,240,7,3,0,0,239,241,3,59,29,0,240,239,1,
0,0,0,240,241,1,0,0,0,241,243,1,0,0,0,242,244,3,75,37,0,243,242,1,0,0,0,
244,245,1,0,0,0,245,243,1,0,0,0,245,246,1,0,0,0,246,248,1,0,0,0,247,238,
1,0,0,0,247,248,1,0,0,0,248,270,1,0,0,0,249,251,3,59,29,0,250,249,1,0,0,
0,250,251,1,0,0,0,251,252,1,0,0,0,252,254,5,46,0,0,253,255,3,75,37,0,254,
253,1,0,0,0,255,256,1,0,0,0,256,254,1,0,0,0,256,257,1,0,0,0,257,267,1,0,
0,0,258,260,7,3,0,0,259,261,3,59,29,0,260,259,1,0,0,0,260,261,1,0,0,0,261,
263,1,0,0,0,262,264,3,75,37,0,263,262,1,0,0,0,264,265,1,0,0,0,265,263,1,
0,0,0,265,266,1,0,0,0,266,268,1,0,0,0,267,258,1,0,0,0,267,268,1,0,0,0,268,
270,1,0,0,0,269,222,1,0,0,0,269,250,1,0,0,0,270,62,1,0,0,0,271,277,5,34,
0,0,272,276,8,22,0,0,273,274,5,92,0,0,274,276,9,0,0,0,275,272,1,0,0,0,275,
273,1,0,0,0,276,279,1,0,0,0,277,275,1,0,0,0,277,278,1,0,0,0,278,280,1,0,
0,0,279,277,1,0,0,0,280,292,5,34,0,0,281,287,5,39,0,0,282,286,8,23,0,0,
283,284,5,92,0,0,284,286,9,0,0,0,285,282,1,0,0,0,285,283,1,0,0,0,286,289,
1,0,0,0,287,285,1,0,0,0,287,288,1,0,0,0,288,290,1,0,0,0,289,287,1,0,0,0,
290,292,5,39,0,0,291,271,1,0,0,0,291,281,1,0,0,0,292,64,1,0,0,0,293,297,
7,24,0,0,294,296,7,25,0,0,295,294,1,0,0,0,296,299,1,0,0,0,297,295,1,0,0,
0,297,298,1,0,0,0,298,66,1,0,0,0,299,297,1,0,0,0,300,301,5,91,0,0,301,302,
5,93,0,0,302,68,1,0,0,0,303,304,5,91,0,0,304,305,5,42,0,0,305,306,5,93,
0,0,306,70,1,0,0,0,307,320,3,65,32,0,308,309,5,46,0,0,309,319,3,65,32,0,
310,319,3,67,33,0,311,319,3,69,34,0,312,314,5,46,0,0,313,315,3,75,37,0,
314,313,1,0,0,0,315,316,1,0,0,0,316,314,1,0,0,0,316,317,1,0,0,0,317,319,
1,0,0,0,318,308,1,0,0,0,318,310,1,0,0,0,318,311,1,0,0,0,318,312,1,0,0,0,
319,322,1,0,0,0,320,318,1,0,0,0,320,321,1,0,0,0,321,72,1,0,0,0,322,320,
1,0,0,0,323,325,7,26,0,0,324,323,1,0,0,0,325,326,1,0,0,0,326,324,1,0,0,
0,326,327,1,0,0,0,327,328,1,0,0,0,328,329,6,36,0,0,329,74,1,0,0,0,330,331,
7,27,0,0,331,76,1,0,0,0,332,334,8,28,0,0,333,332,1,0,0,0,334,335,1,0,0,
0,335,333,1,0,0,0,335,336,1,0,0,0,336,78,1,0,0,0,29,0,92,135,152,217,222,
227,233,236,240,245,247,250,256,260,265,267,269,275,277,285,287,291,297,
316,318,320,326,335,1,6,0,0];
31,7,31,2,32,7,32,2,33,7,33,2,34,7,34,2,35,7,35,2,36,7,36,1,0,1,0,1,1,1,
1,1,2,1,2,1,3,1,3,1,4,1,4,1,5,1,5,1,5,3,5,89,8,5,1,6,1,6,1,6,1,7,1,7,1,
7,1,8,1,8,1,9,1,9,1,9,1,10,1,10,1,11,1,11,1,11,1,12,1,12,1,12,1,12,1,12,
1,13,1,13,1,13,1,13,1,13,1,13,1,14,1,14,1,14,1,14,1,14,1,14,1,14,1,14,1,
15,1,15,1,15,1,15,1,15,1,15,3,15,132,8,15,1,16,1,16,1,16,1,16,1,16,1,16,
1,16,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,3,17,149,8,17,1,18,1,18,1,
18,1,19,1,19,1,19,1,19,1,20,1,20,1,20,1,20,1,21,1,21,1,21,1,22,1,22,1,22,
1,22,1,22,1,22,1,22,1,22,1,22,1,23,1,23,1,23,1,23,1,24,1,24,1,24,1,24,1,
24,1,24,1,24,1,25,1,25,1,25,1,25,1,25,1,25,1,25,1,26,1,26,1,26,1,26,1,26,
1,26,1,26,1,26,1,26,3,26,201,8,26,1,27,1,27,1,28,3,28,206,8,28,1,28,4,28,
209,8,28,11,28,12,28,210,1,28,1,28,5,28,215,8,28,10,28,12,28,218,9,28,3,
28,220,8,28,1,28,1,28,3,28,224,8,28,1,28,4,28,227,8,28,11,28,12,28,228,
3,28,231,8,28,1,28,3,28,234,8,28,1,28,1,28,4,28,238,8,28,11,28,12,28,239,
1,28,1,28,3,28,244,8,28,1,28,4,28,247,8,28,11,28,12,28,248,3,28,251,8,28,
3,28,253,8,28,1,29,1,29,1,29,1,29,5,29,259,8,29,10,29,12,29,262,9,29,1,
29,1,29,1,29,1,29,1,29,5,29,269,8,29,10,29,12,29,272,9,29,1,29,3,29,275,
8,29,1,30,1,30,5,30,279,8,30,10,30,12,30,282,9,30,1,31,1,31,1,31,1,32,1,
32,1,32,1,32,1,33,1,33,1,33,1,33,1,33,1,33,1,33,4,33,298,8,33,11,33,12,
33,299,5,33,302,8,33,10,33,12,33,305,9,33,1,34,4,34,308,8,34,11,34,12,34,
309,1,34,1,34,1,35,1,35,1,36,4,36,317,8,36,11,36,12,36,318,0,0,37,1,1,3,
2,5,3,7,4,9,5,11,6,13,7,15,8,17,9,19,10,21,11,23,12,25,13,27,14,29,15,31,
16,33,17,35,18,37,19,39,20,41,21,43,22,45,23,47,24,49,25,51,26,53,27,55,
0,57,28,59,29,61,0,63,0,65,0,67,30,69,31,71,0,73,32,1,0,29,2,0,76,76,108,
108,2,0,73,73,105,105,2,0,75,75,107,107,2,0,69,69,101,101,2,0,66,66,98,
98,2,0,84,84,116,116,2,0,87,87,119,119,2,0,78,78,110,110,2,0,88,88,120,
120,2,0,83,83,115,115,2,0,82,82,114,114,2,0,71,71,103,103,2,0,80,80,112,
112,2,0,67,67,99,99,2,0,79,79,111,111,2,0,65,65,97,97,2,0,68,68,100,100,
2,0,72,72,104,104,2,0,89,89,121,121,2,0,85,85,117,117,2,0,70,70,102,102,
2,0,43,43,45,45,2,0,34,34,92,92,2,0,39,39,92,92,4,0,35,36,64,90,95,95,97,
123,7,0,35,36,45,45,47,58,64,90,95,95,97,123,125,125,3,0,9,10,13,13,32,
32,1,0,48,57,8,0,9,10,13,13,32,34,39,41,44,44,60,62,91,91,93,93,344,0,1,
1,0,0,0,0,3,1,0,0,0,0,5,1,0,0,0,0,7,1,0,0,0,0,9,1,0,0,0,0,11,1,0,0,0,0,
13,1,0,0,0,0,15,1,0,0,0,0,17,1,0,0,0,0,19,1,0,0,0,0,21,1,0,0,0,0,23,1,0,
0,0,0,25,1,0,0,0,0,27,1,0,0,0,0,29,1,0,0,0,0,31,1,0,0,0,0,33,1,0,0,0,0,
35,1,0,0,0,0,37,1,0,0,0,0,39,1,0,0,0,0,41,1,0,0,0,0,43,1,0,0,0,0,45,1,0,
0,0,0,47,1,0,0,0,0,49,1,0,0,0,0,51,1,0,0,0,0,53,1,0,0,0,0,57,1,0,0,0,0,
59,1,0,0,0,0,67,1,0,0,0,0,69,1,0,0,0,0,73,1,0,0,0,1,75,1,0,0,0,3,77,1,0,
0,0,5,79,1,0,0,0,7,81,1,0,0,0,9,83,1,0,0,0,11,88,1,0,0,0,13,90,1,0,0,0,
15,93,1,0,0,0,17,96,1,0,0,0,19,98,1,0,0,0,21,101,1,0,0,0,23,103,1,0,0,0,
25,106,1,0,0,0,27,111,1,0,0,0,29,117,1,0,0,0,31,125,1,0,0,0,33,133,1,0,
0,0,35,140,1,0,0,0,37,150,1,0,0,0,39,153,1,0,0,0,41,157,1,0,0,0,43,161,
1,0,0,0,45,164,1,0,0,0,47,173,1,0,0,0,49,177,1,0,0,0,51,184,1,0,0,0,53,
200,1,0,0,0,55,202,1,0,0,0,57,252,1,0,0,0,59,274,1,0,0,0,61,276,1,0,0,0,
63,283,1,0,0,0,65,286,1,0,0,0,67,290,1,0,0,0,69,307,1,0,0,0,71,313,1,0,
0,0,73,316,1,0,0,0,75,76,5,40,0,0,76,2,1,0,0,0,77,78,5,41,0,0,78,4,1,0,
0,0,79,80,5,91,0,0,80,6,1,0,0,0,81,82,5,93,0,0,82,8,1,0,0,0,83,84,5,44,
0,0,84,10,1,0,0,0,85,89,5,61,0,0,86,87,5,61,0,0,87,89,5,61,0,0,88,85,1,
0,0,0,88,86,1,0,0,0,89,12,1,0,0,0,90,91,5,33,0,0,91,92,5,61,0,0,92,14,1,
0,0,0,93,94,5,60,0,0,94,95,5,62,0,0,95,16,1,0,0,0,96,97,5,60,0,0,97,18,
1,0,0,0,98,99,5,60,0,0,99,100,5,61,0,0,100,20,1,0,0,0,101,102,5,62,0,0,
102,22,1,0,0,0,103,104,5,62,0,0,104,105,5,61,0,0,105,24,1,0,0,0,106,107,
7,0,0,0,107,108,7,1,0,0,108,109,7,2,0,0,109,110,7,3,0,0,110,26,1,0,0,0,
111,112,7,1,0,0,112,113,7,0,0,0,113,114,7,1,0,0,114,115,7,2,0,0,115,116,
7,3,0,0,116,28,1,0,0,0,117,118,7,4,0,0,118,119,7,3,0,0,119,120,7,5,0,0,
120,121,7,6,0,0,121,122,7,3,0,0,122,123,7,3,0,0,123,124,7,7,0,0,124,30,
1,0,0,0,125,126,7,3,0,0,126,127,7,8,0,0,127,128,7,1,0,0,128,129,7,9,0,0,
129,131,7,5,0,0,130,132,7,9,0,0,131,130,1,0,0,0,131,132,1,0,0,0,132,32,
1,0,0,0,133,134,7,10,0,0,134,135,7,3,0,0,135,136,7,11,0,0,136,137,7,3,0,
0,137,138,7,8,0,0,138,139,7,12,0,0,139,34,1,0,0,0,140,141,7,13,0,0,141,
142,7,14,0,0,142,143,7,7,0,0,143,144,7,5,0,0,144,145,7,15,0,0,145,146,7,
1,0,0,146,148,7,7,0,0,147,149,7,9,0,0,148,147,1,0,0,0,148,149,1,0,0,0,149,
36,1,0,0,0,150,151,7,1,0,0,151,152,7,7,0,0,152,38,1,0,0,0,153,154,7,7,0,
0,154,155,7,14,0,0,155,156,7,5,0,0,156,40,1,0,0,0,157,158,7,15,0,0,158,
159,7,7,0,0,159,160,7,16,0,0,160,42,1,0,0,0,161,162,7,14,0,0,162,163,7,
10,0,0,163,44,1,0,0,0,164,165,7,17,0,0,165,166,7,15,0,0,166,167,7,9,0,0,
167,168,7,5,0,0,168,169,7,14,0,0,169,170,7,2,0,0,170,171,7,3,0,0,171,172,
7,7,0,0,172,46,1,0,0,0,173,174,7,17,0,0,174,175,7,15,0,0,175,176,7,9,0,
0,176,48,1,0,0,0,177,178,7,17,0,0,178,179,7,15,0,0,179,180,7,9,0,0,180,
181,7,15,0,0,181,182,7,7,0,0,182,183,7,18,0,0,183,50,1,0,0,0,184,185,7,
17,0,0,185,186,7,15,0,0,186,187,7,9,0,0,187,188,7,15,0,0,188,189,7,0,0,
0,189,190,7,0,0,0,190,52,1,0,0,0,191,192,7,5,0,0,192,193,7,10,0,0,193,194,
7,19,0,0,194,201,7,3,0,0,195,196,7,20,0,0,196,197,7,15,0,0,197,198,7,0,
0,0,198,199,7,9,0,0,199,201,7,3,0,0,200,191,1,0,0,0,200,195,1,0,0,0,201,
54,1,0,0,0,202,203,7,21,0,0,203,56,1,0,0,0,204,206,3,55,27,0,205,204,1,
0,0,0,205,206,1,0,0,0,206,208,1,0,0,0,207,209,3,71,35,0,208,207,1,0,0,0,
209,210,1,0,0,0,210,208,1,0,0,0,210,211,1,0,0,0,211,219,1,0,0,0,212,216,
5,46,0,0,213,215,3,71,35,0,214,213,1,0,0,0,215,218,1,0,0,0,216,214,1,0,
0,0,216,217,1,0,0,0,217,220,1,0,0,0,218,216,1,0,0,0,219,212,1,0,0,0,219,
220,1,0,0,0,220,230,1,0,0,0,221,223,7,3,0,0,222,224,3,55,27,0,223,222,1,
0,0,0,223,224,1,0,0,0,224,226,1,0,0,0,225,227,3,71,35,0,226,225,1,0,0,0,
227,228,1,0,0,0,228,226,1,0,0,0,228,229,1,0,0,0,229,231,1,0,0,0,230,221,
1,0,0,0,230,231,1,0,0,0,231,253,1,0,0,0,232,234,3,55,27,0,233,232,1,0,0,
0,233,234,1,0,0,0,234,235,1,0,0,0,235,237,5,46,0,0,236,238,3,71,35,0,237,
236,1,0,0,0,238,239,1,0,0,0,239,237,1,0,0,0,239,240,1,0,0,0,240,250,1,0,
0,0,241,243,7,3,0,0,242,244,3,55,27,0,243,242,1,0,0,0,243,244,1,0,0,0,244,
246,1,0,0,0,245,247,3,71,35,0,246,245,1,0,0,0,247,248,1,0,0,0,248,246,1,
0,0,0,248,249,1,0,0,0,249,251,1,0,0,0,250,241,1,0,0,0,250,251,1,0,0,0,251,
253,1,0,0,0,252,205,1,0,0,0,252,233,1,0,0,0,253,58,1,0,0,0,254,260,5,34,
0,0,255,259,8,22,0,0,256,257,5,92,0,0,257,259,9,0,0,0,258,255,1,0,0,0,258,
256,1,0,0,0,259,262,1,0,0,0,260,258,1,0,0,0,260,261,1,0,0,0,261,263,1,0,
0,0,262,260,1,0,0,0,263,275,5,34,0,0,264,270,5,39,0,0,265,269,8,23,0,0,
266,267,5,92,0,0,267,269,9,0,0,0,268,265,1,0,0,0,268,266,1,0,0,0,269,272,
1,0,0,0,270,268,1,0,0,0,270,271,1,0,0,0,271,273,1,0,0,0,272,270,1,0,0,0,
273,275,5,39,0,0,274,254,1,0,0,0,274,264,1,0,0,0,275,60,1,0,0,0,276,280,
7,24,0,0,277,279,7,25,0,0,278,277,1,0,0,0,279,282,1,0,0,0,280,278,1,0,0,
0,280,281,1,0,0,0,281,62,1,0,0,0,282,280,1,0,0,0,283,284,5,91,0,0,284,285,
5,93,0,0,285,64,1,0,0,0,286,287,5,91,0,0,287,288,5,42,0,0,288,289,5,93,
0,0,289,66,1,0,0,0,290,303,3,61,30,0,291,292,5,46,0,0,292,302,3,61,30,0,
293,302,3,63,31,0,294,302,3,65,32,0,295,297,5,46,0,0,296,298,3,71,35,0,
297,296,1,0,0,0,298,299,1,0,0,0,299,297,1,0,0,0,299,300,1,0,0,0,300,302,
1,0,0,0,301,291,1,0,0,0,301,293,1,0,0,0,301,294,1,0,0,0,301,295,1,0,0,0,
302,305,1,0,0,0,303,301,1,0,0,0,303,304,1,0,0,0,304,68,1,0,0,0,305,303,
1,0,0,0,306,308,7,26,0,0,307,306,1,0,0,0,308,309,1,0,0,0,309,307,1,0,0,
0,309,310,1,0,0,0,310,311,1,0,0,0,311,312,6,34,0,0,312,70,1,0,0,0,313,314,
7,27,0,0,314,72,1,0,0,0,315,317,8,28,0,0,316,315,1,0,0,0,317,318,1,0,0,
0,318,316,1,0,0,0,318,319,1,0,0,0,319,74,1,0,0,0,29,0,88,131,148,200,205,
210,216,219,223,228,230,233,239,243,248,250,252,258,260,268,270,274,280,
299,301,303,309,318,1,6,0,0];
private static __ATN: ATN;
public static get _ATN(): ATN {
@@ -233,4 +225,4 @@ export default class FilterQueryLexer extends Lexer {
static DecisionsToDFA = FilterQueryLexer._ATN.decisionToState.map( (ds: DecisionState, index: number) => new DFA(ds, index) );
}
}

View File

@@ -1,28 +1,25 @@
// Generated from grammar/FilterQuery.g4 by ANTLR 4.13.2
// Generated from FilterQuery.g4 by ANTLR 4.13.1
import {ParseTreeListener} from "antlr4";
import { QueryContext } from "./FilterQueryParser.js";
import { ExpressionContext } from "./FilterQueryParser.js";
import { OrExpressionContext } from "./FilterQueryParser.js";
import { AndExpressionContext } from "./FilterQueryParser.js";
import { UnaryExpressionContext } from "./FilterQueryParser.js";
import { PrimaryContext } from "./FilterQueryParser.js";
import { ComparisonContext } from "./FilterQueryParser.js";
import { InClauseContext } from "./FilterQueryParser.js";
import { NotInClauseContext } from "./FilterQueryParser.js";
import { ValueListContext } from "./FilterQueryParser.js";
import { FullTextContext } from "./FilterQueryParser.js";
import { FunctionCallContext } from "./FilterQueryParser.js";
import { SearchCallContext } from "./FilterQueryParser.js";
import { FunctionParamListContext } from "./FilterQueryParser.js";
import { FunctionParamContext } from "./FilterQueryParser.js";
import { ArrayContext } from "./FilterQueryParser.js";
import { ValueContext } from "./FilterQueryParser.js";
import { KeyContext } from "./FilterQueryParser.js";
import { FieldContext } from "./FilterQueryParser.js";
import { ExactCallContext } from "./FilterQueryParser.js";
import { QueryContext } from "./FilterQueryParser";
import { ExpressionContext } from "./FilterQueryParser";
import { OrExpressionContext } from "./FilterQueryParser";
import { AndExpressionContext } from "./FilterQueryParser";
import { UnaryExpressionContext } from "./FilterQueryParser";
import { PrimaryContext } from "./FilterQueryParser";
import { ComparisonContext } from "./FilterQueryParser";
import { InClauseContext } from "./FilterQueryParser";
import { NotInClauseContext } from "./FilterQueryParser";
import { ValueListContext } from "./FilterQueryParser";
import { FullTextContext } from "./FilterQueryParser";
import { FunctionCallContext } from "./FilterQueryParser";
import { FunctionParamListContext } from "./FilterQueryParser";
import { FunctionParamContext } from "./FilterQueryParser";
import { ArrayContext } from "./FilterQueryParser";
import { ValueContext } from "./FilterQueryParser";
import { KeyContext } from "./FilterQueryParser";
/**
@@ -150,16 +147,6 @@ export default class FilterQueryListener extends ParseTreeListener {
* @param ctx the parse tree
*/
exitFunctionCall?: (ctx: FunctionCallContext) => void;
/**
* Enter a parse tree produced by `FilterQueryParser.searchCall`.
* @param ctx the parse tree
*/
enterSearchCall?: (ctx: SearchCallContext) => void;
/**
* Exit a parse tree produced by `FilterQueryParser.searchCall`.
* @param ctx the parse tree
*/
exitSearchCall?: (ctx: SearchCallContext) => void;
/**
* Enter a parse tree produced by `FilterQueryParser.functionParamList`.
* @param ctx the parse tree
@@ -210,25 +197,5 @@ export default class FilterQueryListener extends ParseTreeListener {
* @param ctx the parse tree
*/
exitKey?: (ctx: KeyContext) => void;
/**
* Enter a parse tree produced by `FilterQueryParser.field`.
* @param ctx the parse tree
*/
enterField?: (ctx: FieldContext) => void;
/**
* Exit a parse tree produced by `FilterQueryParser.field`.
* @param ctx the parse tree
*/
exitField?: (ctx: FieldContext) => void;
/**
* Enter a parse tree produced by `FilterQueryParser.exactCall`.
* @param ctx the parse tree
*/
enterExactCall?: (ctx: ExactCallContext) => void;
/**
* Exit a parse tree produced by `FilterQueryParser.exactCall`.
* @param ctx the parse tree
*/
exitExactCall?: (ctx: ExactCallContext) => void;
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,28 +1,25 @@
// Generated from grammar/FilterQuery.g4 by ANTLR 4.13.2
// Generated from FilterQuery.g4 by ANTLR 4.13.1
import {ParseTreeVisitor} from 'antlr4';
import { QueryContext } from "./FilterQueryParser.js";
import { ExpressionContext } from "./FilterQueryParser.js";
import { OrExpressionContext } from "./FilterQueryParser.js";
import { AndExpressionContext } from "./FilterQueryParser.js";
import { UnaryExpressionContext } from "./FilterQueryParser.js";
import { PrimaryContext } from "./FilterQueryParser.js";
import { ComparisonContext } from "./FilterQueryParser.js";
import { InClauseContext } from "./FilterQueryParser.js";
import { NotInClauseContext } from "./FilterQueryParser.js";
import { ValueListContext } from "./FilterQueryParser.js";
import { FullTextContext } from "./FilterQueryParser.js";
import { FunctionCallContext } from "./FilterQueryParser.js";
import { SearchCallContext } from "./FilterQueryParser.js";
import { FunctionParamListContext } from "./FilterQueryParser.js";
import { FunctionParamContext } from "./FilterQueryParser.js";
import { ArrayContext } from "./FilterQueryParser.js";
import { ValueContext } from "./FilterQueryParser.js";
import { KeyContext } from "./FilterQueryParser.js";
import { FieldContext } from "./FilterQueryParser.js";
import { ExactCallContext } from "./FilterQueryParser.js";
import { QueryContext } from "./FilterQueryParser";
import { ExpressionContext } from "./FilterQueryParser";
import { OrExpressionContext } from "./FilterQueryParser";
import { AndExpressionContext } from "./FilterQueryParser";
import { UnaryExpressionContext } from "./FilterQueryParser";
import { PrimaryContext } from "./FilterQueryParser";
import { ComparisonContext } from "./FilterQueryParser";
import { InClauseContext } from "./FilterQueryParser";
import { NotInClauseContext } from "./FilterQueryParser";
import { ValueListContext } from "./FilterQueryParser";
import { FullTextContext } from "./FilterQueryParser";
import { FunctionCallContext } from "./FilterQueryParser";
import { FunctionParamListContext } from "./FilterQueryParser";
import { FunctionParamContext } from "./FilterQueryParser";
import { ArrayContext } from "./FilterQueryParser";
import { ValueContext } from "./FilterQueryParser";
import { KeyContext } from "./FilterQueryParser";
/**
@@ -105,12 +102,6 @@ export default class FilterQueryVisitor<Result> extends ParseTreeVisitor<Result>
* @return the visitor result
*/
visitFunctionCall?: (ctx: FunctionCallContext) => Result;
/**
* Visit a parse tree produced by `FilterQueryParser.searchCall`.
* @param ctx the parse tree
* @return the visitor result
*/
visitSearchCall?: (ctx: SearchCallContext) => Result;
/**
* Visit a parse tree produced by `FilterQueryParser.functionParamList`.
* @param ctx the parse tree
@@ -141,17 +132,5 @@ export default class FilterQueryVisitor<Result> extends ParseTreeVisitor<Result>
* @return the visitor result
*/
visitKey?: (ctx: KeyContext) => Result;
/**
* Visit a parse tree produced by `FilterQueryParser.field`.
* @param ctx the parse tree
* @return the visitor result
*/
visitField?: (ctx: FieldContext) => Result;
/**
* Visit a parse tree produced by `FilterQueryParser.exactCall`.
* @param ctx the parse tree
* @return the visitor result
*/
visitExactCall?: (ctx: ExactCallContext) => Result;
}

View File

@@ -223,12 +223,6 @@
padding-left: 6px !important;
}
&__label {
display: inline-flex;
align-items: baseline;
gap: 6px;
}
&__pinned-icon {
flex-shrink: 0;
color: var(--text-robin-400);

View File

@@ -67,7 +67,6 @@ export interface PrettyViewProps {
*/
pinnedFieldsValue?: string[];
onPinnedFieldsChange?: (next: string[]) => void;
labelSuffixRenderer?: (fieldKey: string) => React.ReactNode;
}
function PrettyView({
@@ -79,7 +78,6 @@ function PrettyView({
drawerKey = 'default',
pinnedFieldsValue,
onPinnedFieldsChange,
labelSuffixRenderer,
}: PrettyViewProps): JSX.Element {
const isDarkMode = useIsDarkMode();
const [, setCopy] = useCopyToClipboard();
@@ -307,24 +305,10 @@ function PrettyView({
}}
/>
<span>{displayKey}</span>
{labelSuffixRenderer?.(displayKey)}
</span>
);
},
[togglePin, pinnedEntries, labelSuffixRenderer],
);
const labelRenderer = useCallback(
(keyPath: KeyPath): React.ReactNode => {
const displayKey = String(keyPath[0]);
return (
<span className="pretty-view__label">
<span>{displayKey}</span>
{labelSuffixRenderer?.(displayKey)}
</span>
);
},
[labelSuffixRenderer],
[togglePin, pinnedEntries],
);
return (
@@ -367,7 +351,6 @@ function PrettyView({
shouldExpandNodeInitially={shouldExpandNodeInitially}
valueRenderer={valueRenderer}
getItemString={getItemString}
labelRenderer={labelRenderer}
/>
</div>
);

View File

@@ -1,14 +0,0 @@
export interface SemconvMigrationReportEntry {
current: string;
old: string;
signal: string;
services: string[];
resourceSets: number;
lastSeenUnixMilli: number;
}
export interface SemconvMigrationReport {
startUnixMilli: number;
endUnixMilli: number;
entries: SemconvMigrationReportEntry[];
}

View File

@@ -117,8 +117,6 @@ export type SpaceAggregation =
export type ColumnType = 'group' | 'aggregation';
export type FieldResolution = 'exact';
// ===================== Variable Types =====================
export type VariableType = 'query' | 'dynamic' | 'custom' | 'text';
@@ -138,7 +136,6 @@ export interface TelemetryFieldKey {
signal?: SignalType;
fieldContext?: FieldContext;
fieldDataType?: FieldDataType;
fieldResolution?: FieldResolution;
materialized?: boolean;
isIndexed?: boolean;
}

View File

@@ -1,40 +0,0 @@
import {
findOldSemconvNames,
getSemconvMembers,
getSemconvRename,
} from 'utils/semconv';
describe('semantic convention helpers', () => {
it('returns the current name for an old attribute', () => {
expect(getSemconvRename('deployment.environment')).toMatchObject({
old: 'deployment.environment',
current: 'deployment.environment.name',
});
});
it('finds old names in editor text without matching larger custom names', () => {
expect(
findOldSemconvNames(
"deployment.environment = 'prod' AND custom.db.system.value = 'x'",
),
).toStrictEqual([
expect.objectContaining({
old: 'deployment.environment',
current: 'deployment.environment.name',
}),
]);
});
it('does not warn for current names', () => {
expect(
findOldSemconvNames('deployment.environment.name = prod'),
).toStrictEqual([]);
});
it('returns current-first members for compatibility readers', () => {
expect(getSemconvMembers('http.request.method')).toStrictEqual([
'http.request.method',
'http.method',
]);
});
});

View File

@@ -16,9 +16,14 @@ export const lazyRetry = (componentImport: ComponentImport): Promise<any> =>
resolve(component);
})
.catch((error: Error) => {
if (!hasRefreshed) {
setSessionStorageApi(SESSIONSTORAGE.RETRY_LAZY_REFRESHED, 'true');
// A stale chunk reference right after a deploy self-heals: one reload pulls a
// fresh index.html with the new hashed asset names. That reload is only
// once-only if the flag persists, so a failed write (sessionStorage blocked in
// an iframe, storage disabled) must not reload at all — it would loop forever.
if (
!hasRefreshed &&
setSessionStorageApi(SESSIONSTORAGE.RETRY_LAZY_REFRESHED, 'true')
) {
window.location.reload();
}

View File

@@ -1,61 +0,0 @@
import {
SEMCONV_FAMILIES,
SemconvFamily,
} from 'constants/generated/semconvFamilies.gen';
export type SemconvRename = {
old: string;
current: string;
family: SemconvFamily;
};
const OLD_NAMES = SEMCONV_FAMILIES.flatMap((family) =>
family.old.map((old) => ({ old, current: family.current, family })),
);
const OLD_NAME_INDEX = new Map(OLD_NAMES.map((rename) => [rename.old, rename]));
const FAMILY_BY_NAME = new Map(
SEMCONV_FAMILIES.flatMap((family) =>
[family.current, ...family.old].map((name) => [name, family] as const),
),
);
export function getSemconvRename(name: string): SemconvRename | undefined {
return OLD_NAME_INDEX.get(name);
}
/** Returns the current name first, followed by every historical spelling. */
export function getSemconvMembers(name: string): readonly string[] {
const family = FAMILY_BY_NAME.get(name);
return family ? [family.current, ...family.old] : [name];
}
export function findOldSemconvNames(text: string): SemconvRename[] {
if (!text) {
return [];
}
return OLD_NAMES.filter(({ old }) => containsSemconvName(text, old));
}
function containsSemconvName(text: string, name: string): boolean {
let offset = 0;
while (offset < text.length) {
const index = text.indexOf(name, offset);
if (index === -1) {
return false;
}
const before = index === 0 ? '' : text[index - 1];
const afterIndex = index + name.length;
const after = afterIndex === text.length ? '' : text[afterIndex];
if (!isSemconvNameCharacter(before) && !isSemconvNameCharacter(after)) {
return true;
}
offset = index + 1;
}
return false;
}
function isSemconvNameCharacter(value: string): boolean {
return /[A-Za-z0-9_.-]/.test(value);
}

2
go.mod
View File

@@ -4,7 +4,7 @@ go 1.25.7
require (
dario.cat/mergo v1.0.2
github.com/AfterShip/clickhouse-sql-parser v0.5.4
github.com/AfterShip/clickhouse-sql-parser v0.5.5
github.com/ClickHouse/clickhouse-go/v2 v2.44.0
github.com/DATA-DOG/go-sqlmock v1.5.2
github.com/SigNoz/clickhouse-go-mock v0.14.0

4
go.sum
View File

@@ -66,8 +66,8 @@ dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
github.com/AfterShip/clickhouse-sql-parser v0.5.4 h1:yiCQaMq8EO+dpKdnpP9YYd/ne6MSuOXgsMsNL33NiTI=
github.com/AfterShip/clickhouse-sql-parser v0.5.4/go.mod h1:Qi3qvPTfZb/aFwI5V4WFOahgjsLJa4MzVijIAfwOhDw=
github.com/AfterShip/clickhouse-sql-parser v0.5.5 h1:LCA23yAA4GgF73PoYXb67yzCdC4sXsj4geQz1Oij3U8=
github.com/AfterShip/clickhouse-sql-parser v0.5.5/go.mod h1:Qi3qvPTfZb/aFwI5V4WFOahgjsLJa4MzVijIAfwOhDw=
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA=

View File

@@ -50,30 +50,30 @@ primary
* [NOT] BETWEEN, [NOT] IN, [NOT] EXISTS, [NOT] REGEXP, [NOT] CONTAINS, etc.
*/
comparison
: field EQUALS value
| field (NOT_EQUALS | NEQ) value
| field LT value
| field LE value
| field GT value
| field GE value
: key EQUALS value
| key (NOT_EQUALS | NEQ) value
| key LT value
| key LE value
| key GT value
| key GE value
| field (LIKE | ILIKE) value
| field NOT (LIKE | ILIKE) value
| key (LIKE | ILIKE) value
| key NOT (LIKE | ILIKE) value
| field BETWEEN value AND value
| field NOT BETWEEN value AND value
| key BETWEEN value AND value
| key NOT BETWEEN value AND value
| field inClause
| field notInClause
| key inClause
| key notInClause
| field EXISTS
| field NOT EXISTS
| key EXISTS
| key NOT EXISTS
| field REGEXP value
| field NOT REGEXP value
| key REGEXP value
| key NOT REGEXP value
| field CONTAINS value
| field NOT CONTAINS value
| key CONTAINS value
| key NOT CONTAINS value
;
// in(...) or in[...]
@@ -126,7 +126,7 @@ functionParamList
;
functionParam
: field
: key
| value
| array
;
@@ -155,17 +155,6 @@ key
: KEY
;
// exact(key) disables semantic-convention family resolution for this field.
// It is deliberately a field wrapper rather than a general function.
field
: key
| exactCall
;
exactCall
: EXACT LPAREN key RPAREN
;
/*
* Lexer Rules
@@ -206,7 +195,6 @@ HAS : [Hh][Aa][Ss] ;
HASANY : [Hh][Aa][Ss][Aa][Nn][Yy] ;
HASALL : [Hh][Aa][Ss][Aa][Ll][Ll] ;
SEARCH : [Ss][Ee][Aa][Rr][Cc][Hh] ;
EXACT : [Ee][Xx][Aa][Cc][Tt] ;
// Potential boolean constants
BOOL

View File

@@ -46,23 +46,5 @@ func (provider *provider) addFieldsRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/fields/semconv-migration", handler.New(provider.authzMiddleware.ViewAccess(provider.fieldsHandler.GetSemconvMigrationReport), handler.OpenAPIDef{
ID: "GetSemconvMigrationReport",
Tags: []string{"fields"},
Summary: "Get semantic-convention migration report",
Description: "Returns services that still emit old semantic-convention names without the current family name",
Request: nil,
RequestQuery: new(telemetrytypes.PostableSemconvMigrationReportParams),
RequestContentType: "",
Response: new(telemetrytypes.GettableSemconvMigrationReport),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
return nil
}

View File

@@ -51,9 +51,9 @@ func postableRuleExamples() []handler.OpenAPIExample {
"filter": map[string]any{"expression": "k8s.deployment.name = 'api-service'"},
"groupBy": []any{
map[string]any{"name": "k8s.pod.name", "fieldContext": "resource", "fieldDataType": "string"},
map[string]any{"name": "deployment.environment.name", "fieldContext": "resource", "fieldDataType": "string"},
map[string]any{"name": "deployment.environment", "fieldContext": "resource", "fieldDataType": "string"},
},
"legend": "{{k8s.pod.name}} ({{deployment.environment.name}})",
"legend": "{{k8s.pod.name}} ({{deployment.environment}})",
},
},
},
@@ -74,12 +74,12 @@ func postableRuleExamples() []handler.OpenAPIExample {
},
"evaluation": rolling("15m", "1m"),
"notificationSettings": map[string]any{
"groupBy": []any{"k8s.pod.name", "deployment.environment.name"},
"groupBy": []any{"k8s.pod.name", "deployment.environment"},
"renotify": renotify("4h", "firing"),
},
"labels": map[string]any{"severity": "critical", "team": "platform"},
"annotations": map[string]any{
"description": "Pod {{$k8s.pod.name}} CPU is at {{$value}} of request in {{$deployment.environment.name}}.",
"description": "Pod {{$k8s.pod.name}} CPU is at {{$value}} of request in {{$deployment.environment}}.",
"summary": "Pod CPU above {{$threshold}} of request",
},
},
@@ -170,7 +170,7 @@ func postableRuleExamples() []handler.OpenAPIExample {
{
Name: "metric_promql",
Summary: "Metric threshold PromQL rule",
Description: "PromQL expression instead of the builder. Dotted OTEL resource attributes are quoted (\"deployment.environment.name\"). Useful for queries that combine series with group_right or other Prom operators.",
Description: "PromQL expression instead of the builder. Dotted OTEL resource attributes are quoted (\"deployment.environment\"). Useful for queries that combine series with group_right or other Prom operators.",
Value: map[string]any{
"alert": "Kafka consumer group lag above 1000",
"alertType": "METRIC_BASED_ALERT",
@@ -187,7 +187,7 @@ func postableRuleExamples() []handler.OpenAPIExample {
"type": "promql",
"spec": map[string]any{
"name": "A",
"query": "(max by(topic, partition, \"deployment.environment.name\")(kafka_log_end_offset) - on(topic, partition, \"deployment.environment.name\") group_right max by(group, topic, partition, \"deployment.environment.name\")(kafka_consumer_committed_offset)) > 0",
"query": "(max by(topic, partition, \"deployment.environment\")(kafka_log_end_offset) - on(topic, partition, \"deployment.environment\") group_right max by(group, topic, partition, \"deployment.environment\")(kafka_consumer_committed_offset)) > 0",
"legend": "{{topic}}/{{partition}} ({{group}})",
},
},
@@ -299,9 +299,9 @@ func postableRuleExamples() []handler.OpenAPIExample {
"filter": map[string]any{"expression": "service.name = 'payments-api' AND severity_text = 'ERROR' AND body CONTAINS 'panic'"},
"groupBy": []any{
map[string]any{"name": "k8s.pod.name", "fieldContext": "resource", "fieldDataType": "string"},
map[string]any{"name": "deployment.environment.name", "fieldContext": "resource", "fieldDataType": "string"},
map[string]any{"name": "deployment.environment", "fieldContext": "resource", "fieldDataType": "string"},
},
"legend": "{{k8s.pod.name}} ({{deployment.environment.name}})",
"legend": "{{k8s.pod.name}} ({{deployment.environment}})",
},
},
},
@@ -322,12 +322,12 @@ func postableRuleExamples() []handler.OpenAPIExample {
},
"evaluation": rolling("5m", "1m"),
"notificationSettings": map[string]any{
"groupBy": []any{"k8s.pod.name", "deployment.environment.name"},
"groupBy": []any{"k8s.pod.name", "deployment.environment"},
"renotify": renotify("15m", "firing"),
},
"labels": map[string]any{"severity": "critical", "team": "payments"},
"annotations": map[string]any{
"description": "{{$k8s.pod.name}} emitted {{$value}} panic log(s) in {{$deployment.environment.name}}.",
"description": "{{$k8s.pod.name}} emitted {{$value}} panic log(s) in {{$deployment.environment}}.",
"summary": "Payments service panic",
},
},
@@ -358,7 +358,7 @@ func postableRuleExamples() []handler.OpenAPIExample {
"disabled": true,
"aggregations": []any{map[string]any{"expression": "count()"}},
"filter": map[string]any{"expression": "service.name = 'payments-api' AND severity_text IN ['ERROR', 'FATAL']"},
"groupBy": []any{map[string]any{"name": "deployment.environment.name", "fieldContext": "resource", "fieldDataType": "string"}},
"groupBy": []any{map[string]any{"name": "deployment.environment", "fieldContext": "resource", "fieldDataType": "string"}},
},
},
map[string]any{
@@ -370,7 +370,7 @@ func postableRuleExamples() []handler.OpenAPIExample {
"disabled": true,
"aggregations": []any{map[string]any{"expression": "count()"}},
"filter": map[string]any{"expression": "service.name = 'payments-api'"},
"groupBy": []any{map[string]any{"name": "deployment.environment.name", "fieldContext": "resource", "fieldDataType": "string"}},
"groupBy": []any{map[string]any{"name": "deployment.environment", "fieldContext": "resource", "fieldDataType": "string"}},
},
},
map[string]any{
@@ -378,7 +378,7 @@ func postableRuleExamples() []handler.OpenAPIExample {
"spec": map[string]any{
"name": "F1",
"expression": "(A / B) * 100",
"legend": "{{deployment.environment.name}}",
"legend": "{{deployment.environment}}",
},
},
},
@@ -399,12 +399,12 @@ func postableRuleExamples() []handler.OpenAPIExample {
},
"evaluation": rolling("5m", "1m"),
"notificationSettings": map[string]any{
"groupBy": []any{"deployment.environment.name"},
"groupBy": []any{"deployment.environment"},
"renotify": renotify("30m", "firing"),
},
"labels": map[string]any{"severity": "critical", "team": "payments"},
"annotations": map[string]any{
"description": "Error log rate in {{$deployment.environment.name}} is {{$value}}%",
"description": "Error log rate in {{$deployment.environment}} is {{$value}}%",
"summary": "Payments-api error rate above {{$threshold}}%",
},
},
@@ -669,10 +669,10 @@ func postableRuleExamples() []handler.OpenAPIExample {
"stepInterval": 60,
"disabled": true,
"aggregations": []any{map[string]any{"expression": "count()"}},
"filter": map[string]any{"expression": "service.name CONTAINS 'api' AND http.response.status_code >= 500"},
"filter": map[string]any{"expression": "service.name CONTAINS 'api' AND http.status_code >= 500"},
"groupBy": []any{
map[string]any{"name": "service.name", "fieldContext": "resource", "fieldDataType": "string"},
map[string]any{"name": "deployment.environment.name", "fieldContext": "resource", "fieldDataType": "string"},
map[string]any{"name": "deployment.environment", "fieldContext": "resource", "fieldDataType": "string"},
},
},
},
@@ -687,7 +687,7 @@ func postableRuleExamples() []handler.OpenAPIExample {
"filter": map[string]any{"expression": "service.name CONTAINS 'api'"},
"groupBy": []any{
map[string]any{"name": "service.name", "fieldContext": "resource", "fieldDataType": "string"},
map[string]any{"name": "deployment.environment.name", "fieldContext": "resource", "fieldDataType": "string"},
map[string]any{"name": "deployment.environment", "fieldContext": "resource", "fieldDataType": "string"},
},
},
},
@@ -696,7 +696,7 @@ func postableRuleExamples() []handler.OpenAPIExample {
"spec": map[string]any{
"name": "F1",
"expression": "(A / B) * 100",
"legend": "{{service.name}} ({{deployment.environment.name}})",
"legend": "{{service.name}} ({{deployment.environment}})",
},
},
},
@@ -717,14 +717,14 @@ func postableRuleExamples() []handler.OpenAPIExample {
},
"evaluation": rolling("5m", "1m"),
"notificationSettings": map[string]any{
"groupBy": []any{"service.name", "deployment.environment.name"},
"groupBy": []any{"service.name", "deployment.environment"},
"newGroupEvalDelay": "2m",
"usePolicy": false,
"renotify": renotify("30m", "firing", "nodata"),
},
"labels": map[string]any{"team": "platform"},
"annotations": map[string]any{
"description": "{{$service.name}} 5xx rate in {{$deployment.environment.name}} is {{$value}}%.",
"description": "{{$service.name}} 5xx rate in {{$deployment.environment}} is {{$value}}%.",
"summary": "API service error rate elevated",
},
},

View File

@@ -6,7 +6,6 @@ import (
"strings"
parser "github.com/SigNoz/signoz/pkg/parser/filterquery/grammar"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/antlr4-go/antlr/v4"
"golang.org/x/exp/maps"
@@ -188,35 +187,25 @@ func (r *WhereClauseRewriter) VisitPrimary(ctx *parser.PrimaryContext) any {
// VisitComparison visits comparison expressions.
func (r *WhereClauseRewriter) VisitComparison(ctx *parser.ComparisonContext) any {
if ctx.Field() == nil {
if ctx.Key() == nil {
return nil
}
field := ctx.Field().GetText()
key := field
if exactCall := ctx.Field().ExactCall(); exactCall != nil {
key = exactCall.Key().GetText()
}
key := ctx.Key().GetText()
r.keysSeen[key] = struct{}{}
parsedKey := telemetrytypes.GetFieldKeyFromKeyText(key)
r.keysSeen[parsedKey.Name] = struct{}{}
labelKey := key
if _, exists := r.labels[labelKey]; !exists {
labelKey = parsedKey.Name
}
// Check if this key is in the labels and was part of group by
if value, exists := r.labels[labelKey]; exists {
if _, partOfGroup := r.groupBySet[labelKey]; partOfGroup {
if value, exists := r.labels[key]; exists {
if _, partOfGroup := r.groupBySet[key]; partOfGroup {
// Case 1: Replace with actual value
escapedValue := escapeValueIfNeeded(value)
fmt.Fprintf(&r.rewritten, "%s=%s", field, escapedValue)
fmt.Fprintf(&r.rewritten, "%s=%s", key, escapedValue)
return nil
}
}
// Otherwise, keep the original comparison
r.rewritten.WriteString(field)
r.rewritten.WriteString(key)
if ctx.EQUALS() != nil {
r.rewritten.WriteString("=")
@@ -419,8 +408,8 @@ func (r *WhereClauseRewriter) VisitFunctionParamList(ctx *parser.FunctionParamLi
// VisitFunctionParam visits function parameters.
func (r *WhereClauseRewriter) VisitFunctionParam(ctx *parser.FunctionParamContext) any {
if ctx.Field() != nil {
r.rewritten.WriteString(ctx.Field().GetText())
if ctx.Key() != nil {
ctx.Key().Accept(r)
} else if ctx.Value() != nil {
ctx.Value().Accept(r)
} else if ctx.Array() != nil {

View File

@@ -234,18 +234,6 @@ func TestPrepareFiltersV5(t *testing.T) {
expected: "(error_details EXISTS) AND service.name='serviceA'",
description: "Should preserve EXISTS operator",
},
{
name: "exact_field_label_replacement",
labels: map[string]string{
"deployment.environment": "production",
},
whereClause: "exact(resource.deployment.environment) = 'staging'",
groupByItems: []qbtypes.GroupByKey{
{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "deployment.environment"}},
},
expected: "exact(resource.deployment.environment)='production'",
description: "Should keep the exact wrapper when replacing a grouped label",
},
{
name: "empty_where_clause_with_labels",

View File

@@ -23,13 +23,13 @@ func TestSource(t *testing.T) {
err := json.Unmarshal(buf.Bytes(), &m)
require.NoError(t, err)
assert.Contains(t, m, "code.file.path")
assert.Contains(t, m, "code.function.name")
assert.Contains(t, m, "code.line.number")
assert.Contains(t, m, "code.filepath")
assert.Contains(t, m, "code.function")
assert.Contains(t, m, "code.lineno")
assert.Contains(t, m["code.file.path"], "source_test.go")
assert.Contains(t, m["code.function.name"], "TestSource")
assert.NotZero(t, m["code.line.number"])
assert.Contains(t, m["code.filepath"], "source_test.go")
assert.Contains(t, m["code.function"], "TestSource")
assert.NotZero(t, m["code.lineno"])
// Ensure the nested "source" key is not present.
assert.NotContains(t, m, "source")

View File

@@ -136,12 +136,7 @@ func (v *visitor) VisitPrimary(ctx *grammar.PrimaryContext) any {
// predicate; any other identifier is treated as a tag key — the operator
// applies to the tag's value, with a case-insensitive match on the tag's key.
func (v *visitor) VisitComparison(ctx *grammar.ComparisonContext) any {
field := ctx.Field()
keyText := field.GetText()
if exactCall := field.ExactCall(); exactCall != nil {
keyText = exactCall.Key().GetText()
}
key := strings.ToLower(strings.TrimSpace(keyText))
key := strings.ToLower(strings.TrimSpace(ctx.Key().GetText()))
operation, ok := v.extractOperation(ctx)
if !ok {
@@ -432,7 +427,7 @@ func (v *visitor) buildFreeTextTerm(value string) string {
}
// buildFreeTextContains emits a case-insensitive contains as
// LOWER(COALESCE(col, )) LIKE LOWER(?), identical on SQLite and Postgres.
// LOWER(COALESCE(col, '')) LIKE LOWER(?), identical on SQLite and Postgres.
// COALESCE keeps a NULL column (an absent description) false rather than NULL —
// otherwise `NOT (…)` goes NULL and drops every description-less dashboard. The
// value's % and _ are escaped, and ESCAPE pins backslash as the escape char.

View File

@@ -8,7 +8,4 @@ type Handler interface {
// Gets the fields values for the given field value selector
GetFieldsValues(http.ResponseWriter, *http.Request)
// Gets services that still emit only historical semantic-convention names.
GetSemconvMigrationReport(http.ResponseWriter, *http.Request)
}

View File

@@ -2,7 +2,6 @@ package implfields
import (
"net/http"
"time"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/http/binding"
@@ -17,43 +16,6 @@ type handler struct {
telemetryMetadataStore telemetrytypes.MetadataStore
}
func (handler *handler) GetSemconvMigrationReport(rw http.ResponseWriter, req *http.Request) {
ctx := req.Context()
var params telemetrytypes.PostableSemconvMigrationReportParams
if err := binding.Query.BindQuery(req.URL.Query(), &params); err != nil {
render.Error(rw, err)
return
}
now := time.Now()
if params.EndUnixMilli == 0 {
params.EndUnixMilli = now.UnixMilli()
}
if params.StartUnixMilli == 0 {
params.StartUnixMilli = now.Add(-24 * time.Hour).UnixMilli()
}
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
report, err := handler.telemetryMetadataStore.GetSemconvMigrationReport(
ctx,
valuer.MustNewUUID(claims.OrgID),
params.StartUnixMilli,
params.EndUnixMilli,
)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusOK, report)
}
func NewHandler(settings factory.ProviderSettings, telemetryMetadataStore telemetrytypes.MetadataStore) fields.Handler {
return &handler{
telemetryMetadataStore: telemetryMetadataStore,

View File

@@ -1,65 +0,0 @@
package metricreductionrule
import (
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/semconv"
"github.com/SigNoz/signoz/pkg/types/metricreductionruletypes"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
)
var protectedLabels = buildProtectedLabels()
// ValidatePostableReductionRule validates request structure and protected-label
// policy before a rule reaches storage.
func ValidatePostableReductionRule(req *metricreductionruletypes.PostableReductionRule) error {
if err := req.Validate(); err != nil {
return err
}
return validateProtectedLabels(req.MatchType, req.Labels)
}
// ValidateUpdatableReductionRule validates request structure and
// protected-label policy before a rule reaches storage.
func ValidateUpdatableReductionRule(req *metricreductionruletypes.UpdatableReductionRule) error {
if err := req.Validate(); err != nil {
return err
}
return validateProtectedLabels(req.MatchType, req.Labels)
}
// IsProtectedLabel reports whether metric reduction must always retain label.
func IsProtectedLabel(label string) bool {
_, ok := protectedLabels[label]
return ok
}
func buildProtectedLabels() map[string]struct{} {
labels := map[string]struct{}{
"le": {},
"quantile": {},
"__name__": {},
"__temporality__": {},
}
selector := telemetrytypes.FieldKeySelector{
Name: "deployment.environment.name",
Signal: telemetrytypes.SignalMetrics,
FieldContext: telemetrytypes.FieldContextResource,
}
for _, member := range semconv.Members(semconv.KindAttribute, selector) {
labels[member] = struct{}{}
}
return labels
}
func validateProtectedLabels(matchType metricreductionruletypes.MatchType, labels []string) error {
if matchType != metricreductionruletypes.MatchTypeDrop {
return nil
}
for _, label := range labels {
if IsProtectedLabel(label) {
return errors.Newf(errors.TypeInvalidInput, metricreductionruletypes.ErrCodeMetricReductionRuleProtectedLabel,
"label %q is protected and cannot be dropped", label)
}
}
return nil
}

View File

@@ -1,64 +0,0 @@
package metricreductionrule_test
import (
"testing"
"github.com/SigNoz/signoz/pkg/modules/metricreductionrule"
"github.com/SigNoz/signoz/pkg/types/metricreductionruletypes"
"github.com/stretchr/testify/assert"
)
func TestDropRuleRejectsBuiltInProtectedLabel(t *testing.T) {
req := &metricreductionruletypes.UpdatableReductionRule{
MatchType: metricreductionruletypes.MatchTypeDrop,
Labels: []string{"le"},
}
err := metricreductionrule.ValidateUpdatableReductionRule(req)
assert.Error(t, err, "histogram boundary label must remain protected")
}
func TestDropRuleRejectsCurrentEnvironmentLabel(t *testing.T) {
req := &metricreductionruletypes.UpdatableReductionRule{
MatchType: metricreductionruletypes.MatchTypeDrop,
Labels: []string{"deployment.environment.name"},
}
err := metricreductionrule.ValidateUpdatableReductionRule(req)
assert.Error(t, err, "current deployment environment label must remain protected")
}
func TestDropRuleRejectsHistoricalEnvironmentLabel(t *testing.T) {
req := &metricreductionruletypes.UpdatableReductionRule{
MatchType: metricreductionruletypes.MatchTypeDrop,
Labels: []string{"deployment.environment"},
}
err := metricreductionrule.ValidateUpdatableReductionRule(req)
assert.Error(t, err, "historical deployment environment label must remain protected")
}
func TestKeepRuleAllowsProtectedLabel(t *testing.T) {
req := &metricreductionruletypes.UpdatableReductionRule{
MatchType: metricreductionruletypes.MatchTypeKeep,
Labels: []string{"le"},
}
err := metricreductionrule.ValidateUpdatableReductionRule(req)
assert.NoError(t, err, "a keep rule may retain a protected label")
}
func TestDropRuleAllowsUnprotectedLabel(t *testing.T) {
req := &metricreductionruletypes.UpdatableReductionRule{
MatchType: metricreductionruletypes.MatchTypeDrop,
Labels: []string{"host.name"},
}
err := metricreductionrule.ValidateUpdatableReductionRule(req)
assert.NoError(t, err, "an ordinary label may be dropped")
}

File diff suppressed because one or more lines are too long

View File

@@ -25,13 +25,12 @@ HAS=24
HASANY=25
HASALL=26
SEARCH=27
EXACT=28
BOOL=29
NUMBER=30
QUOTED_TEXT=31
KEY=32
WS=33
FREETEXT=34
BOOL=28
NUMBER=29
QUOTED_TEXT=30
KEY=31
WS=32
FREETEXT=33
'('=1
')'=2
'['=3

File diff suppressed because one or more lines are too long

View File

@@ -25,13 +25,12 @@ HAS=24
HASANY=25
HASALL=26
SEARCH=27
EXACT=28
BOOL=29
NUMBER=30
QUOTED_TEXT=31
KEY=32
WS=33
FREETEXT=34
BOOL=28
NUMBER=29
QUOTED_TEXT=30
KEY=31
WS=32
FREETEXT=33
'('=1
')'=2
'['=3

View File

@@ -1,4 +1,4 @@
// Code generated from grammar/FilterQuery.g4 by ANTLR 4.13.2. DO NOT EDIT.
// Code generated from FilterQuery.g4 by ANTLR 4.13.2. DO NOT EDIT.
package parser // FilterQuery
@@ -128,15 +128,3 @@ func (s *BaseFilterQueryListener) EnterKey(ctx *KeyContext) {}
// ExitKey is called when production key is exited.
func (s *BaseFilterQueryListener) ExitKey(ctx *KeyContext) {}
// EnterField is called when production field is entered.
func (s *BaseFilterQueryListener) EnterField(ctx *FieldContext) {}
// ExitField is called when production field is exited.
func (s *BaseFilterQueryListener) ExitField(ctx *FieldContext) {}
// EnterExactCall is called when production exactCall is entered.
func (s *BaseFilterQueryListener) EnterExactCall(ctx *ExactCallContext) {}
// ExitExactCall is called when production exactCall is exited.
func (s *BaseFilterQueryListener) ExitExactCall(ctx *ExactCallContext) {}

View File

@@ -1,4 +1,4 @@
// Code generated from grammar/FilterQuery.g4 by ANTLR 4.13.2. DO NOT EDIT.
// Code generated from FilterQuery.g4 by ANTLR 4.13.2. DO NOT EDIT.
package parser // FilterQuery
@@ -79,11 +79,3 @@ func (v *BaseFilterQueryVisitor) VisitValue(ctx *ValueContext) interface{} {
func (v *BaseFilterQueryVisitor) VisitKey(ctx *KeyContext) interface{} {
return v.VisitChildren(ctx)
}
func (v *BaseFilterQueryVisitor) VisitField(ctx *FieldContext) interface{} {
return v.VisitChildren(ctx)
}
func (v *BaseFilterQueryVisitor) VisitExactCall(ctx *ExactCallContext) interface{} {
return v.VisitChildren(ctx)
}

View File

@@ -1,4 +1,4 @@
// Code generated from grammar/FilterQuery.g4 by ANTLR 4.13.2. DO NOT EDIT.
// Code generated from FilterQuery.g4 by ANTLR 4.13.2. DO NOT EDIT.
package parser
@@ -50,69 +50,66 @@ func filterquerylexerLexerInit() {
"", "LPAREN", "RPAREN", "LBRACK", "RBRACK", "COMMA", "EQUALS", "NOT_EQUALS",
"NEQ", "LT", "LE", "GT", "GE", "LIKE", "ILIKE", "BETWEEN", "EXISTS",
"REGEXP", "CONTAINS", "IN", "NOT", "AND", "OR", "HASTOKEN", "HAS", "HASANY",
"HASALL", "SEARCH", "EXACT", "BOOL", "NUMBER", "QUOTED_TEXT", "KEY",
"WS", "FREETEXT",
"HASALL", "SEARCH", "BOOL", "NUMBER", "QUOTED_TEXT", "KEY", "WS", "FREETEXT",
}
staticData.RuleNames = []string{
"LPAREN", "RPAREN", "LBRACK", "RBRACK", "COMMA", "EQUALS", "NOT_EQUALS",
"NEQ", "LT", "LE", "GT", "GE", "LIKE", "ILIKE", "BETWEEN", "EXISTS",
"REGEXP", "CONTAINS", "IN", "NOT", "AND", "OR", "HASTOKEN", "HAS", "HASANY",
"HASALL", "SEARCH", "EXACT", "BOOL", "SIGN", "NUMBER", "QUOTED_TEXT",
"SEGMENT", "EMPTY_BRACKS", "OLD_JSON_BRACKS", "KEY", "WS", "DIGIT",
"FREETEXT",
"HASALL", "SEARCH", "BOOL", "SIGN", "NUMBER", "QUOTED_TEXT", "SEGMENT",
"EMPTY_BRACKS", "OLD_JSON_BRACKS", "KEY", "WS", "DIGIT", "FREETEXT",
}
staticData.PredictionContextCache = antlr.NewPredictionContextCache()
staticData.serializedATN = []int32{
4, 0, 34, 337, 6, -1, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2,
4, 0, 33, 329, 6, -1, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2,
4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2,
10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 2, 14, 7, 14, 2, 15,
7, 15, 2, 16, 7, 16, 2, 17, 7, 17, 2, 18, 7, 18, 2, 19, 7, 19, 2, 20, 7,
20, 2, 21, 7, 21, 2, 22, 7, 22, 2, 23, 7, 23, 2, 24, 7, 24, 2, 25, 7, 25,
2, 26, 7, 26, 2, 27, 7, 27, 2, 28, 7, 28, 2, 29, 7, 29, 2, 30, 7, 30, 2,
31, 7, 31, 2, 32, 7, 32, 2, 33, 7, 33, 2, 34, 7, 34, 2, 35, 7, 35, 2, 36,
7, 36, 2, 37, 7, 37, 2, 38, 7, 38, 1, 0, 1, 0, 1, 1, 1, 1, 1, 2, 1, 2,
1, 3, 1, 3, 1, 4, 1, 4, 1, 5, 1, 5, 1, 5, 3, 5, 93, 8, 5, 1, 6, 1, 6, 1,
6, 1, 7, 1, 7, 1, 7, 1, 8, 1, 8, 1, 9, 1, 9, 1, 9, 1, 10, 1, 10, 1, 11,
1, 11, 1, 11, 1, 12, 1, 12, 1, 12, 1, 12, 1, 12, 1, 13, 1, 13, 1, 13, 1,
13, 1, 13, 1, 13, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14,
1, 15, 1, 15, 1, 15, 1, 15, 1, 15, 1, 15, 3, 15, 136, 8, 15, 1, 16, 1,
16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17,
1, 17, 1, 17, 1, 17, 3, 17, 153, 8, 17, 1, 18, 1, 18, 1, 18, 1, 19, 1,
19, 1, 19, 1, 19, 1, 20, 1, 20, 1, 20, 1, 20, 1, 21, 1, 21, 1, 21, 1, 22,
1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 23, 1, 23, 1,
23, 1, 23, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 25, 1, 25,
1, 25, 1, 25, 1, 25, 1, 25, 1, 25, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1,
26, 1, 26, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 28, 1, 28, 1, 28,
1, 28, 1, 28, 1, 28, 1, 28, 1, 28, 1, 28, 3, 28, 218, 8, 28, 1, 29, 1,
29, 1, 30, 3, 30, 223, 8, 30, 1, 30, 4, 30, 226, 8, 30, 11, 30, 12, 30,
227, 1, 30, 1, 30, 5, 30, 232, 8, 30, 10, 30, 12, 30, 235, 9, 30, 3, 30,
237, 8, 30, 1, 30, 1, 30, 3, 30, 241, 8, 30, 1, 30, 4, 30, 244, 8, 30,
11, 30, 12, 30, 245, 3, 30, 248, 8, 30, 1, 30, 3, 30, 251, 8, 30, 1, 30,
1, 30, 4, 30, 255, 8, 30, 11, 30, 12, 30, 256, 1, 30, 1, 30, 3, 30, 261,
8, 30, 1, 30, 4, 30, 264, 8, 30, 11, 30, 12, 30, 265, 3, 30, 268, 8, 30,
3, 30, 270, 8, 30, 1, 31, 1, 31, 1, 31, 1, 31, 5, 31, 276, 8, 31, 10, 31,
12, 31, 279, 9, 31, 1, 31, 1, 31, 1, 31, 1, 31, 1, 31, 5, 31, 286, 8, 31,
10, 31, 12, 31, 289, 9, 31, 1, 31, 3, 31, 292, 8, 31, 1, 32, 1, 32, 5,
32, 296, 8, 32, 10, 32, 12, 32, 299, 9, 32, 1, 33, 1, 33, 1, 33, 1, 34,
1, 34, 1, 34, 1, 34, 1, 35, 1, 35, 1, 35, 1, 35, 1, 35, 1, 35, 1, 35, 4,
35, 315, 8, 35, 11, 35, 12, 35, 316, 5, 35, 319, 8, 35, 10, 35, 12, 35,
322, 9, 35, 1, 36, 4, 36, 325, 8, 36, 11, 36, 12, 36, 326, 1, 36, 1, 36,
1, 37, 1, 37, 1, 38, 4, 38, 334, 8, 38, 11, 38, 12, 38, 335, 0, 0, 39,
1, 1, 3, 2, 5, 3, 7, 4, 9, 5, 11, 6, 13, 7, 15, 8, 17, 9, 19, 10, 21, 11,
23, 12, 25, 13, 27, 14, 29, 15, 31, 16, 33, 17, 35, 18, 37, 19, 39, 20,
41, 21, 43, 22, 45, 23, 47, 24, 49, 25, 51, 26, 53, 27, 55, 28, 57, 29,
59, 0, 61, 30, 63, 31, 65, 0, 67, 0, 69, 0, 71, 32, 73, 33, 75, 0, 77,
34, 1, 0, 29, 2, 0, 76, 76, 108, 108, 2, 0, 73, 73, 105, 105, 2, 0, 75,
75, 107, 107, 2, 0, 69, 69, 101, 101, 2, 0, 66, 66, 98, 98, 2, 0, 84, 84,
116, 116, 2, 0, 87, 87, 119, 119, 2, 0, 78, 78, 110, 110, 2, 0, 88, 88,
120, 120, 2, 0, 83, 83, 115, 115, 2, 0, 82, 82, 114, 114, 2, 0, 71, 71,
103, 103, 2, 0, 80, 80, 112, 112, 2, 0, 67, 67, 99, 99, 2, 0, 79, 79, 111,
111, 2, 0, 65, 65, 97, 97, 2, 0, 68, 68, 100, 100, 2, 0, 72, 72, 104, 104,
2, 0, 89, 89, 121, 121, 2, 0, 85, 85, 117, 117, 2, 0, 70, 70, 102, 102,
2, 0, 43, 43, 45, 45, 2, 0, 34, 34, 92, 92, 2, 0, 39, 39, 92, 92, 4, 0,
35, 36, 64, 90, 95, 95, 97, 123, 7, 0, 35, 36, 45, 45, 47, 58, 64, 90,
95, 95, 97, 123, 125, 125, 3, 0, 9, 10, 13, 13, 32, 32, 1, 0, 48, 57, 8,
0, 9, 10, 13, 13, 32, 34, 39, 41, 44, 44, 60, 62, 91, 91, 93, 93, 361,
7, 36, 2, 37, 7, 37, 1, 0, 1, 0, 1, 1, 1, 1, 1, 2, 1, 2, 1, 3, 1, 3, 1,
4, 1, 4, 1, 5, 1, 5, 1, 5, 3, 5, 91, 8, 5, 1, 6, 1, 6, 1, 6, 1, 7, 1, 7,
1, 7, 1, 8, 1, 8, 1, 9, 1, 9, 1, 9, 1, 10, 1, 10, 1, 11, 1, 11, 1, 11,
1, 12, 1, 12, 1, 12, 1, 12, 1, 12, 1, 13, 1, 13, 1, 13, 1, 13, 1, 13, 1,
13, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 15, 1, 15,
1, 15, 1, 15, 1, 15, 1, 15, 3, 15, 134, 8, 15, 1, 16, 1, 16, 1, 16, 1,
16, 1, 16, 1, 16, 1, 16, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17,
1, 17, 3, 17, 151, 8, 17, 1, 18, 1, 18, 1, 18, 1, 19, 1, 19, 1, 19, 1,
19, 1, 20, 1, 20, 1, 20, 1, 20, 1, 21, 1, 21, 1, 21, 1, 22, 1, 22, 1, 22,
1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 23, 1, 23, 1, 23, 1, 23, 1,
24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 25, 1, 25, 1, 25, 1, 25,
1, 25, 1, 25, 1, 25, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1,
27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 3, 27, 210,
8, 27, 1, 28, 1, 28, 1, 29, 3, 29, 215, 8, 29, 1, 29, 4, 29, 218, 8, 29,
11, 29, 12, 29, 219, 1, 29, 1, 29, 5, 29, 224, 8, 29, 10, 29, 12, 29, 227,
9, 29, 3, 29, 229, 8, 29, 1, 29, 1, 29, 3, 29, 233, 8, 29, 1, 29, 4, 29,
236, 8, 29, 11, 29, 12, 29, 237, 3, 29, 240, 8, 29, 1, 29, 3, 29, 243,
8, 29, 1, 29, 1, 29, 4, 29, 247, 8, 29, 11, 29, 12, 29, 248, 1, 29, 1,
29, 3, 29, 253, 8, 29, 1, 29, 4, 29, 256, 8, 29, 11, 29, 12, 29, 257, 3,
29, 260, 8, 29, 3, 29, 262, 8, 29, 1, 30, 1, 30, 1, 30, 1, 30, 5, 30, 268,
8, 30, 10, 30, 12, 30, 271, 9, 30, 1, 30, 1, 30, 1, 30, 1, 30, 1, 30, 5,
30, 278, 8, 30, 10, 30, 12, 30, 281, 9, 30, 1, 30, 3, 30, 284, 8, 30, 1,
31, 1, 31, 5, 31, 288, 8, 31, 10, 31, 12, 31, 291, 9, 31, 1, 32, 1, 32,
1, 32, 1, 33, 1, 33, 1, 33, 1, 33, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1,
34, 1, 34, 4, 34, 307, 8, 34, 11, 34, 12, 34, 308, 5, 34, 311, 8, 34, 10,
34, 12, 34, 314, 9, 34, 1, 35, 4, 35, 317, 8, 35, 11, 35, 12, 35, 318,
1, 35, 1, 35, 1, 36, 1, 36, 1, 37, 4, 37, 326, 8, 37, 11, 37, 12, 37, 327,
0, 0, 38, 1, 1, 3, 2, 5, 3, 7, 4, 9, 5, 11, 6, 13, 7, 15, 8, 17, 9, 19,
10, 21, 11, 23, 12, 25, 13, 27, 14, 29, 15, 31, 16, 33, 17, 35, 18, 37,
19, 39, 20, 41, 21, 43, 22, 45, 23, 47, 24, 49, 25, 51, 26, 53, 27, 55,
28, 57, 0, 59, 29, 61, 30, 63, 0, 65, 0, 67, 0, 69, 31, 71, 32, 73, 0,
75, 33, 1, 0, 29, 2, 0, 76, 76, 108, 108, 2, 0, 73, 73, 105, 105, 2, 0,
75, 75, 107, 107, 2, 0, 69, 69, 101, 101, 2, 0, 66, 66, 98, 98, 2, 0, 84,
84, 116, 116, 2, 0, 87, 87, 119, 119, 2, 0, 78, 78, 110, 110, 2, 0, 88,
88, 120, 120, 2, 0, 83, 83, 115, 115, 2, 0, 82, 82, 114, 114, 2, 0, 71,
71, 103, 103, 2, 0, 80, 80, 112, 112, 2, 0, 67, 67, 99, 99, 2, 0, 79, 79,
111, 111, 2, 0, 65, 65, 97, 97, 2, 0, 68, 68, 100, 100, 2, 0, 72, 72, 104,
104, 2, 0, 89, 89, 121, 121, 2, 0, 85, 85, 117, 117, 2, 0, 70, 70, 102,
102, 2, 0, 43, 43, 45, 45, 2, 0, 34, 34, 92, 92, 2, 0, 39, 39, 92, 92,
4, 0, 35, 36, 64, 90, 95, 95, 97, 123, 7, 0, 35, 36, 45, 45, 47, 58, 64,
90, 95, 95, 97, 123, 125, 125, 3, 0, 9, 10, 13, 13, 32, 32, 1, 0, 48, 57,
8, 0, 9, 10, 13, 13, 32, 34, 39, 41, 44, 44, 60, 62, 91, 91, 93, 93, 353,
0, 1, 1, 0, 0, 0, 0, 3, 1, 0, 0, 0, 0, 5, 1, 0, 0, 0, 0, 7, 1, 0, 0, 0,
0, 9, 1, 0, 0, 0, 0, 11, 1, 0, 0, 0, 0, 13, 1, 0, 0, 0, 0, 15, 1, 0, 0,
0, 0, 17, 1, 0, 0, 0, 0, 19, 1, 0, 0, 0, 0, 21, 1, 0, 0, 0, 0, 23, 1, 0,
@@ -120,110 +117,107 @@ func filterquerylexerLexerInit() {
0, 0, 0, 0, 33, 1, 0, 0, 0, 0, 35, 1, 0, 0, 0, 0, 37, 1, 0, 0, 0, 0, 39,
1, 0, 0, 0, 0, 41, 1, 0, 0, 0, 0, 43, 1, 0, 0, 0, 0, 45, 1, 0, 0, 0, 0,
47, 1, 0, 0, 0, 0, 49, 1, 0, 0, 0, 0, 51, 1, 0, 0, 0, 0, 53, 1, 0, 0, 0,
0, 55, 1, 0, 0, 0, 0, 57, 1, 0, 0, 0, 0, 61, 1, 0, 0, 0, 0, 63, 1, 0, 0,
0, 0, 71, 1, 0, 0, 0, 0, 73, 1, 0, 0, 0, 0, 77, 1, 0, 0, 0, 1, 79, 1, 0,
0, 0, 3, 81, 1, 0, 0, 0, 5, 83, 1, 0, 0, 0, 7, 85, 1, 0, 0, 0, 9, 87, 1,
0, 0, 0, 11, 92, 1, 0, 0, 0, 13, 94, 1, 0, 0, 0, 15, 97, 1, 0, 0, 0, 17,
100, 1, 0, 0, 0, 19, 102, 1, 0, 0, 0, 21, 105, 1, 0, 0, 0, 23, 107, 1,
0, 0, 0, 25, 110, 1, 0, 0, 0, 27, 115, 1, 0, 0, 0, 29, 121, 1, 0, 0, 0,
31, 129, 1, 0, 0, 0, 33, 137, 1, 0, 0, 0, 35, 144, 1, 0, 0, 0, 37, 154,
1, 0, 0, 0, 39, 157, 1, 0, 0, 0, 41, 161, 1, 0, 0, 0, 43, 165, 1, 0, 0,
0, 45, 168, 1, 0, 0, 0, 47, 177, 1, 0, 0, 0, 49, 181, 1, 0, 0, 0, 51, 188,
1, 0, 0, 0, 53, 195, 1, 0, 0, 0, 55, 202, 1, 0, 0, 0, 57, 217, 1, 0, 0,
0, 59, 219, 1, 0, 0, 0, 61, 269, 1, 0, 0, 0, 63, 291, 1, 0, 0, 0, 65, 293,
1, 0, 0, 0, 67, 300, 1, 0, 0, 0, 69, 303, 1, 0, 0, 0, 71, 307, 1, 0, 0,
0, 73, 324, 1, 0, 0, 0, 75, 330, 1, 0, 0, 0, 77, 333, 1, 0, 0, 0, 79, 80,
5, 40, 0, 0, 80, 2, 1, 0, 0, 0, 81, 82, 5, 41, 0, 0, 82, 4, 1, 0, 0, 0,
83, 84, 5, 91, 0, 0, 84, 6, 1, 0, 0, 0, 85, 86, 5, 93, 0, 0, 86, 8, 1,
0, 0, 0, 87, 88, 5, 44, 0, 0, 88, 10, 1, 0, 0, 0, 89, 93, 5, 61, 0, 0,
90, 91, 5, 61, 0, 0, 91, 93, 5, 61, 0, 0, 92, 89, 1, 0, 0, 0, 92, 90, 1,
0, 0, 0, 93, 12, 1, 0, 0, 0, 94, 95, 5, 33, 0, 0, 95, 96, 5, 61, 0, 0,
96, 14, 1, 0, 0, 0, 97, 98, 5, 60, 0, 0, 98, 99, 5, 62, 0, 0, 99, 16, 1,
0, 0, 0, 100, 101, 5, 60, 0, 0, 101, 18, 1, 0, 0, 0, 102, 103, 5, 60, 0,
0, 103, 104, 5, 61, 0, 0, 104, 20, 1, 0, 0, 0, 105, 106, 5, 62, 0, 0, 106,
22, 1, 0, 0, 0, 107, 108, 5, 62, 0, 0, 108, 109, 5, 61, 0, 0, 109, 24,
1, 0, 0, 0, 110, 111, 7, 0, 0, 0, 111, 112, 7, 1, 0, 0, 112, 113, 7, 2,
0, 0, 113, 114, 7, 3, 0, 0, 114, 26, 1, 0, 0, 0, 115, 116, 7, 1, 0, 0,
116, 117, 7, 0, 0, 0, 117, 118, 7, 1, 0, 0, 118, 119, 7, 2, 0, 0, 119,
120, 7, 3, 0, 0, 120, 28, 1, 0, 0, 0, 121, 122, 7, 4, 0, 0, 122, 123, 7,
3, 0, 0, 123, 124, 7, 5, 0, 0, 124, 125, 7, 6, 0, 0, 125, 126, 7, 3, 0,
0, 126, 127, 7, 3, 0, 0, 127, 128, 7, 7, 0, 0, 128, 30, 1, 0, 0, 0, 129,
130, 7, 3, 0, 0, 130, 131, 7, 8, 0, 0, 131, 132, 7, 1, 0, 0, 132, 133,
7, 9, 0, 0, 133, 135, 7, 5, 0, 0, 134, 136, 7, 9, 0, 0, 135, 134, 1, 0,
0, 0, 135, 136, 1, 0, 0, 0, 136, 32, 1, 0, 0, 0, 137, 138, 7, 10, 0, 0,
138, 139, 7, 3, 0, 0, 139, 140, 7, 11, 0, 0, 140, 141, 7, 3, 0, 0, 141,
142, 7, 8, 0, 0, 142, 143, 7, 12, 0, 0, 143, 34, 1, 0, 0, 0, 144, 145,
7, 13, 0, 0, 145, 146, 7, 14, 0, 0, 146, 147, 7, 7, 0, 0, 147, 148, 7,
5, 0, 0, 148, 149, 7, 15, 0, 0, 149, 150, 7, 1, 0, 0, 150, 152, 7, 7, 0,
0, 151, 153, 7, 9, 0, 0, 152, 151, 1, 0, 0, 0, 152, 153, 1, 0, 0, 0, 153,
36, 1, 0, 0, 0, 154, 155, 7, 1, 0, 0, 155, 156, 7, 7, 0, 0, 156, 38, 1,
0, 0, 0, 157, 158, 7, 7, 0, 0, 158, 159, 7, 14, 0, 0, 159, 160, 7, 5, 0,
0, 160, 40, 1, 0, 0, 0, 161, 162, 7, 15, 0, 0, 162, 163, 7, 7, 0, 0, 163,
164, 7, 16, 0, 0, 164, 42, 1, 0, 0, 0, 165, 166, 7, 14, 0, 0, 166, 167,
7, 10, 0, 0, 167, 44, 1, 0, 0, 0, 168, 169, 7, 17, 0, 0, 169, 170, 7, 15,
0, 0, 170, 171, 7, 9, 0, 0, 171, 172, 7, 5, 0, 0, 172, 173, 7, 14, 0, 0,
173, 174, 7, 2, 0, 0, 174, 175, 7, 3, 0, 0, 175, 176, 7, 7, 0, 0, 176,
46, 1, 0, 0, 0, 177, 178, 7, 17, 0, 0, 178, 179, 7, 15, 0, 0, 179, 180,
7, 9, 0, 0, 180, 48, 1, 0, 0, 0, 181, 182, 7, 17, 0, 0, 182, 183, 7, 15,
0, 0, 183, 184, 7, 9, 0, 0, 184, 185, 7, 15, 0, 0, 185, 186, 7, 7, 0, 0,
186, 187, 7, 18, 0, 0, 187, 50, 1, 0, 0, 0, 188, 189, 7, 17, 0, 0, 189,
190, 7, 15, 0, 0, 190, 191, 7, 9, 0, 0, 191, 192, 7, 15, 0, 0, 192, 193,
7, 0, 0, 0, 193, 194, 7, 0, 0, 0, 194, 52, 1, 0, 0, 0, 195, 196, 7, 9,
0, 0, 196, 197, 7, 3, 0, 0, 197, 198, 7, 15, 0, 0, 198, 199, 7, 10, 0,
0, 199, 200, 7, 13, 0, 0, 200, 201, 7, 17, 0, 0, 201, 54, 1, 0, 0, 0, 202,
203, 7, 3, 0, 0, 203, 204, 7, 8, 0, 0, 204, 205, 7, 15, 0, 0, 205, 206,
7, 13, 0, 0, 206, 207, 7, 5, 0, 0, 207, 56, 1, 0, 0, 0, 208, 209, 7, 5,
0, 0, 209, 210, 7, 10, 0, 0, 210, 211, 7, 19, 0, 0, 211, 218, 7, 3, 0,
0, 212, 213, 7, 20, 0, 0, 213, 214, 7, 15, 0, 0, 214, 215, 7, 0, 0, 0,
215, 216, 7, 9, 0, 0, 216, 218, 7, 3, 0, 0, 217, 208, 1, 0, 0, 0, 217,
212, 1, 0, 0, 0, 218, 58, 1, 0, 0, 0, 219, 220, 7, 21, 0, 0, 220, 60, 1,
0, 0, 0, 221, 223, 3, 59, 29, 0, 222, 221, 1, 0, 0, 0, 222, 223, 1, 0,
0, 0, 223, 225, 1, 0, 0, 0, 224, 226, 3, 75, 37, 0, 225, 224, 1, 0, 0,
0, 226, 227, 1, 0, 0, 0, 227, 225, 1, 0, 0, 0, 227, 228, 1, 0, 0, 0, 228,
236, 1, 0, 0, 0, 229, 233, 5, 46, 0, 0, 230, 232, 3, 75, 37, 0, 231, 230,
1, 0, 0, 0, 232, 235, 1, 0, 0, 0, 233, 231, 1, 0, 0, 0, 233, 234, 1, 0,
0, 0, 234, 237, 1, 0, 0, 0, 235, 233, 1, 0, 0, 0, 236, 229, 1, 0, 0, 0,
236, 237, 1, 0, 0, 0, 237, 247, 1, 0, 0, 0, 238, 240, 7, 3, 0, 0, 239,
241, 3, 59, 29, 0, 240, 239, 1, 0, 0, 0, 240, 241, 1, 0, 0, 0, 241, 243,
1, 0, 0, 0, 242, 244, 3, 75, 37, 0, 243, 242, 1, 0, 0, 0, 244, 245, 1,
0, 0, 0, 245, 243, 1, 0, 0, 0, 245, 246, 1, 0, 0, 0, 246, 248, 1, 0, 0,
0, 247, 238, 1, 0, 0, 0, 247, 248, 1, 0, 0, 0, 248, 270, 1, 0, 0, 0, 249,
251, 3, 59, 29, 0, 250, 249, 1, 0, 0, 0, 250, 251, 1, 0, 0, 0, 251, 252,
1, 0, 0, 0, 252, 254, 5, 46, 0, 0, 253, 255, 3, 75, 37, 0, 254, 253, 1,
0, 0, 0, 255, 256, 1, 0, 0, 0, 256, 254, 1, 0, 0, 0, 256, 257, 1, 0, 0,
0, 257, 267, 1, 0, 0, 0, 258, 260, 7, 3, 0, 0, 259, 261, 3, 59, 29, 0,
260, 259, 1, 0, 0, 0, 260, 261, 1, 0, 0, 0, 261, 263, 1, 0, 0, 0, 262,
264, 3, 75, 37, 0, 263, 262, 1, 0, 0, 0, 264, 265, 1, 0, 0, 0, 265, 263,
1, 0, 0, 0, 265, 266, 1, 0, 0, 0, 266, 268, 1, 0, 0, 0, 267, 258, 1, 0,
0, 0, 267, 268, 1, 0, 0, 0, 268, 270, 1, 0, 0, 0, 269, 222, 1, 0, 0, 0,
269, 250, 1, 0, 0, 0, 270, 62, 1, 0, 0, 0, 271, 277, 5, 34, 0, 0, 272,
276, 8, 22, 0, 0, 273, 274, 5, 92, 0, 0, 274, 276, 9, 0, 0, 0, 275, 272,
1, 0, 0, 0, 275, 273, 1, 0, 0, 0, 276, 279, 1, 0, 0, 0, 277, 275, 1, 0,
0, 0, 277, 278, 1, 0, 0, 0, 278, 280, 1, 0, 0, 0, 279, 277, 1, 0, 0, 0,
280, 292, 5, 34, 0, 0, 281, 287, 5, 39, 0, 0, 282, 286, 8, 23, 0, 0, 283,
284, 5, 92, 0, 0, 284, 286, 9, 0, 0, 0, 285, 282, 1, 0, 0, 0, 285, 283,
1, 0, 0, 0, 286, 289, 1, 0, 0, 0, 287, 285, 1, 0, 0, 0, 287, 288, 1, 0,
0, 0, 288, 290, 1, 0, 0, 0, 289, 287, 1, 0, 0, 0, 290, 292, 5, 39, 0, 0,
291, 271, 1, 0, 0, 0, 291, 281, 1, 0, 0, 0, 292, 64, 1, 0, 0, 0, 293, 297,
7, 24, 0, 0, 294, 296, 7, 25, 0, 0, 295, 294, 1, 0, 0, 0, 296, 299, 1,
0, 0, 0, 297, 295, 1, 0, 0, 0, 297, 298, 1, 0, 0, 0, 298, 66, 1, 0, 0,
0, 299, 297, 1, 0, 0, 0, 300, 301, 5, 91, 0, 0, 301, 302, 5, 93, 0, 0,
302, 68, 1, 0, 0, 0, 303, 304, 5, 91, 0, 0, 304, 305, 5, 42, 0, 0, 305,
306, 5, 93, 0, 0, 306, 70, 1, 0, 0, 0, 307, 320, 3, 65, 32, 0, 308, 309,
5, 46, 0, 0, 309, 319, 3, 65, 32, 0, 310, 319, 3, 67, 33, 0, 311, 319,
3, 69, 34, 0, 312, 314, 5, 46, 0, 0, 313, 315, 3, 75, 37, 0, 314, 313,
1, 0, 0, 0, 315, 316, 1, 0, 0, 0, 316, 314, 1, 0, 0, 0, 316, 317, 1, 0,
0, 0, 317, 319, 1, 0, 0, 0, 318, 308, 1, 0, 0, 0, 318, 310, 1, 0, 0, 0,
318, 311, 1, 0, 0, 0, 318, 312, 1, 0, 0, 0, 319, 322, 1, 0, 0, 0, 320,
318, 1, 0, 0, 0, 320, 321, 1, 0, 0, 0, 321, 72, 1, 0, 0, 0, 322, 320, 1,
0, 0, 0, 323, 325, 7, 26, 0, 0, 324, 323, 1, 0, 0, 0, 325, 326, 1, 0, 0,
0, 326, 324, 1, 0, 0, 0, 326, 327, 1, 0, 0, 0, 327, 328, 1, 0, 0, 0, 328,
329, 6, 36, 0, 0, 329, 74, 1, 0, 0, 0, 330, 331, 7, 27, 0, 0, 331, 76,
1, 0, 0, 0, 332, 334, 8, 28, 0, 0, 333, 332, 1, 0, 0, 0, 334, 335, 1, 0,
0, 0, 335, 333, 1, 0, 0, 0, 335, 336, 1, 0, 0, 0, 336, 78, 1, 0, 0, 0,
29, 0, 92, 135, 152, 217, 222, 227, 233, 236, 240, 245, 247, 250, 256,
260, 265, 267, 269, 275, 277, 285, 287, 291, 297, 316, 318, 320, 326, 335,
1, 6, 0, 0,
0, 55, 1, 0, 0, 0, 0, 59, 1, 0, 0, 0, 0, 61, 1, 0, 0, 0, 0, 69, 1, 0, 0,
0, 0, 71, 1, 0, 0, 0, 0, 75, 1, 0, 0, 0, 1, 77, 1, 0, 0, 0, 3, 79, 1, 0,
0, 0, 5, 81, 1, 0, 0, 0, 7, 83, 1, 0, 0, 0, 9, 85, 1, 0, 0, 0, 11, 90,
1, 0, 0, 0, 13, 92, 1, 0, 0, 0, 15, 95, 1, 0, 0, 0, 17, 98, 1, 0, 0, 0,
19, 100, 1, 0, 0, 0, 21, 103, 1, 0, 0, 0, 23, 105, 1, 0, 0, 0, 25, 108,
1, 0, 0, 0, 27, 113, 1, 0, 0, 0, 29, 119, 1, 0, 0, 0, 31, 127, 1, 0, 0,
0, 33, 135, 1, 0, 0, 0, 35, 142, 1, 0, 0, 0, 37, 152, 1, 0, 0, 0, 39, 155,
1, 0, 0, 0, 41, 159, 1, 0, 0, 0, 43, 163, 1, 0, 0, 0, 45, 166, 1, 0, 0,
0, 47, 175, 1, 0, 0, 0, 49, 179, 1, 0, 0, 0, 51, 186, 1, 0, 0, 0, 53, 193,
1, 0, 0, 0, 55, 209, 1, 0, 0, 0, 57, 211, 1, 0, 0, 0, 59, 261, 1, 0, 0,
0, 61, 283, 1, 0, 0, 0, 63, 285, 1, 0, 0, 0, 65, 292, 1, 0, 0, 0, 67, 295,
1, 0, 0, 0, 69, 299, 1, 0, 0, 0, 71, 316, 1, 0, 0, 0, 73, 322, 1, 0, 0,
0, 75, 325, 1, 0, 0, 0, 77, 78, 5, 40, 0, 0, 78, 2, 1, 0, 0, 0, 79, 80,
5, 41, 0, 0, 80, 4, 1, 0, 0, 0, 81, 82, 5, 91, 0, 0, 82, 6, 1, 0, 0, 0,
83, 84, 5, 93, 0, 0, 84, 8, 1, 0, 0, 0, 85, 86, 5, 44, 0, 0, 86, 10, 1,
0, 0, 0, 87, 91, 5, 61, 0, 0, 88, 89, 5, 61, 0, 0, 89, 91, 5, 61, 0, 0,
90, 87, 1, 0, 0, 0, 90, 88, 1, 0, 0, 0, 91, 12, 1, 0, 0, 0, 92, 93, 5,
33, 0, 0, 93, 94, 5, 61, 0, 0, 94, 14, 1, 0, 0, 0, 95, 96, 5, 60, 0, 0,
96, 97, 5, 62, 0, 0, 97, 16, 1, 0, 0, 0, 98, 99, 5, 60, 0, 0, 99, 18, 1,
0, 0, 0, 100, 101, 5, 60, 0, 0, 101, 102, 5, 61, 0, 0, 102, 20, 1, 0, 0,
0, 103, 104, 5, 62, 0, 0, 104, 22, 1, 0, 0, 0, 105, 106, 5, 62, 0, 0, 106,
107, 5, 61, 0, 0, 107, 24, 1, 0, 0, 0, 108, 109, 7, 0, 0, 0, 109, 110,
7, 1, 0, 0, 110, 111, 7, 2, 0, 0, 111, 112, 7, 3, 0, 0, 112, 26, 1, 0,
0, 0, 113, 114, 7, 1, 0, 0, 114, 115, 7, 0, 0, 0, 115, 116, 7, 1, 0, 0,
116, 117, 7, 2, 0, 0, 117, 118, 7, 3, 0, 0, 118, 28, 1, 0, 0, 0, 119, 120,
7, 4, 0, 0, 120, 121, 7, 3, 0, 0, 121, 122, 7, 5, 0, 0, 122, 123, 7, 6,
0, 0, 123, 124, 7, 3, 0, 0, 124, 125, 7, 3, 0, 0, 125, 126, 7, 7, 0, 0,
126, 30, 1, 0, 0, 0, 127, 128, 7, 3, 0, 0, 128, 129, 7, 8, 0, 0, 129, 130,
7, 1, 0, 0, 130, 131, 7, 9, 0, 0, 131, 133, 7, 5, 0, 0, 132, 134, 7, 9,
0, 0, 133, 132, 1, 0, 0, 0, 133, 134, 1, 0, 0, 0, 134, 32, 1, 0, 0, 0,
135, 136, 7, 10, 0, 0, 136, 137, 7, 3, 0, 0, 137, 138, 7, 11, 0, 0, 138,
139, 7, 3, 0, 0, 139, 140, 7, 8, 0, 0, 140, 141, 7, 12, 0, 0, 141, 34,
1, 0, 0, 0, 142, 143, 7, 13, 0, 0, 143, 144, 7, 14, 0, 0, 144, 145, 7,
7, 0, 0, 145, 146, 7, 5, 0, 0, 146, 147, 7, 15, 0, 0, 147, 148, 7, 1, 0,
0, 148, 150, 7, 7, 0, 0, 149, 151, 7, 9, 0, 0, 150, 149, 1, 0, 0, 0, 150,
151, 1, 0, 0, 0, 151, 36, 1, 0, 0, 0, 152, 153, 7, 1, 0, 0, 153, 154, 7,
7, 0, 0, 154, 38, 1, 0, 0, 0, 155, 156, 7, 7, 0, 0, 156, 157, 7, 14, 0,
0, 157, 158, 7, 5, 0, 0, 158, 40, 1, 0, 0, 0, 159, 160, 7, 15, 0, 0, 160,
161, 7, 7, 0, 0, 161, 162, 7, 16, 0, 0, 162, 42, 1, 0, 0, 0, 163, 164,
7, 14, 0, 0, 164, 165, 7, 10, 0, 0, 165, 44, 1, 0, 0, 0, 166, 167, 7, 17,
0, 0, 167, 168, 7, 15, 0, 0, 168, 169, 7, 9, 0, 0, 169, 170, 7, 5, 0, 0,
170, 171, 7, 14, 0, 0, 171, 172, 7, 2, 0, 0, 172, 173, 7, 3, 0, 0, 173,
174, 7, 7, 0, 0, 174, 46, 1, 0, 0, 0, 175, 176, 7, 17, 0, 0, 176, 177,
7, 15, 0, 0, 177, 178, 7, 9, 0, 0, 178, 48, 1, 0, 0, 0, 179, 180, 7, 17,
0, 0, 180, 181, 7, 15, 0, 0, 181, 182, 7, 9, 0, 0, 182, 183, 7, 15, 0,
0, 183, 184, 7, 7, 0, 0, 184, 185, 7, 18, 0, 0, 185, 50, 1, 0, 0, 0, 186,
187, 7, 17, 0, 0, 187, 188, 7, 15, 0, 0, 188, 189, 7, 9, 0, 0, 189, 190,
7, 15, 0, 0, 190, 191, 7, 0, 0, 0, 191, 192, 7, 0, 0, 0, 192, 52, 1, 0,
0, 0, 193, 194, 7, 9, 0, 0, 194, 195, 7, 3, 0, 0, 195, 196, 7, 15, 0, 0,
196, 197, 7, 10, 0, 0, 197, 198, 7, 13, 0, 0, 198, 199, 7, 17, 0, 0, 199,
54, 1, 0, 0, 0, 200, 201, 7, 5, 0, 0, 201, 202, 7, 10, 0, 0, 202, 203,
7, 19, 0, 0, 203, 210, 7, 3, 0, 0, 204, 205, 7, 20, 0, 0, 205, 206, 7,
15, 0, 0, 206, 207, 7, 0, 0, 0, 207, 208, 7, 9, 0, 0, 208, 210, 7, 3, 0,
0, 209, 200, 1, 0, 0, 0, 209, 204, 1, 0, 0, 0, 210, 56, 1, 0, 0, 0, 211,
212, 7, 21, 0, 0, 212, 58, 1, 0, 0, 0, 213, 215, 3, 57, 28, 0, 214, 213,
1, 0, 0, 0, 214, 215, 1, 0, 0, 0, 215, 217, 1, 0, 0, 0, 216, 218, 3, 73,
36, 0, 217, 216, 1, 0, 0, 0, 218, 219, 1, 0, 0, 0, 219, 217, 1, 0, 0, 0,
219, 220, 1, 0, 0, 0, 220, 228, 1, 0, 0, 0, 221, 225, 5, 46, 0, 0, 222,
224, 3, 73, 36, 0, 223, 222, 1, 0, 0, 0, 224, 227, 1, 0, 0, 0, 225, 223,
1, 0, 0, 0, 225, 226, 1, 0, 0, 0, 226, 229, 1, 0, 0, 0, 227, 225, 1, 0,
0, 0, 228, 221, 1, 0, 0, 0, 228, 229, 1, 0, 0, 0, 229, 239, 1, 0, 0, 0,
230, 232, 7, 3, 0, 0, 231, 233, 3, 57, 28, 0, 232, 231, 1, 0, 0, 0, 232,
233, 1, 0, 0, 0, 233, 235, 1, 0, 0, 0, 234, 236, 3, 73, 36, 0, 235, 234,
1, 0, 0, 0, 236, 237, 1, 0, 0, 0, 237, 235, 1, 0, 0, 0, 237, 238, 1, 0,
0, 0, 238, 240, 1, 0, 0, 0, 239, 230, 1, 0, 0, 0, 239, 240, 1, 0, 0, 0,
240, 262, 1, 0, 0, 0, 241, 243, 3, 57, 28, 0, 242, 241, 1, 0, 0, 0, 242,
243, 1, 0, 0, 0, 243, 244, 1, 0, 0, 0, 244, 246, 5, 46, 0, 0, 245, 247,
3, 73, 36, 0, 246, 245, 1, 0, 0, 0, 247, 248, 1, 0, 0, 0, 248, 246, 1,
0, 0, 0, 248, 249, 1, 0, 0, 0, 249, 259, 1, 0, 0, 0, 250, 252, 7, 3, 0,
0, 251, 253, 3, 57, 28, 0, 252, 251, 1, 0, 0, 0, 252, 253, 1, 0, 0, 0,
253, 255, 1, 0, 0, 0, 254, 256, 3, 73, 36, 0, 255, 254, 1, 0, 0, 0, 256,
257, 1, 0, 0, 0, 257, 255, 1, 0, 0, 0, 257, 258, 1, 0, 0, 0, 258, 260,
1, 0, 0, 0, 259, 250, 1, 0, 0, 0, 259, 260, 1, 0, 0, 0, 260, 262, 1, 0,
0, 0, 261, 214, 1, 0, 0, 0, 261, 242, 1, 0, 0, 0, 262, 60, 1, 0, 0, 0,
263, 269, 5, 34, 0, 0, 264, 268, 8, 22, 0, 0, 265, 266, 5, 92, 0, 0, 266,
268, 9, 0, 0, 0, 267, 264, 1, 0, 0, 0, 267, 265, 1, 0, 0, 0, 268, 271,
1, 0, 0, 0, 269, 267, 1, 0, 0, 0, 269, 270, 1, 0, 0, 0, 270, 272, 1, 0,
0, 0, 271, 269, 1, 0, 0, 0, 272, 284, 5, 34, 0, 0, 273, 279, 5, 39, 0,
0, 274, 278, 8, 23, 0, 0, 275, 276, 5, 92, 0, 0, 276, 278, 9, 0, 0, 0,
277, 274, 1, 0, 0, 0, 277, 275, 1, 0, 0, 0, 278, 281, 1, 0, 0, 0, 279,
277, 1, 0, 0, 0, 279, 280, 1, 0, 0, 0, 280, 282, 1, 0, 0, 0, 281, 279,
1, 0, 0, 0, 282, 284, 5, 39, 0, 0, 283, 263, 1, 0, 0, 0, 283, 273, 1, 0,
0, 0, 284, 62, 1, 0, 0, 0, 285, 289, 7, 24, 0, 0, 286, 288, 7, 25, 0, 0,
287, 286, 1, 0, 0, 0, 288, 291, 1, 0, 0, 0, 289, 287, 1, 0, 0, 0, 289,
290, 1, 0, 0, 0, 290, 64, 1, 0, 0, 0, 291, 289, 1, 0, 0, 0, 292, 293, 5,
91, 0, 0, 293, 294, 5, 93, 0, 0, 294, 66, 1, 0, 0, 0, 295, 296, 5, 91,
0, 0, 296, 297, 5, 42, 0, 0, 297, 298, 5, 93, 0, 0, 298, 68, 1, 0, 0, 0,
299, 312, 3, 63, 31, 0, 300, 301, 5, 46, 0, 0, 301, 311, 3, 63, 31, 0,
302, 311, 3, 65, 32, 0, 303, 311, 3, 67, 33, 0, 304, 306, 5, 46, 0, 0,
305, 307, 3, 73, 36, 0, 306, 305, 1, 0, 0, 0, 307, 308, 1, 0, 0, 0, 308,
306, 1, 0, 0, 0, 308, 309, 1, 0, 0, 0, 309, 311, 1, 0, 0, 0, 310, 300,
1, 0, 0, 0, 310, 302, 1, 0, 0, 0, 310, 303, 1, 0, 0, 0, 310, 304, 1, 0,
0, 0, 311, 314, 1, 0, 0, 0, 312, 310, 1, 0, 0, 0, 312, 313, 1, 0, 0, 0,
313, 70, 1, 0, 0, 0, 314, 312, 1, 0, 0, 0, 315, 317, 7, 26, 0, 0, 316,
315, 1, 0, 0, 0, 317, 318, 1, 0, 0, 0, 318, 316, 1, 0, 0, 0, 318, 319,
1, 0, 0, 0, 319, 320, 1, 0, 0, 0, 320, 321, 6, 35, 0, 0, 321, 72, 1, 0,
0, 0, 322, 323, 7, 27, 0, 0, 323, 74, 1, 0, 0, 0, 324, 326, 8, 28, 0, 0,
325, 324, 1, 0, 0, 0, 326, 327, 1, 0, 0, 0, 327, 325, 1, 0, 0, 0, 327,
328, 1, 0, 0, 0, 328, 76, 1, 0, 0, 0, 29, 0, 90, 133, 150, 209, 214, 219,
225, 228, 232, 237, 239, 242, 248, 252, 257, 259, 261, 267, 269, 277, 279,
283, 289, 308, 310, 312, 318, 327, 1, 6, 0, 0,
}
deserializer := antlr.NewATNDeserializer(nil)
staticData.atn = deserializer.Deserialize(staticData.serializedATN)
@@ -291,11 +285,10 @@ const (
FilterQueryLexerHASANY = 25
FilterQueryLexerHASALL = 26
FilterQueryLexerSEARCH = 27
FilterQueryLexerEXACT = 28
FilterQueryLexerBOOL = 29
FilterQueryLexerNUMBER = 30
FilterQueryLexerQUOTED_TEXT = 31
FilterQueryLexerKEY = 32
FilterQueryLexerWS = 33
FilterQueryLexerFREETEXT = 34
FilterQueryLexerBOOL = 28
FilterQueryLexerNUMBER = 29
FilterQueryLexerQUOTED_TEXT = 30
FilterQueryLexerKEY = 31
FilterQueryLexerWS = 32
FilterQueryLexerFREETEXT = 33
)

View File

@@ -1,4 +1,4 @@
// Code generated from grammar/FilterQuery.g4 by ANTLR 4.13.2. DO NOT EDIT.
// Code generated from FilterQuery.g4 by ANTLR 4.13.2. DO NOT EDIT.
package parser // FilterQuery
@@ -62,12 +62,6 @@ type FilterQueryListener interface {
// EnterKey is called when entering the key production.
EnterKey(c *KeyContext)
// EnterField is called when entering the field production.
EnterField(c *FieldContext)
// EnterExactCall is called when entering the exactCall production.
EnterExactCall(c *ExactCallContext)
// ExitQuery is called when exiting the query production.
ExitQuery(c *QueryContext)
@@ -121,10 +115,4 @@ type FilterQueryListener interface {
// ExitKey is called when exiting the key production.
ExitKey(c *KeyContext)
// ExitField is called when exiting the field production.
ExitField(c *FieldContext)
// ExitExactCall is called when exiting the exactCall production.
ExitExactCall(c *ExactCallContext)
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
// Code generated from grammar/FilterQuery.g4 by ANTLR 4.13.2. DO NOT EDIT.
// Code generated from FilterQuery.g4 by ANTLR 4.13.2. DO NOT EDIT.
package parser // FilterQuery
@@ -61,10 +61,4 @@ type FilterQueryVisitor interface {
// Visit a parse tree produced by FilterQueryParser#key.
VisitKey(ctx *KeyContext) interface{}
// Visit a parse tree produced by FilterQueryParser#field.
VisitField(ctx *FieldContext) interface{}
// Visit a parse tree produced by FilterQueryParser#exactCall.
VisitExactCall(ctx *ExactCallContext) interface{}
}

View File

@@ -0,0 +1,162 @@
package clickhouseprometheusv2
import (
"encoding/json"
"flag"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"testing"
"github.com/prometheus/prometheus/promql/parser"
"github.com/stretchr/testify/require"
)
var updateGolden = flag.Bool("update", false, "rewrite the classification golden file")
const goldenFile = "testdata/classification_golden.json"
// corpusFile is the conformance corpus that the integration suite replays.
// The golden freezes the route of every expression in it.
const corpusFile = "../../../tests/integration/testdata/promqltestcorpus/corpus.json"
// TestClassificationGolden freezes the route of every conformance-corpus
// expression: "full", "hybrid(<units>)", or "fallback: <reason>". The route
// is a correctness surface of its own. A change that silently sends a shape
// to the engine loses the pushdown. A change that silently transpiles an
// unproven shape risks wrong numbers. Both must show as a diff of this file.
// The corpus suite's clickhousev2 leg then judges the numbers.
//
// The golden keys on the expression alone. The corpus evaluates each
// expression on several grids, and the test requires the route to be the
// same on all of them. If a classifier change ever makes the route depend
// on the grid, this test fails and the key must grow.
//
// Regenerate after an intended classifier change:
//
// go test ./pkg/prometheus/clickhouseprometheusv2 -run TestClassificationGolden -update
func TestClassificationGolden(t *testing.T) {
raw, err := os.ReadFile(corpusFile)
require.NoError(t, err)
var corpus struct {
Cases []struct {
Expr string `json:"expr"`
StartMs int64 `json:"start_ms"`
EndMs int64 `json:"end_ms"`
StepMs int64 `json:"step_ms"`
} `json:"cases"`
}
require.NoError(t, json.Unmarshal(raw, &corpus))
require.NotEmpty(t, corpus.Cases)
promParser := parser.NewParser(parser.Options{})
routes := map[string]string{}
for _, c := range corpus.Cases {
expr, err := promParser.ParseExpr(c.Expr)
require.NoError(t, err, "corpus expression must parse: %q", c.Expr)
var route string
plan, ok := classify(expr, gridContext{startMs: c.StartMs, endMs: c.EndMs, stepMs: c.StepMs})
switch {
case ok && plan.full:
route = "full"
case ok:
route = fmt.Sprintf("hybrid(%d)", len(plan.units))
default:
route = "fallback: " + fallbackShape(expr)
}
if prev, seen := routes[c.Expr]; seen {
require.Equal(t, prev, route,
"route differs between grids for %q — the golden key must grow to include the grid", c.Expr)
continue
}
routes[c.Expr] = route
}
// json.MarshalIndent sorts map keys: the file is deterministic.
got, err := json.MarshalIndent(routes, "", " ")
require.NoError(t, err)
got = append(got, '\n')
if *updateGolden {
require.NoError(t, os.MkdirAll(filepath.Dir(goldenFile), 0o755))
require.NoError(t, os.WriteFile(goldenFile, got, 0o644))
return
}
want, err := os.ReadFile(goldenFile)
require.NoError(t, err, "golden missing — generate it with -update")
require.Equal(t, string(want), string(got),
"classification route changed; if intended, regenerate with -update and explain the diff in review")
}
// fallbackShape buckets a non-transpilable query by why it stays on the engine
// path, to separate "already served well" (instant selectors on the last-sample-per-step
// path) from genuine compiler gaps.
func fallbackShape(expr parser.Expr) string {
var hasMatrix, hasSubquery, hasAt, hasDurationExpr, overTime bool
rangeFns := map[string]bool{"rate": true, "increase": true, "delta": true, "irate": true, "idelta": true}
var unsupportedFns []string
parser.Inspect(expr, func(node parser.Node, _ []parser.Node) error {
switch n := node.(type) {
case *parser.MatrixSelector:
hasMatrix = true
if n.RangeExpr != nil {
hasDurationExpr = true
}
case *parser.SubqueryExpr:
hasSubquery = true
if n.RangeExpr != nil || n.StepExpr != nil || n.OriginalOffsetExpr != nil {
hasDurationExpr = true
}
case *parser.VectorSelector:
if n.Timestamp != nil || n.StartOrEnd != 0 {
hasAt = true
}
if n.OriginalOffsetExpr != nil {
hasDurationExpr = true
}
case *parser.Call:
if strings.HasSuffix(n.Func.Name, "_over_time") {
overTime = true
} else if !rangeFns[n.Func.Name] {
unsupportedFns = append(unsupportedFns, n.Func.Name)
}
}
return nil
})
switch {
case hasDurationExpr:
return "duration expression (resolved only at evaluation time)"
case hasSubquery:
return "subquery"
case hasAt:
return "@ modifier"
case overTime:
return "*_over_time range function"
case !hasMatrix:
return "instant-selector shape (last-sample-per-step engine path)"
case len(unsupportedFns) > 0:
return fmt.Sprintf("range shape with unsupported function(s): %s", strings.Join(dedupe(unsupportedFns), ",")) //nolint:makezero
default:
return "other range shape"
}
}
func dedupe(in []string) []string {
seen := map[string]bool{}
var out []string
for _, s := range in {
if !seen[s] {
seen[s] = true
out = append(out, s)
}
}
sort.Strings(out)
return out
}

View File

@@ -2,27 +2,27 @@ package clickhouseprometheusv2
import (
"context"
"time"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/prometheus"
"github.com/SigNoz/signoz/pkg/telemetrystore"
"github.com/prometheus/prometheus/promql"
"github.com/prometheus/prometheus/storage"
)
// provider ties the package together: its own engine and parser, and the
// ClickHouse client behind the native storage.Querier. It stays unexported:
// callers hold the prometheus.Prometheus interface, which is the boundary
// between the two provider implementations.
type provider struct {
settings factory.ScopedProviderSettings
engine *prometheus.Engine
parser prometheus.Parser
client *client
executor *executor
}
var (
_ prometheus.Prometheus = (*provider)(nil)
_ prometheus.StatementCapturer = (*provider)(nil)
_ prometheus.RangeExecutor = (*provider)(nil)
)
func NewFactory(telemetryStore telemetrystore.TelemetryStore) factory.ProviderFactory[prometheus.Prometheus, prometheus.Config] {
@@ -43,9 +43,14 @@ func New(_ context.Context, providerSettings factory.ProviderSettings, config pr
engine: engine,
parser: parser,
client: client,
executor: &executor{client: client, engine: engine, parser: parser},
}, nil
}
func (p *provider) TryExecuteRange(ctx context.Context, query string, start, end time.Time, step time.Duration) (promql.Matrix, bool, error) {
return p.executor.TryExecuteRange(ctx, query, start, end, step)
}
func (p *provider) Engine() *prometheus.Engine {
return p.engine
}

View File

@@ -0,0 +1,319 @@
{
"(metric1_total offset 2) ^ 2": "full",
"-metric_a or -metric_b": "hybrid(2)",
"-metric_total": "full",
"-{job=\"api\"}": "full",
"10 atan2 20": "fallback: instant-selector shape (last-sample-per-step engine path)",
"10 atan2 NaN": "fallback: instant-selector shape (last-sample-per-step engine path)",
"AVG(http_requests) BY (job)": "full",
"COUNT(http_requests) BY (job)": "full",
"MAX(http_requests) BY (job)": "full",
"MIN(http_requests) BY (job)": "full",
"SUM BY (group) (((http_requests{job=\"api-server\"})))": "full",
"SUM BY (group) (http_requests{job=\"api-server\"})": "full",
"SUM(http_requests)": "full",
"SUM(http_requests) BY (job)": "full",
"SUM(http_requests) BY (job, group)": "full",
"SUM(http_requests) BY (job, nonexistent)": "full",
"SUM(http_requests{instance=\"0\"}) BY(job)": "full",
"abs(-1 * http_requests{group=\"production\",job=\"api-server\"})": "hybrid(1)",
"acos(trig - 10.1)": "hybrid(1)",
"acosh(trig)": "fallback: instant-selector shape (last-sample-per-step engine path)",
"asin(trig - 10.1)": "hybrid(1)",
"asinh(trig)": "fallback: instant-selector shape (last-sample-per-step engine path)",
"atan(trig)": "fallback: instant-selector shape (last-sample-per-step engine path)",
"atanh(trig - 10.1)": "hybrid(1)",
"avg by (group) (data{test=\"nan\"})": "full",
"avg by (group) (data{test=\"neg_inf\"})": "full",
"avg by (group) (data{test=\"pos_inf\"})": "full",
"avg by (group) (http_requests{job=\"api-server\"})": "full",
"avg(data)": "full",
"avg(data{test=\"-big\"})": "full",
"avg(data{test=\"-inf\"})": "full",
"avg(data{test=\"-inf2\"})": "full",
"avg(data{test=\"-inf3\"})": "full",
"avg(data{test=\"big\"})": "full",
"avg(data{test=\"bigzero\"})": "full",
"avg(data{test=\"inf\"})": "full",
"avg(data{test=\"inf2\"})": "full",
"avg(data{test=\"inf3\"})": "full",
"avg(data{test=\"inf_inf\"})": "full",
"avg(data{test=\"nan\"})": "full",
"avg(data{test=\"ten\"})": "full",
"avg(foo) - 52": "full",
"avg(foo) == 52": "full",
"avg(topk(10, foo)) - 52": "fallback: instant-selector shape (last-sample-per-step engine path)",
"avg(topk(10, foo)) == 52": "fallback: instant-selector shape (last-sample-per-step engine path)",
"avg(topk(11, foo)) - 52": "fallback: instant-selector shape (last-sample-per-step engine path)",
"avg(topk(11, foo)) == 52": "fallback: instant-selector shape (last-sample-per-step engine path)",
"avg(topk(8, foo)) - 52": "fallback: instant-selector shape (last-sample-per-step engine path)",
"avg(topk(8, foo)) == 52": "fallback: instant-selector shape (last-sample-per-step engine path)",
"avg(topk(9, foo)) - 52": "fallback: instant-selector shape (last-sample-per-step engine path)",
"avg(topk(9, foo)) == 52": "fallback: instant-selector shape (last-sample-per-step engine path)",
"avg_over_time(foo[100s]) - 52": "full",
"avg_over_time(foo[100s]) == 52": "full",
"avg_over_time(foo[110s]) - 52": "full",
"avg_over_time(foo[110s]) == 52": "full",
"avg_over_time(foo[120s]) - 52": "full",
"avg_over_time(foo[120s]) == 52": "full",
"avg_over_time(foo[130s]) - 52": "full",
"avg_over_time(foo[130s]) == 52": "full",
"avg_over_time(metric10[1m])": "full",
"avg_over_time(metric11[1m])": "full",
"avg_over_time(metric1[1m])": "full",
"avg_over_time(metric2[1m])": "full",
"avg_over_time(metric3[1m])": "full",
"avg_over_time(metric4[1m])": "full",
"avg_over_time(metric5[1m])": "full",
"avg_over_time(metric6[1m])": "full",
"avg_over_time(metric7[1m])": "full",
"avg_over_time(metric8[1m])": "full",
"avg_over_time(metric9[1m])": "full",
"avg_over_time(metric[2m])": "full",
"avg_over_time(rate(http_requests_total[1m])[1m:1s])": "hybrid(1)",
"ceil(0.004 * http_requests{group=\"production\",job=\"api-server\"})": "hybrid(1)",
"changes(http_requests[1800])": "fallback: range shape with unsupported function(s): changes",
"changes(http_requests[30m])": "fallback: range shape with unsupported function(s): changes",
"changes(metric[1m])": "fallback: range shape with unsupported function(s): changes",
"changes(metric[5m])": "fallback: range shape with unsupported function(s): changes",
"changes(x[20m])": "fallback: range shape with unsupported function(s): changes",
"clamp(metric_total, 0, 100)": "fallback: instant-selector shape (last-sample-per-step engine path)",
"cos(trig)": "fallback: instant-selector shape (last-sample-per-step engine path)",
"cosh(trig)": "fallback: instant-selector shape (last-sample-per-step engine path)",
"count by (group) (http_requests{job=\"api-server\"})": "full",
"count by(namespace, pod, cpu) (node_cpu_seconds_total{cpu=~\".*\",job=\"node-exporter\",mode=\"idle\",namespace=\"observability\",pod=\"node-exporter-l454v\"}) * on(namespace, pod) group_left(node) node_namespace_pod:kube_pod_info:{namespace=\"observability\",pod=\"node-exporter-l454v\"}": "hybrid(1)",
"count_over_time(metric1_total[range()])": "fallback: duration expression (resolved only at evaluation time)",
"count_over_time(metric1_total[step()])": "fallback: duration expression (resolved only at evaluation time)",
"count_over_time(metric[10])": "full",
"count_over_time(metric[10s])": "full",
"count_over_time(metric[1m])": "full",
"count_over_time(metric[1s])": "full",
"count_over_time(metric[20])": "full",
"count_over_time(metric[20s])": "full",
"deg(trig - 10)": "hybrid(1)",
"deg(trig - 20)": "hybrid(1)",
"deg(trig)": "fallback: instant-selector shape (last-sample-per-step engine path)",
"delta(metric[1m])": "full",
"floor(0.004 * http_requests{group=\"production\",job=\"api-server\"})": "hybrid(1)",
"foo \u003e 2 or bar": "fallback: instant-selector shape (last-sample-per-step engine path)",
"http_requests_total{foo!=\"bar\", job=\"api-server\"}": "full",
"http_requests_total{foo!=\"bar\"}": "full",
"http_requests_total{foo!~\"bar\", job=\"api-server\", instance=\"1\", x!=\"y\", z=\"\", group!=\"\"}": "full",
"http_requests_total{foo!~\"bar\", job=\"api-server\"}": "full",
"http_requests_total{group!=\"canary\"}": "full",
"http_requests_total{group=\"production\",job=\"api-server\"} offset 5m": "full",
"http_requests_total{group=\"production\",job=~\"api-.+\"}": "full",
"http_requests_total{job!~\"api-.+\",group!=\"canary\"}": "full",
"http_requests_total{job=~\".+-server\",group!=\"canary\"}": "full",
"increase(http_requests_total[100m])": "full",
"increase(http_requests_total[30m])": "full",
"increase(http_requests_total[50m])": "full",
"increase(metric[1m])": "full",
"increase(metric[5m])": "full",
"label_join(series, \"idx\", \",\", \"label\", \"label\")": "fallback: instant-selector shape (last-sample-per-step engine path)",
"label_replace((((testmetric))), ((\"dst\")), ((\"value-$1\")), ((\"src\")), ((\"non-matching-regex\")))": "fallback: instant-selector shape (last-sample-per-step engine path)",
"label_replace(series, \"idx\", \"replaced\", \"idx\", \".*\")": "fallback: instant-selector shape (last-sample-per-step engine path)",
"label_replace(sum by (__name__) (rate(metric_total{env=\"2\"}[5m])), \"__name__\", \"$1\", \"__name__\", \"(.+)\")": "fallback: range shape with unsupported function(s): label_replace",
"label_replace(testmetric, \"dst\", \"\", \"dst\", \".*\")": "fallback: instant-selector shape (last-sample-per-step engine path)",
"label_replace(testmetric, \"dst\", \"$1-value-$2\", \"src\", \"(.*)-value-(.*)\")": "fallback: instant-selector shape (last-sample-per-step engine path)",
"label_replace(testmetric, \"dst\", \"destination-value-$1\", \"src\", \"source-value-(.*)\")": "fallback: instant-selector shape (last-sample-per-step engine path)",
"label_replace(testmetric, \"dst\", \"destination-value-$1\", \"src\", \"value-(.*)\")": "fallback: instant-selector shape (last-sample-per-step engine path)",
"label_replace(testmetric, \"dst\", \"value-$1\", \"nonexistent-src\", \"(.*)\")": "fallback: instant-selector shape (last-sample-per-step engine path)",
"label_replace(testmetric, \"dst\", \"value-$1\", \"nonexistent-src\", \"source-value-(.*)\")": "fallback: instant-selector shape (last-sample-per-step engine path)",
"label_replace(testmetric, \"dst\", \"value-$1\", \"src\", \"non-matching-regex\")": "fallback: instant-selector shape (last-sample-per-step engine path)",
"last_over_time(metric_total{env=\"1\"}[10m])": "full",
"max_over_time(metric_total{env=\"1\"}[10m])": "full",
"metric": "full",
"metric1 offset 15m or metric2 offset 45m": "fallback: instant-selector shape (last-sample-per-step engine path)",
"metric1_total offset +min(step(), 1s)^0": "fallback: duration expression (resolved only at evaluation time)",
"metric1_total offset -(min(step(), 1s))+8000": "fallback: duration expression (resolved only at evaluation time)",
"metric1_total offset -min(step(), 1s)+8000": "fallback: duration expression (resolved only at evaluation time)",
"metric1_total offset -min(step(), 1s)^0": "fallback: duration expression (resolved only at evaluation time)",
"metric1_total offset -step()*2": "fallback: duration expression (resolved only at evaluation time)",
"metric1_total offset 100 + 2": "full",
"metric1_total offset 2 ^ 2": "full",
"metric1_total offset STEP()": "fallback: duration expression (resolved only at evaluation time)",
"metric1_total offset max(3s,min(step(), 1s))+8000": "fallback: duration expression (resolved only at evaluation time)",
"metric1_total offset min(range(), 8s)": "fallback: duration expression (resolved only at evaluation time)",
"metric1_total offset min(step(), 1s)": "fallback: duration expression (resolved only at evaluation time)",
"metric1_total offset min(step(), 1s)+8000": "fallback: duration expression (resolved only at evaluation time)",
"metric1_total offset min(step(), 1s)^0": "fallback: duration expression (resolved only at evaluation time)",
"metric1_total offset range()": "fallback: duration expression (resolved only at evaluation time)",
"metric1_total offset step()": "fallback: duration expression (resolved only at evaluation time)",
"metric1_total offset step()*0": "fallback: duration expression (resolved only at evaluation time)",
"metric1_total offset step()^0": "fallback: duration expression (resolved only at evaluation time)",
"metricA + ignoring() metricB": "fallback: instant-selector shape (last-sample-per-step engine path)",
"metricA + metricB": "fallback: instant-selector shape (last-sample-per-step engine path)",
"metric_total * 2": "full",
"metric_total + another_metric_total": "fallback: instant-selector shape (last-sample-per-step engine path)",
"metric_total \u003c= another_metric_total": "fallback: instant-selector shape (last-sample-per-step engine path)",
"metric_total \u003c= bool another_metric_total": "fallback: instant-selector shape (last-sample-per-step engine path)",
"metric_total{env=\"1\"}": "full",
"min_over_time(metric_total[10s])": "full",
"min_over_time(metric_total[15s:10s])": "fallback: subquery",
"min_over_time(rate(metric_total[5m])[20m:1m])": "hybrid(1)",
"node_cpu % 2": "full",
"node_cpu * 2": "full",
"node_cpu * ignoring (role, mode) group_left (role) node_role": "fallback: instant-selector shape (last-sample-per-step engine path)",
"node_cpu * on (instance) group_left (role) node_role": "fallback: instant-selector shape (last-sample-per-step engine path)",
"node_cpu + 2": "full",
"node_cpu + on(dummy) group_left(foo) random*0": "hybrid(1)",
"node_cpu - 2": "full",
"node_cpu / 2": "full",
"node_cpu / ignoring (mode) group_left sum without (mode)(node_cpu)": "hybrid(1)",
"node_cpu / ignoring (mode) group_left(dummy) sum without (mode)(node_cpu)": "hybrid(1)",
"node_cpu / on (instance) group_left sum by (instance,job)(node_cpu)": "hybrid(1)",
"node_cpu \u003e on(job, instance) group_left(target) (threshold or on (job, instance) (sum by (job, instance)(node_cpu) * 0 + 1))": "hybrid(1)",
"node_cpu \u003e on(job, instance) group_left(target) threshold": "fallback: instant-selector shape (last-sample-per-step engine path)",
"node_cpu ^ 2": "full",
"node_role * ignoring (role) group_right (role) node_var": "fallback: instant-selector shape (last-sample-per-step engine path)",
"node_role * on (instance) group_right (role) node_var": "fallback: instant-selector shape (last-sample-per-step engine path)",
"node_var * ignoring (role) group_left (role) node_role": "fallback: instant-selector shape (last-sample-per-step engine path)",
"node_var * on (instance) group_left (role) node_role": "fallback: instant-selector shape (last-sample-per-step engine path)",
"other + fill": "fallback: instant-selector shape (last-sample-per-step engine path)",
"present_over_time(http_requests_total[10m])": "fallback: *_over_time range function",
"present_over_time(http_requests_total[16m])": "fallback: *_over_time range function",
"present_over_time(http_requests_total[5m])": "fallback: *_over_time range function",
"present_over_time(http_requests_total[6m])": "fallback: *_over_time range function",
"present_over_time(httpd_handshake_failures_total[1m])": "fallback: *_over_time range function",
"present_over_time(httpd_log_lines_total[30s])": "fallback: *_over_time range function",
"present_over_time(rate(http_requests_total[5m])[5m:1m])": "hybrid(1)",
"present_over_time({instance=\"127.0.0.1\"}[5m:5s])": "fallback: subquery",
"present_over_time({instance=\"127.0.0.1\"}[5m])": "fallback: *_over_time range function",
"present_over_time({job=\"grok\"}[20m])": "fallback: *_over_time range function",
"present_over_time({job=\"ingress\"}[4m])": "fallback: *_over_time range function",
"rad(trig - 10)": "hybrid(1)",
"rad(trig - 20)": "hybrid(1)",
"rad(trig)": "fallback: instant-selector shape (last-sample-per-step engine path)",
"random + on() metricA": "fallback: instant-selector shape (last-sample-per-step engine path)",
"rate(calculate_rate_offset_total[10m] offset 5m)": "full",
"rate(calculate_rate_window_total[50m])": "full",
"rate(http_requests_total[1m])": "full",
"rate(http_requests_total[40s]) - rate(http_requests_total[1m] offset 10000s)": "hybrid(2)",
"rate(http_requests_total{group=~\"((?i)PRO).*\"}[1m])": "full",
"rate(http_requests_total{group=~\"(?i:PRO).*\"}[1m])": "full",
"rate(http_requests_total{group=~\"(?i:PRODUCTION)\"}[1m])": "full",
"rate(http_requests_total{group=~\".*((?i)DUC).*\"}[1m])": "full",
"rate(http_requests_total{group=~\".*((?i)TION)\"}[1m])": "full",
"rate(http_requests_total{group=~\".*(?i:C).*\"}[1m])": "full",
"rate(http_requests_total{group=~\".*(?i:DUC).*\"}[1m])": "full",
"rate(http_requests_total{group=~\".*(?i:TION)\"}[1m])": "full",
"rate(http_requests_total{group=~\".*(?i:TION).*?\"}[1m])": "full",
"rate(http_requests_total{group=~\".*?(?i:PRO).*\"}[1m])": "full",
"rate(http_requests_total{group=~\".*ry\", instance=\"1\"}[1m])": "full",
"rate(http_requests_total{group=~\"pro.*\"}[1m:10s])": "fallback: subquery",
"rate(http_requests_total{group=~\"pro.*\"}[1m])": "full",
"rate(http_requests_total{instance!=\"3\"}[1m] offset 10000s)": "full",
"rate(metric_total[1m1s:10s])": "fallback: subquery",
"rate(metric_total[1m500ms:10s])": "fallback: subquery",
"rate(metric_total[1m])": "full",
"rate(metric_total[20s:10s])": "fallback: subquery",
"rate(metric_total[20s:5s])": "fallback: subquery",
"rate(metric_total{env=\"1\"}[10m])": "full",
"rate(sum_over_time((metric1_total+metric2_total+metric3_total)[30s:10s])[30s:10s])": "fallback: subquery",
"rate(sum_over_time(metric1_total[30s:10s])[50s:10s])": "fallback: subquery",
"rate(sum_over_time(metric2_total[30s:10s])[50s:10s])": "fallback: subquery",
"rate(sum_over_time(metric3_total[30s:10s])[50s:10s])": "fallback: subquery",
"rate(testcounter_reset_end_total[5m])": "full",
"rate(testcounter_reset_end_total[6m])": "full",
"rate(testcounter_reset_middle_total[50m])": "full",
"rate(testcounter_zero_cutoff_total[20m])": "full",
"requests * 2": "full",
"resets(metric[1m])": "fallback: range shape with unsupported function(s): resets",
"resets(metric[5m])": "fallback: range shape with unsupported function(s): resets",
"round(-1 * (0.004 * http_requests{group=\"production\",job=\"api-server\"}))": "hybrid(1)",
"round(-1 * (0.005 * http_requests{group=\"production\",job=\"api-server\"}))": "hybrid(1)",
"round(-1 * (1 + 0.005 * http_requests{group=\"production\",job=\"api-server\"}))": "hybrid(1)",
"round(-1 * (5.2 + 0.0005 * http_requests{group=\"production\",job=\"api-server\"}), 0.1)": "hybrid(1)",
"round(0.0005 * http_requests{group=\"production\",job=\"api-server\"}, 0.1)": "hybrid(1)",
"round(0.004 * http_requests{group=\"production\",job=\"api-server\"})": "hybrid(1)",
"round(0.005 * http_requests{group=\"production\",job=\"api-server\"})": "hybrid(1)",
"round(0.025 * http_requests{group=\"production\",job=\"api-server\"}, 5)": "hybrid(1)",
"round(0.045 * http_requests{group=\"production\",job=\"api-server\"}, 5)": "hybrid(1)",
"round(1 + 0.005 * http_requests{group=\"production\",job=\"api-server\"})": "hybrid(1)",
"round(2.1 + 0.0005 * http_requests{group=\"production\",job=\"api-server\"}, 0.1)": "hybrid(1)",
"round(5.2 + 0.0005 * http_requests{group=\"production\",job=\"api-server\"}, 0.1)": "hybrid(1)",
"round(metric_total)": "fallback: instant-selector shape (last-sample-per-step engine path)",
"sin(trig)": "fallback: instant-selector shape (last-sample-per-step engine path)",
"sinh(trig)": "fallback: instant-selector shape (last-sample-per-step engine path)",
"stddev (series)": "fallback: instant-selector shape (last-sample-per-step engine path)",
"stddev by (instance)(http_requests)": "fallback: instant-selector shape (last-sample-per-step engine path)",
"stddev by (label) (series)": "fallback: instant-selector shape (last-sample-per-step engine path)",
"stddev(http_requests)": "fallback: instant-selector shape (last-sample-per-step engine path)",
"stddev(series)": "fallback: instant-selector shape (last-sample-per-step engine path)",
"stddev_over_time(metric[1m])": "fallback: *_over_time range function",
"stdvar (series)": "fallback: instant-selector shape (last-sample-per-step engine path)",
"stdvar by (instance)(http_requests)": "fallback: instant-selector shape (last-sample-per-step engine path)",
"stdvar by (label) (series)": "fallback: instant-selector shape (last-sample-per-step engine path)",
"stdvar(http_requests)": "fallback: instant-selector shape (last-sample-per-step engine path)",
"stdvar(series)": "fallback: instant-selector shape (last-sample-per-step engine path)",
"stdvar_over_time(metric[1m])": "fallback: *_over_time range function",
"sum by () (http_requests{job=\"api-server\"})": "full",
"sum by (__name__) (metric_total{env=\"1\"} or rate(metric_total{env=\"2\"}[5m]))": "fallback: other range shape",
"sum by (__name__) (metric_total{env=\"1\"})": "fallback: instant-selector shape (last-sample-per-step engine path)",
"sum by (__name__) (metric_total{env=\"3\"} or rate(metric_total{env=\"2\"}[5m]))": "fallback: other range shape",
"sum by (__name__) (rate(metric_total{env=\"2\"}[5m]) or metric_total{env=\"1\"})": "fallback: other range shape",
"sum by (__name__) (rate(metric_total{env=\"2\"}[5m]))": "fallback: other range shape",
"sum by (__name__) (rate(metric_total{env=\"3\"}[5m]) or metric_total{env=\"1\"})": "fallback: other range shape",
"sum by (__name__, env) (metric_total{env=\"1\"})": "fallback: instant-selector shape (last-sample-per-step engine path)",
"sum by (group) (data{test=\"nan\"})": "full",
"sum by (group) (data{test=\"neg_inf\"})": "full",
"sum by (group) (data{test=\"pos_inf\"})": "full",
"sum by (group) (http_requests{job=\"api-server\"})": "full",
"sum by (mode, job)(node_cpu) / on (job) group_left sum by (job)(node_cpu)": "hybrid(2)",
"sum without () (http_requests{job=\"api-server\",group=\"production\"})": "full",
"sum without (instance) (http_requests{job=\"api-server\"} or foo)": "fallback: instant-selector shape (last-sample-per-step engine path)",
"sum without (instance) (http_requests{job=\"api-server\"})": "full",
"sum without (instance)(node_cpu) / ignoring (mode) group_left sum without (instance, mode)(node_cpu)": "hybrid(2)",
"sum(data{test=\"inf_inf\"})": "full",
"sum(data{test=\"ten\"})": "full",
"sum(http_requests) by (job) + min(http_requests) by (job) + max(http_requests) by (job) + avg(http_requests) by (job)": "hybrid(4)",
"sum(http_requests{job=\"api-server\"})": "full",
"sum(label_grouping_test) by (a, b)": "full",
"sum(sum by (group) (http_requests{job=\"api-server\"})) by (job)": "hybrid(1)",
"sum(sum by (mode, job)(node_cpu) / on (job) group_left sum by (job)(node_cpu))": "hybrid(2)",
"sum(sum without (instance)(node_cpu) / ignoring (mode) group_left sum without (instance, mode)(node_cpu))": "hybrid(2)",
"sum_over_time((metric1_total)[30:10] offset 3)": "fallback: subquery",
"sum_over_time((metric1_total)[30:10] offset 3s)": "fallback: subquery",
"sum_over_time((metric1_total)[30:10s] offset 3s)": "fallback: subquery",
"sum_over_time((metric1_total)[30s:10s] offset 3s)": "fallback: subquery",
"sum_over_time(bar[30s])": "full",
"sum_over_time(metric1_total[30:10] offset 3)": "fallback: subquery",
"sum_over_time(metric1_total[30s:10s] offset 10s)": "fallback: subquery",
"sum_over_time(metric1_total[30s:10s] offset 3s)": "fallback: subquery",
"sum_over_time(metric1_total[30s:10s] offset 5s)": "fallback: subquery",
"sum_over_time(metric1_total[30s:10s] offset 7s)": "fallback: subquery",
"sum_over_time(metric1_total[30s:10s] offset 9s)": "fallback: subquery",
"sum_over_time(metric1_total[30s:10s])": "fallback: subquery",
"sum_over_time(metric1_total[30s:5s])": "fallback: subquery",
"sum_over_time(metric[1000ms])": "full",
"sum_over_time(metric[1001ms])": "fallback: *_over_time range function",
"sum_over_time(metric[1002ms])": "fallback: *_over_time range function",
"sum_over_time(metric[1003ms])": "fallback: *_over_time range function",
"sum_over_time(metric[2000ms])": "full",
"sum_over_time(metric[2001ms])": "fallback: *_over_time range function",
"sum_over_time(metric[2002ms])": "fallback: *_over_time range function",
"sum_over_time(metric[2003ms])": "fallback: *_over_time range function",
"sum_over_time(metric[2m])": "full",
"sum_over_time(metric[3000ms])": "full",
"sum_over_time(metric[3001ms])": "fallback: *_over_time range function",
"sum_over_time(metric[3002ms])": "fallback: *_over_time range function",
"sum_over_time(metric[3003ms])": "fallback: *_over_time range function",
"sum_over_time(metric_total[50s:10s])": "fallback: subquery",
"sum_over_time(metric_total[50s:5s])": "fallback: subquery",
"sum_over_time(metric_total[60s:10s])": "fallback: subquery",
"tan(trig)": "fallback: instant-selector shape (last-sample-per-step engine path)",
"tanh(trig)": "fallback: instant-selector shape (last-sample-per-step engine path)",
"test_total \u003c bool test_smaller": "fallback: instant-selector shape (last-sample-per-step engine path)",
"test_total \u003c test_smaller": "fallback: instant-selector shape (last-sample-per-step engine path)",
"test_total \u003e bool test_smaller": "fallback: instant-selector shape (last-sample-per-step engine path)",
"test_total \u003e test_smaller": "fallback: instant-selector shape (last-sample-per-step engine path)",
"testmetric": "full",
"topk(10, sum by (__name__, env) (metric_total{env=\"1\"}))": "fallback: instant-selector shape (last-sample-per-step engine path)",
"topk(10, sum by (__name__, env) (rate(metric_total{env=\"1\"}[10m])))": "fallback: other range shape",
"trigy atan2 trigNaN": "fallback: instant-selector shape (last-sample-per-step engine path)",
"trigy atan2 trigx": "fallback: instant-selector shape (last-sample-per-step engine path)",
"x{y=\"testvalue\"}": "full",
"{__name__=~\".+\"}": "full",
"{job=~\".+-server\", job!~\"api-.+\"}": "full"
}

View File

@@ -0,0 +1,470 @@
package clickhouseprometheusv2
import (
"fmt"
"strings"
"github.com/prometheus/prometheus/model/labels"
"github.com/prometheus/prometheus/promql/parser"
)
// The transpiler turns allowlisted PromQL subtrees into single ClickHouse
// statements on the timeSeries*ToGrid aggregate functions. Every other
// shape falls back to the engine over this package's querier. See
// docs/contributing/prometheus.md for the model, the allowlist, and the
// correctness argument of each form.
type rangeFn string
const (
fnRate rangeFn = "rate"
fnIncrease rangeFn = "increase"
fnDelta rangeFn = "delta"
fnIRate rangeFn = "irate"
fnIDelta rangeFn = "idelta"
)
var gridFunction = map[rangeFn]string{
fnRate: "timeSeriesRateToGrid",
fnIncrease: "timeSeriesRateToGrid", // increase == rate * range seconds, exactly (same factor algebra)
fnDelta: "timeSeriesDeltaToGrid",
fnIRate: "timeSeriesInstantRateToGrid",
fnIDelta: "timeSeriesInstantDeltaToGrid",
}
// scalarOp runs in Go during assembly, with the same float64 operations
// the engine uses.
type scalarOp struct {
op parser.ItemType
scalar float64
scalarOnLeft bool
returnBool bool
}
// Comparisons preserve the metric name; arithmetic drops it.
func (o scalarOp) isComparison() bool {
return o.op.IsComparisonOperator()
}
type unitKind int
const (
// unitRange: rate/increase/delta/irate/idelta over a matrix selector.
unitRange unitKind = iota
// unitInstant: a plain vector selector resolved per grid point with
// lookback and stale-marker shadowing.
unitInstant
// unitOverTime: avg/min/max/sum/count/last_over_time over a matrix
// selector (aggregation over the window's samples, stale rows excluded).
unitOverTime
)
// coreUnit is one transpilable subtree: selector [-> range function] ->
// optional aggregation -> scalar-op pipeline.
type coreUnit struct {
kind unitKind
matchers []*labels.Matcher
offsetMs int64
fn rangeFn // unitRange
overFn string // unitOverTime: avg|min|max|sum|count|last
rangeMs int64 // unitRange/unitOverTime window
hasAgg bool
aggOp parser.ItemType // SUM MIN MAX AVG COUNT
by bool
grouping []string
ops []scalarOp
}
// keepsName reports whether the unit's output series keep their real
// __name__. Bare and comparison-filtered instant selectors keep it, and so
// does last_over_time: they return the raw sample, name included. Range
// functions, the other *_over_time functions, aggregations, arithmetic, and
// bool comparisons all drop it. A bool comparison returns 0/1, not the
// sample, so the engine drops the name there too. A unit that keeps the
// name cannot become a synthetic series in a hybrid plan: the synthetic
// name would replace the real one. It transpiles fine as a full plan, where
// assembly emits the real names.
func (u *coreUnit) keepsName() bool {
nameKeepingSelector := u.kind == unitInstant || (u.kind == unitOverTime && u.overFn == "last")
if !nameKeepingSelector || u.hasAgg {
return false
}
for _, op := range u.ops {
if !op.isComparison() || op.returnBool {
return false
}
}
return true
}
// gridContext is the evaluation grid a unit computes on. The query grid for
// top-level units; for units inside subqueries, the subquery's own grid:
// epoch-aligned multiples of its resolution covering the subquery window,
// exactly as the engine derives it (engine.go, *parser.SubqueryExpr case).
type gridContext struct {
startMs int64
endMs int64
stepMs int64
}
// subqueryGrid derives the inner grid for a subquery evaluated on outer:
// interval S, end = outer end offset, start = first multiple of S strictly
// greater than outer start offset range.
func subqueryGrid(outer gridContext, rangeMs, stepMs, offsetMs int64) gridContext {
lower := outer.startMs - offsetMs - rangeMs
start := stepMs * (lower / stepMs)
if start <= lower {
start += stepMs
}
return gridContext{startMs: start, endMs: outer.endMs - offsetMs, stepMs: stepMs}
}
type transpiledUnit struct {
core coreUnit
name string // __signoz_transpiled_<n>__
grid gridContext
}
type transpilePlan struct {
units []*transpiledUnit
grid gridContext // the query's top-level grid
// full is set when the entire query is units[0]; otherwise rewritten
// holds the query with each unit replaced by a synthetic selector, to be
// evaluated by the engine over a hybrid storage.
full bool
rewritten string
}
const syntheticNamePrefix = "__signoz_transpiled_"
func syntheticName(i int) string {
return fmt.Sprintf("%s%d__", syntheticNamePrefix, i)
}
// classifyCore matches a subtree against the transpilable core shape.
// stepMs gates second-granularity: the grid functions take whole-second step
// and window parameters (grid *starts* are millisecond-precise).
func classifyCore(node parser.Expr, stepMs int64) (*coreUnit, bool) {
unit := &coreUnit{}
expr := node
// Peel scalar ops and parens off the top, outermost first; ops apply in
// evaluation order, so prepend while peeling.
for {
switch n := expr.(type) {
case *parser.ParenExpr:
expr = n.Expr
continue
case *parser.UnaryExpr:
if n.Op != parser.SUB {
expr = n.Expr // unary '+' is a no-op
continue
}
// -x == -1 * x for every float64 (incl. NaN and signed zero).
unit.ops = append([]scalarOp{{op: parser.MUL, scalar: -1}}, unit.ops...)
expr = n.Expr
continue
case *parser.StepInvariantExpr:
// @-pinned expressions evaluate on a different grid.
return nil, false
case *parser.BinaryExpr:
lit, litOnLeft, ok := numberLiteralSide(n)
if !ok {
return nil, false
}
if !n.Op.IsOperator() && !n.Op.IsComparisonOperator() {
return nil, false
}
if n.Op == parser.ATAN2 {
// atan2 is arithmetic in PromQL but rarely used; keep the
// allowlist tight.
return nil, false
}
returnBool := n.ReturnBool
unit.ops = append([]scalarOp{{op: n.Op, scalar: lit, scalarOnLeft: litOnLeft, returnBool: returnBool}}, unit.ops...)
if litOnLeft {
expr = n.RHS
} else {
expr = n.LHS
}
continue
}
break
}
// Optional aggregation.
if agg, ok := expr.(*parser.AggregateExpr); ok {
switch agg.Op {
case parser.SUM, parser.MIN, parser.MAX, parser.AVG, parser.COUNT:
default:
return nil, false
}
for _, g := range agg.Grouping {
if g == metricNameLabel {
// by(__name__)/without(__name__) over synthetic or compiled
// output needs name bookkeeping the compiler doesn't do.
return nil, false
}
}
unit.hasAgg = true
unit.aggOp = agg.Op
unit.by = !agg.Without
unit.grouping = agg.Grouping
expr = agg.Expr
for {
if p, ok := expr.(*parser.ParenExpr); ok {
expr = p.Expr
continue
}
break
}
}
// The grid functions take whole-second steps; stepMs == 0 is an instant
// query (single-point grid).
if stepMs < 0 || stepMs%1000 != 0 {
return nil, false
}
// Bare instant selector: resolved per grid point with lookback and
// stale-marker shadowing (see compiler_sql.go).
if vs, ok := expr.(*parser.VectorSelector); ok {
// A duration expression (offset step(), offset range()*2, ...) is
// resolved into OriginalOffset only at evaluation time; at
// classification time the field still holds its zero value, so
// transpiling would silently use the wrong offset.
if vs.Timestamp != nil || vs.StartOrEnd != 0 || vs.Anchored || vs.Smoothed || vs.OriginalOffsetExpr != nil {
return nil, false
}
offsetMs := vs.OriginalOffset.Milliseconds()
if offsetMs < 0 {
return nil, false
}
unit.kind = unitInstant
unit.offsetMs = offsetMs
unit.matchers = vs.LabelMatchers
return unit, true
}
// Range or *_over_time function over a plain matrix selector.
call, ok := expr.(*parser.Call)
if !ok {
return nil, false
}
var fn rangeFn
var overFn string
switch call.Func.Name {
case "rate":
fn = fnRate
case "increase":
fn = fnIncrease
case "delta":
fn = fnDelta
case "irate":
fn = fnIRate
case "idelta":
fn = fnIDelta
case "avg_over_time", "min_over_time", "max_over_time", "sum_over_time", "count_over_time", "last_over_time":
overFn = strings.TrimSuffix(call.Func.Name, "_over_time")
default:
return nil, false
}
if len(call.Args) != 1 {
return nil, false
}
ms, ok := call.Args[0].(*parser.MatrixSelector)
if !ok {
return nil, false
}
vs, ok := ms.VectorSelector.(*parser.VectorSelector)
if !ok {
return nil, false
}
// Duration expressions resolve at evaluation time (see the instant
// selector case above); Range/OriginalOffset would be read as zero here.
if vs.Timestamp != nil || vs.StartOrEnd != 0 || vs.Anchored || vs.Smoothed || vs.OriginalOffsetExpr != nil || ms.RangeExpr != nil {
return nil, false
}
rangeMs := ms.Range.Milliseconds()
offsetMs := vs.OriginalOffset.Milliseconds()
if rangeMs <= 0 || rangeMs%1000 != 0 || offsetMs < 0 {
return nil, false
}
if overFn != "" {
unit.kind = unitOverTime
unit.overFn = overFn
} else {
unit.kind = unitRange
unit.fn = fn
}
unit.rangeMs = rangeMs
unit.offsetMs = offsetMs
unit.matchers = vs.LabelMatchers
return unit, true
}
// numberLiteralSide returns the number literal on one side of a binary
// expression (peeling parens and unary minus), and which side it is on.
func numberLiteralSide(b *parser.BinaryExpr) (float64, bool, bool) {
if v, ok := literalValue(b.LHS); ok {
return v, true, true
}
if v, ok := literalValue(b.RHS); ok {
return v, false, true
}
return 0, false, false
}
func literalValue(e parser.Expr) (float64, bool) {
neg := false
for {
switch n := e.(type) {
case *parser.ParenExpr:
e = n.Expr
continue
case *parser.StepInvariantExpr:
e = n.Expr
continue
case *parser.UnaryExpr:
if n.Op == parser.SUB {
neg = !neg
}
e = n.Expr
continue
case *parser.NumberLiteral:
if neg {
return -n.Val, true
}
return n.Val, true
default:
return 0, false
}
}
}
// classify builds the compile plan for a query: full when the root is a core
// unit, hybrid when core units sit strictly below the root (including inside
// fixed-resolution subqueries, computed on the subquery grid), none
// otherwise.
func classify(root parser.Expr, grid gridContext) (*transpilePlan, bool) {
if unit, ok := classifyCore(root, grid.stepMs); ok {
return &transpilePlan{
units: []*transpiledUnit{{core: *unit, name: syntheticName(0), grid: grid}},
grid: grid,
full: true,
}, true
}
plan := &transpilePlan{grid: grid}
rewritten := rewrite(root, grid, plan, false)
if len(plan.units) == 0 {
return nil, false
}
plan.rewritten = rewritten.String()
return plan, true
}
// rewrite walks top-down replacing maximal transpilable subtrees with synthetic
// vector selectors. nameSensitive marks scopes where an ancestor's semantics
// depend on __name__ (grouping or vector matching on it): synthetic series
// carry a synthetic __name__, so substitution there would change results.
// Fixed-resolution subqueries recurse with the subquery's own grid; scopes
// whose evaluation grid is unknowable (@-pinned, default-resolution
// subqueries) are not entered.
func rewrite(node parser.Expr, grid gridContext, plan *transpilePlan, nameSensitive bool) parser.Expr {
if node == nil {
return nil
}
if !nameSensitive {
// Units whose output keeps the real __name__ (bare instant selectors)
// cannot be substituted: the synthetic name would replace it in the
// engine's output. They still compile as full plans.
if unit, ok := classifyCore(node, grid.stepMs); ok && !unit.keepsName() {
cu := &transpiledUnit{core: *unit, name: syntheticName(len(plan.units)), grid: grid}
plan.units = append(plan.units, cu)
return &parser.VectorSelector{
Name: cu.name,
LabelMatchers: []*labels.Matcher{
labels.MustNewMatcher(labels.MatchEqual, metricNameLabel, cu.name),
},
PosRange: node.PositionRange(),
}
}
}
switch n := node.(type) {
case *parser.ParenExpr:
n.Expr = rewrite(n.Expr, grid, plan, nameSensitive)
case *parser.UnaryExpr:
n.Expr = rewrite(n.Expr, grid, plan, nameSensitive)
case *parser.AggregateExpr:
sensitive := nameSensitive || groupingUsesName(n.Grouping)
n.Expr = rewrite(n.Expr, grid, plan, sensitive)
// n.Param is a scalar/string; nothing transpilable inside for our core.
case *parser.Call:
for i, arg := range n.Args {
n.Args[i] = rewrite(arg, grid, plan, nameSensitive)
}
case *parser.BinaryExpr:
sensitive := nameSensitive || vectorMatchingUsesName(n.VectorMatching)
n.LHS = rewrite(n.LHS, grid, plan, sensitive)
n.RHS = rewrite(n.RHS, grid, plan, sensitive)
case *parser.SubqueryExpr:
// The alert-smoothing idiom fn_over_time((expr)[R:S]) dominates real
// rule fleets; inner units evaluate on the subquery grid, and the
// engine does the smoothing over the synthetic series. Requires an
// explicit whole-second resolution (S == 0 needs the engine's
// default-interval function) and no @ pinning.
stepMs := n.Step.Milliseconds()
rangeMs := n.Range.Milliseconds()
offsetMs := n.OriginalOffset.Milliseconds()
if n.Timestamp == nil && n.StartOrEnd == 0 &&
n.RangeExpr == nil && n.StepExpr == nil && n.OriginalOffsetExpr == nil &&
stepMs > 0 && stepMs%1000 == 0 && rangeMs%1000 == 0 && offsetMs >= 0 {
inner := subqueryGrid(grid, rangeMs, stepMs, offsetMs)
n.Expr = rewrite(n.Expr, inner, plan, nameSensitive)
}
case *parser.StepInvariantExpr, *parser.MatrixSelector,
*parser.VectorSelector, *parser.NumberLiteral, *parser.StringLiteral:
// Leaves, or scopes substitution must not enter.
}
return node
}
func groupingUsesName(grouping []string) bool {
for _, g := range grouping {
if g == metricNameLabel {
return true
}
}
return false
}
func vectorMatchingUsesName(vm *parser.VectorMatching) bool {
if vm == nil {
return false
}
for _, l := range append(append([]string{}, vm.MatchingLabels...), vm.Include...) {
if l == metricNameLabel {
return true
}
}
// Default (all-labels) matching ignores __name__, and by()/ignoring()
// lists were checked above.
return false
}
// isSyntheticSelector reports whether matchers target a compiled unit.
func isSyntheticSelector(matchers []*labels.Matcher) (string, bool) {
for _, m := range matchers {
if m.Name == metricNameLabel && m.Type == labels.MatchEqual && strings.HasPrefix(m.Value, syntheticNamePrefix) {
return m.Value, true
}
}
return "", false
}

View File

@@ -0,0 +1,541 @@
package clickhouseprometheusv2
import (
"context"
"encoding/json"
"math"
"sort"
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/prometheus"
"github.com/prometheus/prometheus/model/labels"
promValue "github.com/prometheus/prometheus/model/value"
"github.com/prometheus/prometheus/promql"
"github.com/prometheus/prometheus/promql/parser"
"github.com/prometheus/prometheus/storage"
"golang.org/x/sync/errgroup"
)
type executor struct {
client *client
engine *prometheus.Engine
parser prometheus.Parser
}
// maxWindowBuckets caps range/step for the windowed *_over_time form.
// Every grid slot combines that many bucket partials. The fleet's windows
// sit well under the cap ([1m]..[17m] at 30-60s steps). Anything larger is
// a long-range query whose step a dashboard scales up anyway. The engine
// path serves the rest.
const maxWindowBuckets = 64
func (e *executor) TryExecuteRange(ctx context.Context, qs string, start, end time.Time, step time.Duration) (promql.Matrix, bool, error) {
expr, err := e.parser.ParseExpr(qs)
if err != nil {
// Let the engine path produce the (enhanced) parse error.
return nil, false, nil
}
plan, ok := classify(expr, queryGrid(start, end, step))
if !ok {
return nil, false, nil
}
// timeSeriesLastToGrid widens its window to max(window, step). We
// probed this: a sample aged (window, step] still fills the slot. The
// rate/delta family enforces the window strictly. The Last-style kinds
// used to fall back when window < step because of that widening. The
// window-sliver filter (see samplesConditions) makes the widening
// harmless there: samples exist only inside (t_k - window, t_k]
// slivers, so the widened window intersected with the data IS the
// lookback window. If a future ClickHouse stops widening, the
// unwidened window is the sliver too. Correct either way. A
// non-positive window still falls back: the sliver argument needs a
// real window to filter to.
//
// The windowed *_over_time form gates only the range >= step regime.
// It decomposes the window into whole step buckets (see windowedInner).
// That is exact only when the range is a multiple of the step. The
// per-slot slide costs range/step bucket combines; maxWindowBuckets
// bounds it, so a long-range short-step query cannot turn the slide
// into the bottleneck. range < step needs neither gate: the windows
// are disjoint slivers, aggregated one slot each, with no slide. Every
// miss falls back to the engine path, which is exact.
for _, unit := range plan.units {
stepMs := unit.grid.stepMs
if stepMs == 0 {
stepMs = 1000
}
switch {
case unit.core.kind == unitInstant || (unit.core.kind == unitOverTime && unit.core.overFn == "last"):
windowMs := unit.core.rangeMs
if unit.core.kind == unitInstant {
windowMs = e.client.lookbackMs
}
if windowMs <= 0 {
return nil, false, nil
}
case unit.core.kind == unitOverTime:
if unit.core.rangeMs < unit.grid.stepMs {
// Disjoint slivers: no divisibility or width requirement.
continue
}
if unit.core.rangeMs%stepMs != 0 || unit.core.rangeMs/stepMs > maxWindowBuckets {
return nil, false, nil
}
}
}
// Evaluate every unit concurrently on its own grid (the query grid, or a
// subquery grid); each is one series lookup plus one grid query.
results := make([][]transpiledSeries, len(plan.units))
eg, egCtx := errgroup.WithContext(ctx)
for i, unit := range plan.units {
eg.Go(func() error {
res, err := e.executeUnit(egCtx, &unit.core, unit.grid)
if err != nil {
return err
}
results[i] = res
return nil
})
}
if err := eg.Wait(); err != nil {
return nil, true, err
}
if plan.full {
g := plan.units[0].grid
return toMatrix(results[0], g.startMs, g.stepMs), true, nil
}
matrix, err := e.executeHybrid(ctx, plan, results)
if err != nil {
return nil, true, err
}
return matrix, true, nil
}
// A step of 0 is an instant query: a single evaluation at end, whatever
// start was.
func queryGrid(start, end time.Time, step time.Duration) gridContext {
startMs, endMs, stepMs := start.UnixMilli(), end.UnixMilli(), step.Milliseconds()
if stepMs == 0 {
startMs = endMs
}
return gridContext{startMs: startMs, endMs: endMs, stepMs: stepMs}
}
// transpiledSeries holds one value pointer per grid point; nil is absent.
type transpiledSeries struct {
lset labels.Labels
values []*float64
}
func (e *executor) executeUnit(ctx context.Context, unit *coreUnit, grid gridContext) ([]transpiledSeries, error) {
startMs, endMs, stepMs := grid.startMs, grid.endMs, grid.stepMs
windowMs := unit.rangeMs
if unit.kind == unitInstant {
windowMs = e.client.lookbackMs
}
dataStart := startMs - unit.offsetMs - windowMs
dataEnd := endMs - unit.offsetMs
seriesQuery, seriesArgs, err := buildSeriesQuery(dataStart, dataEnd, unit.matchers)
if err != nil {
return nil, err
}
lookup, err := e.client.selectSeries(ctx, seriesQuery, seriesArgs)
if err != nil {
return nil, err
}
if len(lookup.fingerprints) == 0 {
return nil, nil
}
query, args, err := buildUnitSQL(unit, lookup.metricNames, dataStart, dataEnd, startMs, endMs, stepMs, e.client.lookbackMs)
if err != nil {
return nil, err
}
rows, err := e.client.telemetryStore.ClickhouseDB().Query(e.client.withContext(ctx, "transpiledUnit"), query, args...)
if err != nil {
return nil, err
}
defer rows.Close()
// Name-dropping units keep __name__ in the SQL group key, so distinct
// metrics never merge server-side. The name comes off here. Two
// metrics can then share a labelset. The engine merges their samples
// into one series when they never overlap in time. It raises the
// duplicate-labelset error only when two samples land on the same
// evaluation timestamp. mergeSameLabelsetSeries reproduces exactly
// that.
stripName := !unit.hasAgg && !unit.keepsName()
// by (...) units return one plain column per grouped label. Everything
// else returns the single canonical JSON key (see groupKeyColumns).
keyNames := groupKeyColumns(unit)
keyVals := make([]string, max(len(keyNames), 1))
targets := make([]any, 0, len(keyVals)+1)
for i := range keyVals {
targets = append(targets, &keyVals[i])
}
var gridValues []*float64
targets = append(targets, &gridValues)
var out []transpiledSeries
for rows.Next() {
if err := rows.Scan(targets...); err != nil {
return nil, err
}
var lset labels.Labels
if keyNames != nil {
builder := labels.NewScratchBuilder(len(keyNames))
for i, name := range keyNames {
// An empty extracted value is the label being absent.
if keyVals[i] != "" {
builder.Add(name, keyVals[i])
}
}
builder.Sort()
lset = builder.Labels()
} else {
lset, err = labelsFromGroupKey(keyVals[0])
if err != nil {
return nil, err
}
}
if stripName {
lset = labels.NewBuilder(lset).Del(metricNameLabel).Labels()
}
values := make([]*float64, len(gridValues))
copy(values, gridValues)
applyScalarOps(unit.ops, values)
out = append(out, transpiledSeries{lset: lset, values: values})
}
if err := rows.Err(); err != nil {
return nil, err
}
if stripName {
if out, err = mergeSameLabelsetSeries(out); err != nil {
return nil, err
}
}
sort.Slice(out, func(i, j int) bool { return labels.Compare(out[i].lset, out[j].lset) < 0 })
return out, nil
}
// mergeSameLabelsetSeries combines series that a name strip left with
// identical labelsets, slot by slot. The engine assembles its result matrix
// by labelset. Post-strip twins whose points interleave in time are one
// series to it. Two values on the same evaluation timestamp are its
// duplicate-labelset error. v1 errors there too, so to silently pick one
// value would be a divergence.
func mergeSameLabelsetSeries(in []transpiledSeries) ([]transpiledSeries, error) {
index := make(map[uint64]int, len(in))
out := in[:0]
for _, s := range in {
hash := s.lset.Hash()
idx, ok := index[hash]
if ok && labels.Equal(out[idx].lset, s.lset) {
dst := out[idx].values
for k, v := range s.values {
if v == nil {
continue
}
if dst[k] != nil {
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "vector cannot contain metrics with the same labelset")
}
dst[k] = v
}
continue
}
index[hash] = len(out)
out = append(out, s)
}
return out, nil
}
// labelsFromGroupKey parses the toJSONString'd sorted [key, value] pairs.
func labelsFromGroupKey(gkey string) (labels.Labels, error) {
var pairs [][]string
if err := json.Unmarshal([]byte(gkey), &pairs); err != nil {
return labels.EmptyLabels(), errors.WrapInternalf(err, errors.CodeInternal, "malformed compiled group key %q", gkey)
}
builder := labels.NewScratchBuilder(len(pairs))
for _, p := range pairs {
if len(p) != 2 {
return labels.EmptyLabels(), errors.NewInternalf(errors.CodeInternal, "malformed compiled group key pair %q", gkey)
}
builder.Add(p[0], p[1])
}
builder.Sort()
return builder.Labels(), nil
}
// applyScalarOps applies the number-literal op pipeline in place, with the
// same float64 arithmetic and comparison-filter semantics as the engine.
func applyScalarOps(ops []scalarOp, values []*float64) {
for _, op := range ops {
for i, v := range values {
if v == nil {
continue
}
lhs, rhs := *v, op.scalar
if op.scalarOnLeft {
lhs, rhs = op.scalar, *v
}
switch op.op {
case parser.ADD:
res := lhs + rhs
values[i] = &res
case parser.SUB:
res := lhs - rhs
values[i] = &res
case parser.MUL:
res := lhs * rhs
values[i] = &res
case parser.DIV:
res := lhs / rhs
values[i] = &res
case parser.MOD:
res := math.Mod(lhs, rhs)
values[i] = &res
case parser.POW:
res := math.Pow(lhs, rhs)
values[i] = &res
default:
keep := compare(op.op, lhs, rhs)
switch {
case op.returnBool:
res := 0.0
if keep {
res = 1.0
}
values[i] = &res
case keep:
// Filter comparisons keep the vector-side value.
vec := *v
values[i] = &vec
default:
values[i] = nil
}
}
}
}
}
func compare(op parser.ItemType, lhs, rhs float64) bool {
switch op {
case parser.EQLC:
return lhs == rhs
case parser.NEQ:
return lhs != rhs
case parser.GTR:
return lhs > rhs
case parser.LSS:
return lhs < rhs
case parser.GTE:
return lhs >= rhs
case parser.LTE:
return lhs <= rhs
}
return false
}
// toMatrix converts a unit result to a promql matrix on the query grid.
func toMatrix(series []transpiledSeries, startMs, stepMs int64) promql.Matrix {
matrix := make(promql.Matrix, 0, len(series))
for _, s := range series {
var floats []promql.FPoint
for i, v := range s.values {
if v == nil {
continue
}
floats = append(floats, promql.FPoint{T: startMs + int64(i)*stepMs, F: *v})
}
if len(floats) == 0 {
continue
}
matrix = append(matrix, promql.Series{Metric: s.lset, Floats: floats})
}
return matrix
}
// executeHybrid substitutes each unit's grids into the engine as synthetic
// series. It evaluates the rewritten query over a storage that serves
// synthetic selectors from memory and everything else from the live
// querier. Absent grid points become stale markers, so the engine's
// lookback cannot resurrect the previous grid point. Each unit's synthetic
// samples sit on its own grid: the query grid, or the subquery grid for
// units inside subqueries.
func (e *executor) executeHybrid(ctx context.Context, plan *transpilePlan, results [][]transpiledSeries) (promql.Matrix, error) {
synthetic := make(map[string][]*series, len(plan.units))
staleMarker := math.Float64frombits(promValue.StaleNaN)
queryGrid := plan.grid
for i, unit := range plan.units {
g := unit.grid
gridLen := 1
if g.stepMs > 0 {
gridLen = int((g.endMs-g.startMs)/g.stepMs) + 1
}
list := make([]*series, 0, len(results[i]))
for _, cs := range results[i] {
builder := labels.NewBuilder(cs.lset)
builder.Set(metricNameLabel, unit.name)
s := &series{lset: builder.Labels()}
s.ts = make([]int64, 0, gridLen)
s.vs = make([]float64, 0, gridLen)
for idx := 0; idx < gridLen; idx++ {
t := g.startMs + int64(idx)*g.stepMs
var v float64
if idx < len(cs.values) && cs.values[idx] != nil {
v = *cs.values[idx]
} else {
v = staleMarker
}
s.ts = append(s.ts, t)
s.vs = append(s.vs, v)
}
list = append(list, s)
}
synthetic[unit.name] = list
}
hybrid := &hybridQueryable{client: e.client, synthetic: synthetic}
var qry promql.Query
var err error
if queryGrid.stepMs == 0 {
qry, err = e.engine.NewInstantQuery(ctx, hybrid, nil, plan.rewritten, time.UnixMilli(queryGrid.endMs))
} else {
qry, err = e.engine.NewRangeQuery(ctx, hybrid, nil, plan.rewritten, time.UnixMilli(queryGrid.startMs), time.UnixMilli(queryGrid.endMs), time.Duration(queryGrid.stepMs)*time.Millisecond)
}
if err != nil {
return nil, err
}
defer qry.Close()
res := qry.Exec(ctx)
if res.Err != nil {
return nil, res.Err
}
matrix, err := resultToMatrix(res)
if err != nil {
return nil, err
}
// Deep-copy before Close returns the result's slices to the engine pool,
// and drop the synthetic __name__ that filter comparisons preserve.
out := make(promql.Matrix, 0, len(matrix))
for _, s := range matrix {
lset := s.Metric
if name := lset.Get(metricNameLabel); len(name) >= len(syntheticNamePrefix) && name[:len(syntheticNamePrefix)] == syntheticNamePrefix {
builder := labels.NewBuilder(lset)
builder.Del(metricNameLabel)
lset = builder.Labels()
}
floats := make([]promql.FPoint, len(s.Floats))
copy(floats, s.Floats)
out = append(out, promql.Series{Metric: lset.Copy(), Floats: floats})
}
// The strip can leave twins: two units' outputs that only their
// synthetic names told apart (e.g. -metric_a or -metric_b, both {}
// once real names are dropped). The engine assembles its matrix by
// labelset. It merges such temporally-disjoint elements into one
// series. Reproduce that, with its duplicate error on same-timestamp
// overlap.
out, err = mergeMatrixByLabelset(out)
if err != nil {
return nil, err
}
sort.Slice(out, func(i, j int) bool { return labels.Compare(out[i].Metric, out[j].Metric) < 0 })
return out, nil
}
// mergeMatrixByLabelset merges series that share a labelset. It interleaves
// their points in timestamp order. A timestamp present in both is the
// engine's duplicate-labelset error.
func mergeMatrixByLabelset(matrix promql.Matrix) (promql.Matrix, error) {
index := make(map[uint64]int, len(matrix))
out := matrix[:0]
for _, s := range matrix {
hash := s.Metric.Hash()
idx, ok := index[hash]
if ok && labels.Equal(out[idx].Metric, s.Metric) {
merged := make([]promql.FPoint, 0, len(out[idx].Floats)+len(s.Floats))
a, b := out[idx].Floats, s.Floats
for len(a) > 0 && len(b) > 0 {
switch {
case a[0].T < b[0].T:
merged, a = append(merged, a[0]), a[1:]
case b[0].T < a[0].T:
merged, b = append(merged, b[0]), b[1:]
default:
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "vector cannot contain metrics with the same labelset")
}
}
out[idx].Floats = append(append(merged, a...), b...)
continue
}
index[hash] = len(out)
out = append(out, s)
}
return out, nil
}
func resultToMatrix(res *promql.Result) (promql.Matrix, error) {
switch v := res.Value.(type) {
case promql.Matrix:
return v, nil
case promql.Vector:
matrix := make(promql.Matrix, 0, len(v))
for _, s := range v {
matrix = append(matrix, promql.Series{Metric: s.Metric, Floats: []promql.FPoint{{T: s.T, F: s.F}}})
}
return matrix, nil
case promql.Scalar:
return promql.Matrix{{Metric: labels.EmptyLabels(), Floats: []promql.FPoint{{T: v.T, F: v.V}}}}, nil
default:
return nil, errors.NewInternalf(errors.CodeInternal, "unexpected hybrid result type %T", res.Value)
}
}
// hybridQueryable serves synthetic (compiled) selectors from memory and
// everything else from the live storage.
type hybridQueryable struct {
client *client
synthetic map[string][]*series
}
func (h *hybridQueryable) Querier(mint, maxt int64) (storage.Querier, error) {
return &hybridQuerier{
querier: querier{mint: mint, maxt: maxt, client: h.client},
synthetic: h.synthetic,
}, nil
}
type hybridQuerier struct {
querier
synthetic map[string][]*series
}
func (h *hybridQuerier) Select(ctx context.Context, sortSeries bool, hints *storage.SelectHints, matchers ...*labels.Matcher) storage.SeriesSet {
if name, ok := isSyntheticSelector(matchers); ok {
list := h.synthetic[name]
if sortSeries {
sorted := make([]*series, len(list))
copy(sorted, list)
sort.Slice(sorted, func(i, j int) bool { return labels.Compare(sorted[i].lset, sorted[j].lset) < 0 })
list = sorted
}
return newSeriesSet(list)
}
return h.querier.Select(ctx, sortSeries, hints, matchers...)
}

View File

@@ -0,0 +1,415 @@
package clickhouseprometheusv2
import (
"fmt"
"strings"
"github.com/SigNoz/signoz/pkg/telemetryschema/metricstelemetryschema"
"github.com/huandu/go-sqlbuilder"
)
// experimental gate for the timeSeries*ToGrid aggregate functions; attached
// as a SETTINGS clause so telemetrystore hooks cannot clobber it.
const gridFunctionsSetting = "SETTINGS allow_experimental_ts_to_grid_aggregate_function = 1"
var aggForEach = map[string]string{
"sum": "sumForEach",
"min": "minForEach",
"max": "maxForEach",
"avg": "avgForEach",
"count": "countForEach",
}
// buildUnitSQL renders the single ClickHouse statement that evaluates one
// core unit over the [startMs, endMs] / stepMs grid. The inner level
// computes per-series grids with a timeSeries*ToGrid aggregate, or with a
// windowed aggregation for *_over_time. The outer level is the spatial
// aggregation: -ForEach combinators grouped by the projected group key.
//
// The heavy level runs on the shards. The top-level FROM is the distributed
// samples table. The group-key join partner is a subquery on the
// shard-local time series table. So the shard rewrite executes the join and
// the per-series aggregation next to the data. Fingerprint co-locality
// makes this complete: samples and series shard on the same key. The
// initiator only merges the per-series states and applies the spatial
// -ForEach step. This is the same layout as the telemetrymetrics statement
// builder. The windowed *_over_time form shares the frame but holds
// per-bucket partials inside each series group (see windowedInner).
//
// The offset shifts the selector's data window. The grid indices map 1:1
// onto the query grid: output ts = startMs + i*stepMs. Grid parameters
// render as literals. They are aggregate-function parameters, not bindable
// values.
//
// Statements nest builder-rendered SQL as text. So the returned args must
// follow the position of each fragment in the final statement: ClickHouse
// binds ? placeholders by position. A JOIN renders before WHERE, so a
// joined subquery's args come before the outer query's condition args.
//
// Row shape: the group-key columns (see groupKeyColumns), then grid
// Array(Nullable(Float64)). A NULL grid point is an absent point, the
// engine's "no value here". The -ForEach combinators preserve it: an index
// where every series is NULL aggregates to NULL, and countForEach's 0 maps
// back to NULL.
func buildUnitSQL(unit *coreUnit, metricNames []string, dataStart, dataEnd int64, startMs, endMs, stepMs, lookbackMs int64) (string, []any, error) {
selStart := startMs - unit.offsetMs
selEnd := endMs - unit.offsetMs
stepSec := stepMs / 1000
if stepSec == 0 {
// Instant query: start == end, so the grid has one point for any
// positive step.
stepSec = 1
}
windowMs := unit.rangeMs
if unit.kind == unitInstant {
windowMs = lookbackMs
}
windowSec := windowMs / 1000
adjustedTsStartU, _, _, localTsTable := metricstelemetryschema.WhichTSTableToUse(uint64(dataStart), uint64(dataEnd), false, nil)
adjustedTsStart := int64(adjustedTsStartU)
keyNames := groupKeyColumns(unit)
// seriesSub computes fingerprint -> group key columns. It reads the
// local series table when it rides inside the shard-rewritten samples
// query, and the distributed one when it joins at the initiator
// (windowed form).
seriesSub := func(table string) (string, []any, error) {
sub := sqlbuilder.NewSelectBuilder()
selects := []string{"fingerprint"}
if keyNames == nil {
selects = append(selects, groupKeyExpr(unit)+" AS gkey")
} else {
// by (...) grouping extracts exactly the listed labels as plain
// columns: no reason to build, sort and stringify every label
// pair per row when the projection is a known short list and
// the label names live in Go anyway.
for i, name := range keyNames {
selects = append(selects, fmt.Sprintf("JSONExtractString(labels, %s) AS g%d", sub.Var(name), i))
}
}
sub.Select(selects...)
sub.From(fmt.Sprintf("%s.%s", metricstelemetryschema.DBName, table))
if err := applySeriesConditions(sub, adjustedTsStart, dataEnd, unit.matchers); err != nil {
return "", nil, err
}
sub.GroupBy(append([]string{"fingerprint"}, keyColumnAliases(keyNames)...)...)
q, args := sub.BuildWithFlavor(sqlbuilder.ClickHouse)
return q, args, nil
}
// samplesConditions adds the samples-side WHERE. The group-key join
// restricts to the matched series; no fingerprint condition is added
// here.
samplesConditions := func(sb *sqlbuilder.SelectBuilder, excludeStale bool) {
switch len(metricNames) {
case 0:
// No name constraint derivable; correct but unable to use the
// metric_name primary-key prefix.
case 1:
sb.Where(sb.EQ("metric_name", metricNames[0]))
default:
sb.Where(sb.In("metric_name", sqlbuilder.List(metricNames)))
}
// temporality precedes metric_name in the samples primary key; the
// fingerprints already come from these temporalities, so this only
// helps granule pruning.
sb.Where("temporality IN ['Cumulative', 'Unspecified']")
// When the window is narrower than the step, the grid windows
// (t_k window, t_k] cover only window/step of the timeline. A
// sample in a gap belongs to no window. It cannot move any grid
// point, but the grid aggregate buffers every row it is fed. This
// predicate keeps only the in-window rows. It cut a 36k-series
// one-week rate from 74s/28GiB to 16s/4.3GiB on fleet data: the
// read stays the same, and the aggregate input shrinks by the
// coverage ratio. The lattice anchors at selStart, because the end
// can sit off-lattice on unaligned grids. positiveModulo is
// necessary because samples above selStart make the dividend
// negative. The upper bound tightens to the last grid point: rows
// past it are equally windowless. When window >= step, the windows
// tile the timeline, and the plain bounds stay.
sliver := stepMs > 0 && windowMs > 0 && windowMs < stepMs
upper := selEnd
if sliver {
upper = selStart + (selEnd-selStart)/stepMs*stepMs
}
// Left-open window: a sample exactly at the window's lower boundary
// is never used (range selectors and lookback are both left-open).
sb.Where(sb.GT("unix_milli", selStart-windowMs), sb.LTE("unix_milli", upper))
if sliver {
sb.Where(fmt.Sprintf("positiveModulo(%s - unix_milli, %s) < %s",
sb.Var(selStart), sb.Var(stepMs), sb.Var(windowMs)))
}
if excludeStale {
// PromQL excludes stale markers from range vectors. Instant
// selectors need the stale rows for shadowing instead.
sb.Where("bitAnd(flags, 1) = 0")
}
}
keyCols := keyColumnAliases(keyNames)
// joinedInner builds the shard-side SELECT for the single-pass kinds:
// grid expression per (fingerprint, group key), group-key join against
// the local series table.
joinedInner := func(gridExpr string, excludeStale bool) (string, []any, error) {
seriesSQL, seriesArgs, err := seriesSub(localTsTable)
if err != nil {
return "", nil, err
}
sb := sqlbuilder.NewSelectBuilder()
selects := make([]string, 0, len(keyCols)+1)
// A fingerprint is the hash of one labelset, so every group-key
// column is functionally dependent on it: any() is exact, and
// grouping by the fingerprint alone spares hashing the joined
// string per sample row — measured -10-13% on a 1.9B-row rate.
for _, col := range keyCols {
selects = append(selects, fmt.Sprintf("any(series.%s) AS %s", col, col))
}
sb.Select(append(selects, gridExpr+" AS grid")...)
sb.From(fmt.Sprintf("%s.%s AS points", metricstelemetryschema.DBName, metricstelemetryschema.SamplesV4TableName))
sb.JoinWithOption(sqlbuilder.InnerJoin, fmt.Sprintf("(%s) AS series", seriesSQL), "points.fingerprint = series.fingerprint")
samplesConditions(sb, excludeStale)
sb.GroupBy("points.fingerprint")
q, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
// The join text renders before WHERE: its args come first.
return q, append(seriesArgs, args...), nil
}
var inner string
var innerArgs []any
var err error
switch unit.kind {
case unitInstant:
// Instant selection with stale shadowing: the grid value is the last
// non-stale sample in (t-lookback, t], absent when the overall last
// sample in that window is a stale marker (verified semantics: the
// -If combinator applies to the grid aggregates, and NULL comparisons
// make a stale-latest point absent).
gridParams := fmt.Sprintf("(fromUnixTimestamp64Milli(%d), fromUnixTimestamp64Milli(%d), %d, %d)", selStart, selEnd, stepSec, windowSec)
gridExpr := fmt.Sprintf(
"arrayMap((tall, tok, vok) -> if(tall IS NULL OR tok IS NULL OR tall != tok, NULL, vok), timeSeriesLastToGrid%s(fromUnixTimestamp64Milli(unix_milli), toFloat64(unix_milli)), timeSeriesLastToGridIf%s(fromUnixTimestamp64Milli(unix_milli), toFloat64(unix_milli), bitAnd(flags, 1) = 0), timeSeriesLastToGridIf%s(fromUnixTimestamp64Milli(unix_milli), value, bitAnd(flags, 1) = 0))",
gridParams, gridParams, gridParams,
)
inner, innerArgs, err = joinedInner(gridExpr, false)
case unitOverTime:
if unit.overFn == "last" {
// last_over_time == last non-stale sample in the window: the
// stale rows are already excluded in WHERE.
gridExpr := fmt.Sprintf(
"timeSeriesLastToGrid(fromUnixTimestamp64Milli(%d), fromUnixTimestamp64Milli(%d), %d, %d)(fromUnixTimestamp64Milli(unix_milli), value)",
selStart, selEnd, stepSec, windowSec,
)
inner, innerArgs, err = joinedInner(gridExpr, true)
break
}
inner, innerArgs, err = windowedInner(unit, samplesConditions, seriesSub, keyCols, localTsTable, selStart, selEnd, stepMs, windowMs)
default: // unitRange
gridExpr := fmt.Sprintf(
"%s(fromUnixTimestamp64Milli(%d), fromUnixTimestamp64Milli(%d), %d, %d)(fromUnixTimestamp64Milli(unix_milli), value)",
gridFunction[unit.fn], selStart, selEnd, stepSec, windowSec,
)
if unit.fn == fnIncrease {
// increase == rate * range-seconds, exactly: extrapolatedRate
// divides by the range only when isRate.
gridExpr = fmt.Sprintf("arrayMap(x -> x * %d, %s)", windowSec, gridExpr)
}
inner, innerArgs, err = joinedInner(gridExpr, true)
}
if err != nil {
return "", nil, err
}
spatial := "maxForEach(grid)"
switch {
case !unit.hasAgg:
// Per-series output: one row per (labels-minus-__name__) group.
// Distinct fingerprints can collapse onto the same projected label
// set only via a regex __name__ selector over metrics with identical
// other labels; maxForEach is a deterministic NULL-skipping merge and
// the identity for the overwhelmingly common one-fingerprint group.
case unit.aggOp.String() == "count":
// count over an all-absent index is an absent point, not 0.
spatial = "arrayMap(c -> if(c = 0, NULL, toFloat64(c)), countForEach(grid))"
default:
spatial = fmt.Sprintf("%s(grid)", aggForEach[unit.aggOp.String()])
}
keyList := strings.Join(keyCols, ", ")
query := fmt.Sprintf("SELECT %s, %s AS grid FROM (%s) GROUP BY %s %s", keyList, spatial, inner, keyList, gridFunctionsSetting)
return query, innerArgs, nil
}
// groupKeyColumns returns the label names to extract as plain group-key
// columns, or nil when the unit needs the canonical JSON key instead. Only
// by (...) grouping qualifies: its projection is a known short list, so
// extracting each label directly beats building, sorting and stringifying
// every label pair per row. without and no-aggregation project a label SET
// that varies per series — there the sorted-JSON key is load-bearing: the
// sort is what makes two fingerprints with different stored JSON key order
// land in one group, and the string carries the labels back out.
func groupKeyColumns(unit *coreUnit) []string {
if unit.hasAgg && unit.by && len(unit.grouping) > 0 {
return unit.grouping
}
return nil
}
// keyColumnAliases names the group-key columns in every SELECT level: g0..gN
// for direct extraction, the single canonical gkey otherwise.
func keyColumnAliases(keyNames []string) []string {
if keyNames == nil {
return []string{"gkey"}
}
cols := make([]string, len(keyNames))
for i := range keyNames {
cols[i] = fmt.Sprintf("g%d", i)
}
return cols
}
// windowedInner builds the avg/min/max/sum/count _over_time form without
// fanning samples out. It runs only when the range is a whole multiple of
// the step (see the transpile gate), because then the window
// (t_k - range, t_k] is exactly the union of W = range/step step buckets —
// both are left-open on the same boundaries — so bucket membership fully
// determines window membership. Fanning each sample into all W windows it
// covers (ARRAY JOIN) multiplies rows by W, which at long ranges over short
// steps is a row explosion measured in billions.
//
// The bucketing itself is the -Resample combinator: one group per (series,
// group key) whose state is a fixed array of per-bucket aggregates, updated
// in place per sample. Grouping by (series, bucket) instead — measured on a
// 100k-series x 371-bucket workload — creates a 37M-entry hash aggregation
// whose per-thread partial tables scale memory WITH max_threads (12 -> 48
// GiB from 2 to 8 threads, dead at 16) and ships one row per group to the
// initiator; the Resample form carries the same numbers in 100k compact
// array states, like every other unit kind.
//
// The wrapper level slides the window: slot k combines buckets k..k+W-1 by
// direct aggregation over at most W partials — no prefix-sum tricks, so no
// large-minus-large cancellation against the engine's directly-summed
// windows. A slot with zero window count is absent, which also keeps
// min/max honest: their slices filter on the bucket counts, so an empty
// bucket's zero-fill can never be mistaken for a value (a real sample can
// legitimately be 0 or +Inf).
func windowedInner(unit *coreUnit, samplesConditions func(*sqlbuilder.SelectBuilder, bool), seriesSub func(string) (string, []any, error), keyCols []string, localSeriesTable string, selStart, selEnd, stepMs, windowMs int64) (string, []any, error) {
effStepMs := stepMs
if effStepMs == 0 {
effStepMs = 1000
}
lastIdx := (selEnd - selStart) / effStepMs
gridLen := lastIdx + 1
w := windowMs / effStepMs
bucketLen := gridLen + w
// A window narrower than the step makes the windows (t_k - range, t_k]
// pairwise disjoint. There is nothing to slide. Each slot reads exactly
// its own window's aggregate. This is exact ONLY over sliver-filtered
// rows (samplesConditions adds the window<step predicate): the index
// below assigns every gap sample to the window above it, and the
// filter removes those samples. This needs a real step. Instant
// queries carry no sliver filter, so they keep the tiled form and its
// gates.
disjoint := stepMs > 0 && windowMs < stepMs
if disjoint {
w = 1
bucketLen = gridLen
}
seriesSQL, seriesArgs, err := seriesSub(localSeriesTable)
if err != nil {
return "", nil, err
}
// Bucket index, shifted so the earliest in-window sample lands at 0:
// jj = ceil((ts - selStart)/step) + W - 1, folded into one intDiv.
// Slot k's window is then buckets jj in [k, k+W-1]. In the disjoint
// form, the same ceil lands each in-window sample directly on its slot
// (W = 1). The numerator stays positive: the fetch floor is
// selStart - range > selStart - step.
jjShift := windowMs
if disjoint {
jjShift = effStepMs
}
jj := fmt.Sprintf("intDiv(unix_milli - %d + %d - 1, %d)", selStart, jjShift, effStepMs)
buckets := sqlbuilder.NewSelectBuilder()
selects := make([]string, 0, len(keyCols)+2)
// any() over the group key: exact because the key is functionally
// dependent on the fingerprint (see joinedInner).
for _, col := range keyCols {
selects = append(selects, fmt.Sprintf("any(series.%s) AS %s", col, col))
}
selects = append(selects, fmt.Sprintf("countResample(0, %d, 1)(value, %s) AS cnts", bucketLen, jj))
if unit.overFn != "count" {
selects = append(selects, fmt.Sprintf("%sResample(0, %d, 1)(value, %s) AS vals", map[string]string{
"avg": "sum",
"sum": "sum",
"min": "min",
"max": "max",
}[unit.overFn], bucketLen, jj))
}
buckets.Select(selects...)
buckets.From(fmt.Sprintf("%s.%s AS points", metricstelemetryschema.DBName, metricstelemetryschema.SamplesV4TableName))
buckets.JoinWithOption(sqlbuilder.InnerJoin, fmt.Sprintf("(%s) AS series", seriesSQL), "points.fingerprint = series.fingerprint")
samplesConditions(buckets, true)
buckets.GroupBy("points.fingerprint")
bucketsSQL, bucketsArgs := buckets.BuildWithFlavor(sqlbuilder.ClickHouse)
windowCnt := fmt.Sprintf("arraySum(arraySlice(cnts, k + 1, %d))", w)
var slot string
switch unit.overFn {
case "count":
slot = fmt.Sprintf("if(%s = 0, NULL, toFloat64(%s))", windowCnt, windowCnt)
case "sum":
slot = fmt.Sprintf("if(%s = 0, NULL, arraySum(arraySlice(vals, k + 1, %d)))", windowCnt, w)
case "avg":
slot = fmt.Sprintf("if(%s = 0, NULL, arraySum(arraySlice(vals, k + 1, %d)) / %s)", windowCnt, w, windowCnt)
case "min":
slot = fmt.Sprintf("if(%s = 0, NULL, arrayMin(arrayFilter((v, c) -> c > 0, arraySlice(vals, k + 1, %d), arraySlice(cnts, k + 1, %d))))", windowCnt, w, w)
case "max":
slot = fmt.Sprintf("if(%s = 0, NULL, arrayMax(arrayFilter((v, c) -> c > 0, arraySlice(vals, k + 1, %d), arraySlice(cnts, k + 1, %d))))", windowCnt, w, w)
}
keyList := strings.Join(keyCols, ", ")
inner := fmt.Sprintf(
"SELECT %s, arrayMap(k -> %s, range(toUInt64(%d))) AS grid FROM (%s)",
keyList, slot, gridLen, bucketsSQL,
)
return inner, append(seriesArgs, bucketsArgs...), nil
}
// groupKeyExpr renders the canonical JSON group key for the units whose
// projected label SET varies per series (see groupKeyColumns): the sorted
// [key, value] pairs of the projected labels, JSON-encoded.
// - by () with no labels: one constant group;
// - without (a, b): keep everything except the listed labels and __name__;
// - no aggregation: keep everything including __name__ — even when the
// unit drops the name from its OUTPUT, the key must keep it so distinct
// metrics never merge in SQL; executeUnit strips the name afterwards and
// turns a post-strip collision into the engine's duplicate-labelset
// error instead of a silently invented merge.
func groupKeyExpr(unit *coreUnit) string {
// An empty label value means "label absent" in Prometheus; the stored
// labels JSON can carry empty attribute values, which must not become
// output labels or group keys.
pairs := "arraySort(JSONExtractKeysAndValues(labels, 'String'))"
if !unit.hasAgg {
return fmt.Sprintf("toJSONString(arrayFilter(p -> p.2 != '', %s))", pairs)
}
if unit.by {
// Non-empty by (...) never reaches here; groupKeyColumns extracts
// those labels as plain columns instead.
return "'[]'"
}
excluded := append([]string{metricNameLabel}, unit.grouping...)
return fmt.Sprintf("toJSONString(arrayFilter(p -> p.2 != '' AND p.1 NOT IN (%s), %s))", quotedList(excluded), pairs)
}
func quotedList(items []string) string {
quoted := make([]string, len(items))
for i, s := range items {
quoted[i] = "'" + strings.ReplaceAll(s, "'", "\\'") + "'"
}
return strings.Join(quoted, ", ")
}

View File

@@ -0,0 +1,698 @@
package clickhouseprometheusv2
import (
"context"
"testing"
"time"
"github.com/DATA-DOG/go-sqlmock"
cmock "github.com/SigNoz/clickhouse-go-mock"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
"github.com/SigNoz/signoz/pkg/prometheus"
"github.com/SigNoz/signoz/pkg/telemetrystore"
"github.com/SigNoz/signoz/pkg/telemetrystore/telemetrystoretest"
"github.com/prometheus/prometheus/model/labels"
"github.com/prometheus/prometheus/promql"
"github.com/prometheus/prometheus/promql/parser"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func newTestClient(t *testing.T) (*client, *telemetrystoretest.Provider) {
t.Helper()
store := telemetrystoretest.New(telemetrystore.Config{Provider: "clickhouse"}, sqlmock.QueryMatcherRegexp)
settings := factory.NewScopedProviderSettings(instrumentationtest.New().ToProviderSettings(), "clickhouseprometheusv2_test")
return newClient(settings, store, prometheus.Config{}), store
}
var seriesCols = []cmock.ColumnType{
{Name: "fingerprint", Type: "UInt64"},
{Name: "labels", Type: "String"},
}
func parse(t *testing.T, q string) parser.Expr {
t.Helper()
expr, err := parser.NewParser(parser.Options{}).ParseExpr(q)
require.NoError(t, err)
return expr
}
func TestClassifyFullShapes(t *testing.T) {
tests := []struct {
name string
query string
check func(t *testing.T, u *coreUnit)
}{
{
name: "sum by rate",
query: `sum by (pod) (rate(http_requests_total{job="api"}[5m]))`,
check: func(t *testing.T, u *coreUnit) {
assert.Equal(t, fnRate, u.fn)
assert.Equal(t, int64(300_000), u.rangeMs)
assert.True(t, u.hasAgg)
assert.True(t, u.by)
assert.Equal(t, []string{"pod"}, u.grouping)
},
},
{
name: "bare increase with offset",
query: `increase(errors_total[10m] offset 30m)`,
check: func(t *testing.T, u *coreUnit) {
assert.Equal(t, fnIncrease, u.fn)
assert.Equal(t, int64(1_800_000), u.offsetMs)
assert.False(t, u.hasAgg)
},
},
{
name: "avg without over delta",
query: `avg without (instance) (delta(gauge_metric[15m]))`,
check: func(t *testing.T, u *coreUnit) {
assert.Equal(t, fnDelta, u.fn)
assert.True(t, u.hasAgg)
assert.False(t, u.by)
},
},
{
name: "scalar pipeline with comparison",
query: `sum(rate(x[5m])) * 100 > 5`,
check: func(t *testing.T, u *coreUnit) {
require.Len(t, u.ops, 2)
assert.Equal(t, parser.ItemType(parser.MUL), u.ops[0].op)
assert.Equal(t, 100.0, u.ops[0].scalar)
assert.Equal(t, parser.ItemType(parser.GTR), u.ops[1].op)
},
},
{
name: "scalar on left with unary minus",
query: `-1 * sum(rate(x[5m]))`,
check: func(t *testing.T, u *coreUnit) {
require.Len(t, u.ops, 1)
assert.True(t, u.ops[0].scalarOnLeft)
assert.Equal(t, -1.0, u.ops[0].scalar)
},
},
{
name: "bool comparison",
query: `sum(rate(x[5m])) >= bool 0.5`,
check: func(t *testing.T, u *coreUnit) {
require.Len(t, u.ops, 1)
assert.True(t, u.ops[0].returnBool)
},
},
{
name: "irate utf8 name",
query: `sum by ("k8s.pod.name") (irate({"k8s.container.cpu.time"}[2m]))`,
check: func(t *testing.T, u *coreUnit) {
assert.Equal(t, fnIRate, u.fn)
assert.Equal(t, []string{"k8s.pod.name"}, u.grouping)
},
},
{
name: "bare instant selector keeps name",
query: `up{job="api"}`,
check: func(t *testing.T, u *coreUnit) {
assert.Equal(t, unitInstant, u.kind)
assert.True(t, u.keepsName())
},
},
{
name: "gauge aggregation",
query: `sum by (pod) (container_memory offset 5m)`,
check: func(t *testing.T, u *coreUnit) {
assert.Equal(t, unitInstant, u.kind)
assert.Equal(t, int64(300_000), u.offsetMs)
assert.True(t, u.hasAgg)
assert.False(t, u.keepsName())
},
},
{
name: "gauge comparison keeps name",
query: `container_memory > 100`,
check: func(t *testing.T, u *coreUnit) {
assert.Equal(t, unitInstant, u.kind)
assert.True(t, u.keepsName())
},
},
{
name: "gauge arithmetic drops name",
query: `container_memory / 1024`,
check: func(t *testing.T, u *coreUnit) {
assert.Equal(t, unitInstant, u.kind)
assert.False(t, u.keepsName())
},
},
{
name: "avg_over_time",
query: `max by (node) (avg_over_time(load1[10m]))`,
check: func(t *testing.T, u *coreUnit) {
assert.Equal(t, unitOverTime, u.kind)
assert.Equal(t, "avg", u.overFn)
assert.Equal(t, int64(600_000), u.rangeMs)
},
},
{
name: "last_over_time keeps name",
query: `last_over_time(load1[10m])`,
check: func(t *testing.T, u *coreUnit) {
assert.Equal(t, unitOverTime, u.kind)
assert.Equal(t, "last", u.overFn)
assert.True(t, u.keepsName())
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
plan, ok := classify(parse(t, tt.query), testGrid(60_000))
require.True(t, ok, "expected transpilable")
require.True(t, plan.full, "expected full compilation")
require.Len(t, plan.units, 1)
tt.check(t, &plan.units[0].core)
})
}
}
func TestClassifyFallbackShapes(t *testing.T) {
queries := []struct {
name string
query string
step int64
}{
{"default-resolution subquery", `max_over_time(rate(x[5m])[30m:])`, 60_000},
{"at modifier", `sum(rate(x[5m] @ 1609746000))`, 60_000},
{"at modifier on gauge", `sum(container_memory @ 1609746000)`, 60_000},
{"sub-second step", `sum(rate(x[5m]))`, 500},
{"sub-second range", `sum(rate(x[1500ms]))`, 60_000},
{"by __name__ full", `sum by (__name__) (rate({__name__=~"a|b"}[5m]))`, 60_000},
{"quantile_over_time unsupported", `quantile_over_time(0.9, load1[10m])`, 60_000},
// Duration expressions resolve into the selectors' static fields only
// at evaluation time; classification reads those fields as zero, so
// transpiling would silently use the wrong offset (caught by the
// conformance corpus' duration_expression.test cases). Offset
// expressions parse without the experimental-parser flag, so they do
// reach the transpiler; range-position expressions are rejected at
// parse (the RangeExpr/StepExpr guards are defense-in-depth).
{"duration expression offset on instant", `x offset step()`, 60_000},
{"duration expression offset arithmetic", `x offset -step()*2`, 60_000},
{"duration expression offset on range", `sum(rate(x[5m] offset max(3s, step())))`, 60_000},
{"duration expression subquery step", `max_over_time(rate(x[5m])[30m:step()])`, 60_000},
}
for _, tt := range queries {
t.Run(tt.name, func(t *testing.T) {
_, ok := classify(parse(t, tt.query), testGrid(tt.step))
assert.False(t, ok, "expected fallback for %s", tt.query)
})
}
}
func TestClassifyHybridShapes(t *testing.T) {
tests := []struct {
name string
query string
wantUnits int
wantRewritten string
}{
{
name: "histogram quantile",
query: `histogram_quantile(0.95, sum by (le) (rate(http_bucket[5m])))`,
wantUnits: 1,
wantRewritten: `histogram_quantile(0.95, __signoz_transpiled_0__)`,
},
{
name: "topk over compiled",
query: `topk(5, sum by (pod) (rate(x[5m])))`,
wantUnits: 1,
wantRewritten: `topk(5, __signoz_transpiled_0__)`,
},
{
name: "ratio of compiled units",
query: `sum(rate(a[5m])) / sum(rate(b[5m]))`,
wantUnits: 2,
wantRewritten: `__signoz_transpiled_0__ / __signoz_transpiled_1__`,
},
{
name: "or vector zero",
query: `sum(rate(a[5m])) or vector(0)`,
wantUnits: 1,
wantRewritten: `__signoz_transpiled_0__ or vector(0)`,
},
{
name: "quantile agg over compiled rate",
query: `quantile(0.9, rate(x[5m]))`,
wantUnits: 1,
wantRewritten: `quantile(0.9, __signoz_transpiled_0__)`,
},
{
name: "non-literal scalar side stays engine-side",
query: `sum(rate(x[5m])) * scalar(y)`,
wantUnits: 1,
wantRewritten: `__signoz_transpiled_0__ * scalar(y)`,
},
{
name: "compiled mixed with raw selector",
query: `sum by (pod) (rate(a[5m])) / on (pod) group_left () b`,
wantUnits: 1,
wantRewritten: `__signoz_transpiled_0__ / on (pod) group_left () b`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
plan, ok := classify(parse(t, tt.query), testGrid(60_000))
require.True(t, ok)
assert.False(t, plan.full)
assert.Len(t, plan.units, tt.wantUnits)
assert.Equal(t, tt.wantRewritten, plan.rewritten)
})
}
}
func TestClassifyHybridGuards(t *testing.T) {
t.Run("no substitution under on(__name__)", func(t *testing.T) {
plan, ok := classify(parse(t, `sum(rate(a[5m])) * on (__name__) b`), testGrid(60_000))
_ = plan
assert.False(t, ok, "matching on __name__ must not see synthetic names")
})
t.Run("no substitution inside @-pinned subquery", func(t *testing.T) {
_, ok := classify(parse(t, `max_over_time(rate(x[5m])[30m:1m] @ 1609746000)`), testGrid(60_000))
assert.False(t, ok)
})
}
// The alert-smoothing idiom: units inside a fixed-resolution subquery
// evaluate on the subquery grid — epoch-aligned multiples of the resolution,
// starting strictly after (outer start - range), exactly as the engine
// derives it.
func TestClassifySubqueryUnits(t *testing.T) {
grid := gridContext{startMs: 1_700_000_030_000, endMs: 1_700_007_200_000, stepMs: 60_000}
plan, ok := classify(parse(t, `min_over_time((sum by (ns) (increase(x[5m])))[10m:5m]) > 0`), grid)
require.True(t, ok)
require.False(t, plan.full)
require.Len(t, plan.units, 1)
assert.Equal(t, `min_over_time(__signoz_transpiled_0__[10m:5m]) > 0`, plan.rewritten)
unit := plan.units[0]
// lower bound = outer start - range = 1_699_999_430_000; first multiple
// of 300_000 strictly greater is 1_699_999_500_000.
assert.Equal(t, int64(1_699_999_500_000), unit.grid.startMs)
assert.Equal(t, grid.endMs, unit.grid.endMs)
assert.Equal(t, int64(300_000), unit.grid.stepMs)
assert.Equal(t, fnIncrease, unit.core.fn)
t.Run("subquery offset shifts the grid", func(t *testing.T) {
plan, ok := classify(parse(t, `max_over_time((sum(rate(x[5m])))[10m:5m] offset 30m)`), grid)
require.True(t, ok)
require.Len(t, plan.units, 1)
// lower = start - offset - range = 1_699_997_630_000 -> first
// multiple of 300_000 above = 1_699_997_700_000; end shifts too.
assert.Equal(t, int64(1_699_997_700_000), plan.units[0].grid.startMs)
assert.Equal(t, grid.endMs-1_800_000, plan.units[0].grid.endMs)
})
t.Run("mollusk ratio-inside-subquery idiom", func(t *testing.T) {
q := `min_over_time(((sum by (a) (rate(m1[5m]))) / (avg by (a) (m2)))[5m:1m])`
plan, ok := classify(parse(t, q), grid)
require.True(t, ok)
// Both sides compile on the subquery grid: the rate side and the
// gauge aggregation side; the engine joins them and smooths.
require.Len(t, plan.units, 2)
assert.Equal(t, int64(60_000), plan.units[0].grid.stepMs)
assert.Equal(t, unitInstant, plan.units[1].core.kind)
assert.Contains(t, plan.rewritten, `__signoz_transpiled_0__ / __signoz_transpiled_1__`)
})
}
func TestBuildUnitSQL(t *testing.T) {
unit := &coreUnit{
fn: fnRate,
rangeMs: 300_000,
hasAgg: true,
aggOp: parser.SUM,
by: true,
grouping: []string{"pod"},
matchers: []*labels.Matcher{mustMatcher(t, labels.MatchEqual, "__name__", "http_requests_total")},
}
sql, args, err := buildUnitSQL(unit, []string{"http_requests_total"}, 1_699_999_700_000, 1_700_003_600_000, 1_700_000_000_000, 1_700_003_600_000, 60_000, 300_000)
require.NoError(t, err)
assert.Contains(t, sql, "timeSeriesRateToGrid(fromUnixTimestamp64Milli(1700000000000), fromUnixTimestamp64Milli(1700003600000), 60, 300)(fromUnixTimestamp64Milli(unix_milli), value)")
assert.Contains(t, sql, "unix_milli > ? AND unix_milli <= ?")
assert.Contains(t, sql, "bitAnd(flags, 1) = 0")
assert.Contains(t, sql, "sumForEach(grid)")
// The group-key join rides inside the shard query: distributed samples
// at the top level, the local series table in the join subquery, the
// grid aggregation grouped per (fingerprint, group key) shard-side.
assert.Contains(t, sql, "FROM signoz_metrics.distributed_samples_v4 AS points INNER JOIN (SELECT fingerprint,")
assert.Contains(t, sql, "FROM signoz_metrics.time_series_v4 WHERE")
// The group key is functionally dependent on the fingerprint (one
// labelset per fingerprint): any() is exact and the per-row hash key
// shrinks to the fingerprint alone.
assert.Contains(t, sql, "any(series.g0) AS g0")
assert.Contains(t, sql, "GROUP BY points.fingerprint)")
// No samples-side fingerprint condition: the group-key join restricts.
assert.NotContains(t, sql, "points.fingerprint IN (")
// by (pod) extracts the grouped label directly — no per-row JSON
// build/sort/stringify for a known projection.
assert.Contains(t, sql, "JSONExtractString(labels, ?) AS g0")
assert.NotContains(t, sql, "toJSONString")
assert.Contains(t, sql, "SETTINGS allow_experimental_ts_to_grid_aggregate_function = 1")
// Args follow placeholder order: the joined series subquery renders
// before the samples WHERE, and its select list ('pod') renders before
// its own conditions.
assert.Equal(t, []any{"pod", "http_requests_total", int64(1_699_999_200_000), int64(1_700_003_600_000), "http_requests_total", int64(1_699_999_700_000), int64(1_700_003_600_000)}, args)
}
func TestBuildUnitSQLIncreaseAndOffset(t *testing.T) {
unit := &coreUnit{
fn: fnIncrease,
rangeMs: 600_000,
offsetMs: 1_800_000,
matchers: []*labels.Matcher{mustMatcher(t, labels.MatchEqual, "__name__", "errors_total")},
}
sql, _, err := buildUnitSQL(unit, nil, 1_699_997_600_000, 1_700_001_800_000, 1_700_000_000_000, 1_700_003_600_000, 60_000, 300_000)
require.NoError(t, err)
// Grid and window shift by the offset; increase multiplies rate by the
// range in seconds.
assert.Contains(t, sql, "fromUnixTimestamp64Milli(1699998200000), fromUnixTimestamp64Milli(1700001800000)")
assert.Contains(t, sql, "arrayMap(x -> x * 600, timeSeriesRateToGrid")
assert.Contains(t, sql, "maxForEach(grid)")
}
func TestBuildUnitSQLWindowSliver(t *testing.T) {
// rate[5m] on a 30m grid evaluates only a 5m sliver before each grid
// point — samples in the gaps belong to no window and would only be
// buffered by the grid aggregate. The WHERE must keep exactly the
// in-window rows: positiveModulo anchored at the selector start (end
// can sit off-lattice on unaligned grids, and samples above the start
// make the plain modulo dividend negative), and the scan capped at the
// last grid point — rows past it are equally windowless.
unit := &coreUnit{
fn: fnRate,
rangeMs: 300_000,
hasAgg: true,
aggOp: parser.SUM,
by: true,
grouping: []string{"pod"},
matchers: []*labels.Matcher{mustMatcher(t, labels.MatchEqual, "__name__", "http_requests_total")},
}
sql, args, err := buildUnitSQL(unit, []string{"http_requests_total"}, 1_699_999_700_000, 1_700_003_600_000, 1_700_000_000_000, 1_700_003_600_000, 1_800_000, 300_000)
require.NoError(t, err)
assert.Contains(t, sql, "positiveModulo(? - unix_milli, ?) < ?")
assert.Equal(t, []any{"pod", "http_requests_total", int64(1_699_999_200_000), int64(1_700_003_600_000), "http_requests_total", int64(1_699_999_700_000), int64(1_700_003_600_000), int64(1_700_000_000_000), int64(1_800_000), int64(300_000)}, args)
t.Run("off-lattice end caps the scan at the last grid point", func(t *testing.T) {
// end - start = 50m at a 30m step: the only grid points are start
// and start+30m; samples in the trailing 20m serve no window.
_, args, err := buildUnitSQL(unit, []string{"http_requests_total"}, 1_699_999_700_000, 1_700_003_000_000, 1_700_000_000_000, 1_700_003_000_000, 1_800_000, 300_000)
require.NoError(t, err)
assert.Contains(t, args, int64(1_700_001_800_000))
})
t.Run("window covering the step keeps plain bounds", func(t *testing.T) {
sql, _, err := buildUnitSQL(unit, []string{"http_requests_total"}, 1_699_999_700_000, 1_700_003_600_000, 1_700_000_000_000, 1_700_003_600_000, 60_000, 300_000)
require.NoError(t, err)
assert.NotContains(t, sql, "positiveModulo")
})
}
func TestBuildUnitSQLWindowedBucketsWithoutFanOut(t *testing.T) {
// The window is W = range/step whole buckets, so each sample lands in
// exactly one bucket via GROUP BY and the window slides over bucket
// partials — fanning samples into every covered window (ARRAY JOIN)
// multiplies rows by W, a row explosion at long ranges.
unit := &coreUnit{
kind: unitOverTime,
overFn: "avg",
rangeMs: 600_000,
matchers: []*labels.Matcher{mustMatcher(t, labels.MatchEqual, "__name__", "node_load1")},
}
sql, _, err := buildUnitSQL(unit, []string{"node_load1"}, 1_699_999_400_000, 1_700_003_600_000, 1_700_000_000_000, 1_700_003_600_000, 60_000, 600_000)
require.NoError(t, err)
assert.NotContains(t, sql, "ARRAY JOIN")
// One group per series with fixed per-bucket arrays (-Resample); the
// bucket index jj = ceil((ts - start)/step) + W - 1 folded into a single
// intDiv. Grouping by (series, bucket) instead measured 37M hash groups
// whose per-thread partials scale memory with max_threads.
assert.Contains(t, sql, "countResample(0, 71, 1)(value, intDiv(unix_milli - 1700000000000 + 600000 - 1, 60000)) AS cnts")
assert.Contains(t, sql, "sumResample(0, 71, 1)(value, intDiv(unix_milli - 1700000000000 + 600000 - 1, 60000)) AS vals")
assert.Contains(t, sql, "any(series.gkey) AS gkey")
assert.Contains(t, sql, "GROUP BY points.fingerprint)")
assert.NotContains(t, sql, "jj) AS jj")
assert.Contains(t, sql, "INNER JOIN (SELECT fingerprint,")
assert.Contains(t, sql, "FROM signoz_metrics.time_series_v4 WHERE")
// Slide: W = 10 buckets per slot, absent when the window count is 0.
assert.Contains(t, sql, "arraySum(arraySlice(cnts, k + 1, 10))")
assert.Contains(t, sql, "arraySum(arraySlice(vals, k + 1, 10))")
}
func TestBuildUnitSQLDisjointOverTime(t *testing.T) {
// avg_over_time[5m] on a 30m grid: the windows are pairwise disjoint,
// so there is no slide — one Resample bucket per grid slot, read
// directly. Exact only together with the window-sliver predicate, which
// removes the gap samples the ceil index would otherwise assign to the
// window above them.
unit := &coreUnit{
kind: unitOverTime,
overFn: "avg",
rangeMs: 300_000,
matchers: []*labels.Matcher{mustMatcher(t, labels.MatchEqual, "__name__", "node_load1")},
}
sql, _, err := buildUnitSQL(unit, []string{"node_load1"}, 1_699_999_700_000, 1_700_003_600_000, 1_700_000_000_000, 1_700_003_600_000, 1_800_000, 300_000)
require.NoError(t, err)
assert.NotContains(t, sql, "ARRAY JOIN")
// gridLen = 3 slots, bucket array the same length — no W tail.
assert.Contains(t, sql, "countResample(0, 3, 1)(value, intDiv(unix_milli - 1700000000000 + 1800000 - 1, 1800000)) AS cnts")
assert.Contains(t, sql, "sumResample(0, 3, 1)(value, intDiv(unix_milli - 1700000000000 + 1800000 - 1, 1800000)) AS vals")
// Single-bucket window: the slide degenerates to reading one slot.
assert.Contains(t, sql, "arraySum(arraySlice(cnts, k + 1, 1))")
// The sliver predicate is the correctness precondition of this form.
assert.Contains(t, sql, "positiveModulo(? - unix_milli, ?) < ?")
}
// TestDisjointWindowLattice brute-forces the disjoint-form arithmetic: a
// sample survives the sliver predicate exactly when some grid window
// contains it, and the ceil bucket index then lands it on that window's
// slot. This is the pure-Go mirror of the SQL expressions — the predicate
// in samplesConditions and jj in windowedInner — over random lattices,
// including off-lattice ends and samples beyond the last grid point.
func TestDisjointWindowLattice(t *testing.T) {
rng := func(seed *uint64) int64 {
*seed = *seed*6364136223846793005 + 1442695040888963407
return int64(*seed >> 33)
}
seed := uint64(42)
for trial := 0; trial < 2000; trial++ {
stepMs := 1_000 * (1 + rng(&seed)%3600)
windowMs := 1 + rng(&seed)%(stepMs-1) // strictly below the step
selStart := 1_700_000_000_000 + rng(&seed)%1_000_000
selEnd := selStart + rng(&seed)%(50*stepMs) // end may sit off-lattice
lastIdx := (selEnd - selStart) / stepMs
upper := selStart + lastIdx*stepMs
for i := 0; i < 50; i++ {
u := selStart - windowMs - stepMs + rng(&seed)%(selEnd-selStart+3*stepMs)
// Oracle: is u inside any window (t_k - window, t_k]?
inWindow := false
var slot int64 = -1
for k := int64(0); k <= lastIdx; k++ {
tk := selStart + k*stepMs
if u > tk-windowMs && u <= tk {
inWindow = true
slot = k
break
}
}
// The SQL: fetch bounds, then the sliver predicate
// positiveModulo(selStart - u, step) < window.
kept := u > selStart-windowMs && u <= upper
if kept {
pmod := (selStart - u) % stepMs
if pmod < 0 {
pmod += stepMs
}
kept = pmod < windowMs
}
require.Equal(t, inWindow, kept,
"sliver keep mismatch: u=%d selStart=%d step=%d window=%d", u, selStart, stepMs, windowMs)
if !kept {
continue
}
// jj = ceil((u - selStart)/step) via one intDiv; numerator is
// positive because u > selStart - window > selStart - step.
jj := (u - selStart + stepMs - 1) / stepMs
require.Equal(t, slot, jj,
"slot mismatch: u=%d selStart=%d step=%d window=%d", u, selStart, stepMs, windowMs)
}
}
}
func TestTryExecuteRange_WindowedGateFallsBack(t *testing.T) {
c, store := newTestClient(t)
e := &executor{client: c, parser: prometheus.NewParser()}
start := time.UnixMilli(1_700_000_000_000)
end := time.UnixMilli(1_700_003_600_000)
// 10m range at 90s step: the window is not a whole number of buckets.
_, ok, err := e.TryExecuteRange(context.Background(), `avg_over_time(up[10m])`, start, end, 90*time.Second)
require.NoError(t, err)
assert.False(t, ok, "range not divisible by step must not transpile")
// 1d range at 60s step: 1440 bucket combines per slot, over the cap.
_, ok, err = e.TryExecuteRange(context.Background(), `avg_over_time(up[1d])`, start, end, time.Minute)
require.NoError(t, err)
assert.False(t, ok, "range/step above maxWindowBuckets must not transpile")
// 1m range at 5m step: the windows are disjoint slivers — no
// divisibility or width requirement, so this transpiles.
store.Mock().ExpectQuery("SELECT fingerprint, any\\(labels\\)").WithArgs("up", int64(1_699_999_200_000), int64(1_700_003_600_000)).WillReturnRows(cmock.NewRows(seriesCols, [][]any{}))
_, ok, err = e.TryExecuteRange(context.Background(), `avg_over_time(up[1m])`, start, end, 5*time.Minute)
require.NoError(t, err)
assert.True(t, ok, "range below step is the disjoint form and must transpile")
}
func TestApplyScalarOps(t *testing.T) {
f := func(v float64) *float64 { return &v }
t.Run("arithmetic chain", func(t *testing.T) {
values := []*float64{f(2), nil, f(4)}
applyScalarOps([]scalarOp{{op: parser.MUL, scalar: 100}, {op: parser.ADD, scalar: 1}}, values)
require.NotNil(t, values[0])
assert.Equal(t, 201.0, *values[0])
assert.Nil(t, values[1])
assert.Equal(t, 401.0, *values[2])
})
t.Run("comparison filters points", func(t *testing.T) {
values := []*float64{f(1), f(10)}
applyScalarOps([]scalarOp{{op: parser.GTR, scalar: 5}}, values)
assert.Nil(t, values[0])
require.NotNil(t, values[1])
assert.Equal(t, 10.0, *values[1], "filter comparisons keep the original value")
})
t.Run("bool comparison emits 0/1", func(t *testing.T) {
values := []*float64{f(1), f(10)}
applyScalarOps([]scalarOp{{op: parser.GTR, scalar: 5, returnBool: true}}, values)
assert.Equal(t, 0.0, *values[0])
assert.Equal(t, 1.0, *values[1])
})
t.Run("scalar on left division", func(t *testing.T) {
values := []*float64{f(4)}
applyScalarOps([]scalarOp{{op: parser.DIV, scalar: 100, scalarOnLeft: true}}, values)
assert.Equal(t, 25.0, *values[0])
})
}
func TestLabelsFromGroupKey(t *testing.T) {
lset, err := labelsFromGroupKey(`[["pod","api-0"],["ns","prod"]]`)
require.NoError(t, err)
assert.Equal(t, "api-0", lset.Get("pod"))
assert.Equal(t, "prod", lset.Get("ns"))
empty, err := labelsFromGroupKey(`[]`)
require.NoError(t, err)
assert.True(t, empty.IsEmpty())
}
// testGrid is a 2h query grid ending on a round timestamp.
func testGrid(stepMs int64) gridContext {
return gridContext{startMs: 1_700_000_000_000, endMs: 1_700_007_200_000, stepMs: stepMs}
}
// A bool comparison returns 0/1, not the sample, so the engine drops
// __name__; keeping it would change downstream vector matching.
func TestKeepsName_BoolComparisonDropsName(t *testing.T) {
plan, ok := classify(parse(t, `up > bool 0`), testGrid(60_000))
require.True(t, ok)
assert.False(t, plan.units[0].core.keepsName())
plan, ok = classify(parse(t, `up > 0`), testGrid(60_000))
require.True(t, ok)
assert.True(t, plan.units[0].core.keepsName())
}
// timeSeriesLastToGrid widens its window to max(window, step) — probed on
// 25.12 — so Last-style units at window < step must fall back or they would
// resurrect samples the engine's lookback already dropped.
func TestTryExecuteRange_LastStyleWindowBelowStepTranspiles(t *testing.T) {
// These used to fall back because timeSeriesLastToGrid widens its window
// to max(window, step). Over sliver-filtered rows the widening is
// harmless — the widened window intersected with the data IS the
// lookback window — so the gate is gone and both shapes transpile. The
// mock returns no series: the point here is the routing, the value
// semantics are the parity suite's job.
c, store := newTestClient(t)
e := &executor{client: c, parser: prometheus.NewParser()}
start := time.UnixMilli(1_700_000_000_000)
end := time.UnixMilli(1_700_003_600_000)
store.Mock().ExpectQuery("SELECT fingerprint, any\\(labels\\)").WithArgs("up", int64(1_699_999_200_000), int64(1_700_003_600_000)).WillReturnRows(cmock.NewRows(seriesCols, [][]any{}))
_, ok, err := e.TryExecuteRange(context.Background(), `sum by (pod) (up)`, start, end, time.Hour)
require.NoError(t, err)
assert.True(t, ok, "instant selection at step > lookback must transpile")
store.Mock().ExpectQuery("SELECT fingerprint, any\\(labels\\)").WithArgs("up", int64(1_699_999_200_000), int64(1_700_003_600_000)).WillReturnRows(cmock.NewRows(seriesCols, [][]any{}))
_, ok, err = e.TryExecuteRange(context.Background(), `last_over_time(up[10m])`, start, end, time.Hour)
require.NoError(t, err)
assert.True(t, ok, "last_over_time at range < step must transpile")
}
// A nameless selector can span metrics whose series alternate in time (one
// dies inside the lookback before the other appears); after the name drop
// the engine merges them into ONE series and errors only when two samples
// share an evaluation timestamp. Pinned by conformance cases
// operators.test:994/997 (-{job="api"} over http_requests/http_errors).
func TestMergeSameLabelsetSeries(t *testing.T) {
f := func(v float64) *float64 { return &v }
api := labels.FromStrings("job", "api")
out, err := mergeSameLabelsetSeries([]transpiledSeries{
{lset: api, values: []*float64{f(-2), nil}},
{lset: api, values: []*float64{nil, f(-4)}},
{lset: labels.FromStrings("job", "web"), values: []*float64{f(7), nil}},
})
require.NoError(t, err)
require.Len(t, out, 2)
assert.Equal(t, []*float64{f(-2), f(-4)}, out[0].values, "temporally disjoint twins must merge into one series")
_, err = mergeSameLabelsetSeries([]transpiledSeries{
{lset: api, values: []*float64{f(1), nil}},
{lset: api, values: []*float64{f(2), nil}},
})
require.Error(t, err, "two values on one evaluation timestamp is the engine's duplicate error")
assert.True(t, errors.Ast(err, errors.TypeInvalidInput))
}
// Hybrid twin case: stripping the synthetic __name__ can leave two engine
// output series distinguishable only by those names (-metric_a or -metric_b:
// both {} once real names are dropped). Pinned by conformance cases
// name_label_dropping.test:137 and operators.test:1016.
func TestMergeMatrixByLabelset(t *testing.T) {
empty := labels.EmptyLabels()
out, err := mergeMatrixByLabelset(promql.Matrix{
{Metric: empty, Floats: []promql.FPoint{{T: 0, F: -1}}},
{Metric: empty, Floats: []promql.FPoint{{T: 600_000, F: -4}}},
})
require.NoError(t, err)
require.Len(t, out, 1)
assert.Equal(t, []promql.FPoint{{T: 0, F: -1}, {T: 600_000, F: -4}}, out[0].Floats)
_, err = mergeMatrixByLabelset(promql.Matrix{
{Metric: empty, Floats: []promql.FPoint{{T: 0, F: -1}}},
{Metric: empty, Floats: []promql.FPoint{{T: 0, F: -3}}},
})
require.Error(t, err)
assert.True(t, errors.Ast(err, errors.TypeInvalidInput))
}

Some files were not shown because too many files have changed in this diff Show More