FanQL
Preview: the syntax may change before GA
FanQL is the text form of a structured telemetry query. It produces the same query.Spec that dashboards store and the engine executes. Panels store the query; text is derived. Problems prevent execution; notices never block it. Each diagnostic says what happened and what to write instead, using a valid example from that field. Diagnostics use display names without internal visualization identifiers and report all independent problems in one pass, in source order. Only follow-on cascades are suppressed.
Shape and entry points
Section titled “Shape and entry points”source [where] filters | measures by fields every bucket sort key limit n
Explore accepts a whole query. Logs and Traces search bars accept filters only; the page supplies source, where and visualization. The visualization picker accepts the query as written. Adding | count() every auto to make a timeseries is a visible edit. One aggregation stage is allowed; another pipe may introduce only sort or limit, and canonical text drops that pipe.
Sources and words
Section titled “Sources and words”Sources: logs, metrics, spans, recognized only as the first token. A bare source selects all rows in the chosen time range. where is optional; canonical text supplies it when filters exist.
Clause words: by, every, limit, sort, where and |, recognized at depth zero, outside quotes and brackets, at a term boundary. After operators they are values (service:where, service in (sort, limit)). Quote a clause word to search for it.
Expression words: and, between, ilike, in, is, like, not, null, or, recognized only in their grammatical positions. Modifiers: as, asc, desc. Keywords, columns, functions, units and vocabulary aliases ignore case. Values and attribute/resource keys preserve case.
A word is a field only before a field operator; otherwise it searches log body or span operation. Metrics require an explicit field. Quotes use doubling as the only escape; backslashes are literal. Quoted api* and embedded $ stay quoted. Wildcards are unsupported except k:* and count(*). A leading - is negation except when a numeric value is expected. + is legal in a sort key or exponent.
Operators and missing values
Section titled “Operators and missing values”| Operator | Meaning |
|---|---|
: | Equality with vocabulary aliases; missing values are excluded. |
= | Equality, with the same typing and alias rules as :. |
!= | SQL spelling, SQL meaning: rows without k are excluded; -k:v includes them. |
<> | SQL inequality, equivalent to !=; missing values are excluded. |
> | Greater than; missing values are excluded. |
>= | Greater than or equal; missing values are excluded. |
< | Less than; missing values are excluded. |
<= | Less than or equal; missing values are excluded. |
~ | Case-insensitive substring; % and _ are ordinary characters. |
!~ | Null-inclusive substring exclusion, like -k ~ v. |
in | Membership in a nonempty list or multi-value variable; missing values are excluded. |
not in | SQL spelling, SQL meaning: missing values are excluded; prefix - includes them. |
between | Inclusive lower and upper bounds separated by and; missing values are excluded. |
not between | SQL spelling, SQL meaning: missing values are excluded; prefix - includes them. |
is null | Matches missing values, canonical -k:*. |
is not null | Matches present values, canonical k:*. |
like | Case-sensitive SQL pattern; requires quoted text; % and _ are wildcards. |
ilike | Case-insensitive SQL pattern; requires quoted text. |
not like | SQL spelling, SQL meaning: missing values are excluded; prefix - includes them. |
not ilike | SQL spelling, SQL meaning: missing values are excluded; prefix - includes them. |
Prefix - or not excludes a term and includes rows where its predicate is NULL. Repeated negation cancels in pairs. SQL operators retain SQL null behavior. Compound negation follows De Morgan’s law. TRY_CAST failures are NULL, so -http_status_code between 400 and 599 includes uncastable codes.
Whitespace and and join filters. An or chain must occupy its whole filter list or be parenthesized: a:1 (b:2 or c:3) and (a:1 b:2) or c:3 differ. This rule also applies inside groups and count_if. Mixing conjunction and or without parentheses is a problem with suggested repairs.
Fields
Section titled “Fields”A single identifier is a source column when one exists, otherwise an attribute key with a notice. Dotted http.target means attributes['http.target']; resource.k8s.pod.name means resource['k8s.pod.name']. Explicit attributes.<path> uses the attributes map. Bracket keys are literal nonempty strings, including spaces, Unicode, punctuation and doubled quotes. Keys beginning resource. or attributes. retain brackets where needed to preserve the map.
Canonical text brackets single-segment attribute keys, so new columns cannot reinterpret canonical or stored text. Map columns require a key. duration denotes duration_ms in filters, measure arguments and trace sorts. duration_ms>500 takes a number; duration>500ms requires a unit. A measure alias named duration or duration_ms stays that alias in sort. Time columns accept range comparisons against quoted timestamps, not equality shorthand.
| Name | Type | Unit | Low cardinality | Description |
|---|---|---|---|---|
namespace | string | none | OpenTelemetry service namespace | |
service | string | none | yes | Service name |
operation | string | none | Span name | |
kind | string | none | yes | SPAN_KIND_CLIENT, SPAN_KIND_CONSUMER, SPAN_KIND_INTERNAL, SPAN_KIND_PRODUCER, SPAN_KIND_SERVER, SPAN_KIND_UNSPECIFIED |
status | string | none | yes | STATUS_CODE_ERROR, STATUS_CODE_OK, STATUS_CODE_UNSET |
status_message | string | none | Status description | |
trace_id | string | none | Trace identifier | |
span_id | string | none | Span identifier | |
parent_span_id | string | none | Parent span identifier; empty for roots | |
start_time | time | none | Span start | |
end_time | time | none | Span end | |
duration_ms | number | ms | Span duration in milliseconds | |
http_method | string | none | yes | HTTP method |
http_status_code | string | none | HTTP response status code as text | |
http_route | string | none | HTTP route template | |
db_system | string | none | yes | Database system, e.g. postgresql, redis |
rpc_method | string | none | RPC method | |
rpc_service | string | none | RPC service | |
peer_service | string | none | Remote service named by the caller | |
service_version | string | none | yes | service.version resource attribute |
deployment_env | string | none | yes | deployment.environment resource attribute |
exception_type | string | none | Exception type from the span’s exception event | |
exception_message | string | none | Exception message | |
messaging_system | string | none | yes | messaging.system attribute |
messaging_destination | string | none | messaging.destination.name attribute | |
scope_name | string | none | Instrumentation scope | |
attributes | map | none | Span attributes; use attributes[‘key’] | |
resource | map | none | Resource attributes; use resource[‘key’] |
| Name | Type | Unit | Low cardinality | Description |
|---|---|---|---|---|
namespace | string | none | OpenTelemetry service namespace | |
service | string | none | yes | Service name |
time | time | none | Log time | |
severity | string | none | yes | Severity text, e.g. ERROR, WARN, INFO |
severity_number | number | none | OpenTelemetry severity number; 17 and above is an error | |
body | string | none | Redacted log body | |
body_template | string | none | Body with variable parts replaced by <*>; groups similar messages | |
trace_id | string | none | Trace identifier | |
span_id | string | none | Span identifier | |
scope_name | string | none | Instrumentation scope | |
attributes | map | none | Log attributes; use attributes[‘key’] | |
resource | map | none | Resource attributes; use resource[‘key’] |
metrics
Section titled “metrics”| Name | Type | Unit | Low cardinality | Description |
|---|---|---|---|---|
namespace | string | none | OpenTelemetry service namespace | |
service | string | none | yes | Service name |
time | time | none | Data point time | |
name | string | none | Metric name | |
unit | string | none | Metric unit as reported | |
type | string | none | yes | counter, gauge, histogram, summary, updown |
temporality | string | none | yes | cumulative or delta; empty for gauge and summary |
start_time | time | none | Data point start time; NULL when absent | |
value | number | none | Gauge or sum value | |
hist_count | number | count | Histogram point count | |
hist_sum | number | none | Histogram point sum | |
attributes | map | none | Data point attributes; use attributes[‘key’] | |
resource | map | none | Resource attributes; use resource[‘key’] |
http_status_code is text. <, <=, >, >= and between use TRY_CAST(http_status_code AS INTEGER) for literals and variables alike. Equality and membership stay text, so http_status_code:500 means '500'.
Vocabularies and severity
Section titled “Vocabularies and severity”Aliases apply under equality, inequality and membership, including prefix negation. Quoted values, contains, SQL patterns and function arguments are literal. Stored spellings are also accepted bare.
status (spans)
Section titled “status (spans)”| Input | Stored |
|---|---|
error | STATUS_CODE_ERROR |
ok | STATUS_CODE_OK |
unset | STATUS_CODE_UNSET |
kind (spans)
Section titled “kind (spans)”| Input | Stored |
|---|---|
client | SPAN_KIND_CLIENT |
consumer | SPAN_KIND_CONSUMER |
internal | SPAN_KIND_INTERNAL |
producer | SPAN_KIND_PRODUCER |
server | SPAN_KIND_SERVER |
unspecified | SPAN_KIND_UNSPECIFIED |
type (metrics)
Section titled “type (metrics)”| Input | Stored |
|---|---|
counter | counter |
gauge | gauge |
histogram | histogram |
summary | summary |
updown | updown |
severity (logs)
Section titled “severity (logs)”| Input | Stored |
|---|---|
alert | FATAL |
crit | FATAL |
critical | FATAL |
dbg | DEBUG |
debug | DEBUG |
emerg | FATAL |
emergency | FATAL |
err | ERROR |
error | ERROR |
exception | ERROR |
fatal | FATAL |
fine | DEBUG |
finest | TRACE |
info | INFO |
information | INFO |
informational | INFO |
notice | INFO |
panic | FATAL |
severe | ERROR |
trace | TRACE |
verbose | TRACE |
warn | WARN |
warning | WARN |
Severity bands, lowest first: TRACE, DEBUG, INFO, WARN, ERROR, FATAL. unspecified maps to UNSPECIFIED; unknown names are uppercased. Trailing digits are dropped (INFO2 becomes INFO). Unknown severity is legal for equality; sampled stored levels may provide a notice. Unknown status or kind aliases are problems.
Every stored log severity is normalized once: a positive severity number has authority (21+ FATAL, 17+ ERROR, 13+ WARN, 9+ INFO, 5+ DEBUG, 1+ TRACE); absent/nonpositive numbers fall back to text. Invariant: no stored log row bypasses normalization. Severity ranges expand to contiguous band membership, never CASE. severity>=warn means WARN, ERROR, FATAL; severity<error means TRACE, DEBUG, INFO, WARN; between info and error is inclusive. Unknown and UNSPECIFIED levels have no rank and never satisfy a range. Use severity_number>=14 for sublevels. Empty or reversed ranges are problems.
On normalized log rows, error_rate() counts exactly severity>=error. Its backend test is upper(severity) IN ('ERROR', 'FATAL', 'CRITICAL') OR severity_number >= 17; normalization makes legacy CRITICAL redundant. Span error_rate() counts status:error.
Literals and durations
Section titled “Literals and durations”| Literal | Rule |
|---|---|
| String | Bare words or single/double quotes; double a quote to escape it. Quoted values are strings and bypass aliases. |
| Number | Integers, decimals and exponents (500, 1.5, 2e3, -3); non-finite values are rejected. Numbers stay numeric on number columns and attributes, and stay text as typed on string columns. |
| Boolean | Bare true/false, case-insensitive; attributes retain boolean type, string columns compare text, number columns reject them. |
| Duration | us, ms, s, m, h, only on duration; converted to milliseconds. Examples: 500ms, 1.5s, 250us, 2m, 1h. Negative, compound and overflowing durations are rejected. |
| Timestamp | Quoted RFC 3339 or YYYY-MM-DD HH:MM:SS, optional fraction/offset; no offset means UTC. Time comparisons retain nanoseconds through TIMESTAMPTZ_NS. |
| Existence | k:* is IS NOT NULL; -k:* is IS NULL. |
| Free text | Case-insensitive substring of log body or span operation; explicit field required on metrics. |
Contains emits contains(lower(field), lower(value)); map values first cast to VARCHAR. SQL LIKE/ILIKE require quoted patterns; ESCAPE is not expressible, so use ~ when % should be literal.
Buckets
Section titled “Buckets”auto, 10s, 30s, 1m, 5m, 10m, 15m, 30m, 1h, 3h, 6h, 12h, 1d. auto lets the engine choose an interval. A bucket follows every and belongs to an aggregate query.
Measures
Section titled “Measures”The registry defines accepted functions and signal availability. Fields follow the filter/grouping resolution rules. Numeric attribute measures use TRY_CAST AS DOUBLE; nonnumeric values become NULL. count(*) canonicalizes to count().
| Name | Form | Signals | Unit | Semantics |
|---|---|---|---|---|
avg | avg(f) | spans, logs, metrics | field unit | Numeric average. Metrics: gauge/updown point average; histogram sum/count; counter has no value and reports a diagnosis. |
count | count() | spans, logs, metrics | count | Rows; metric observation/point count according to kind. |
count_distinct | count_distinct(f) | spans, logs, metrics | count | Distinct values of any field, including string attributes. |
error_rate | error_rate() | spans, logs | percent | Spans: percent status:error. Logs: percent severity>=error on normalized rows. Unavailable for metrics. |
last | last(f) | metrics | field unit | Metrics only: kind-aware last value. |
max | max(f) | spans, logs, metrics | field unit | Maximum numeric value, retaining metric-kind semantics. |
min | min(f) | spans, logs, metrics | field unit | Minimum numeric value, retaining metric-kind semantics. |
p50 | p50(f) | spans, logs, metrics | field unit | Registered percentile of numeric observations; metric distributions retain native semantics. |
p75 | p75(f) | spans, logs, metrics | field unit | Registered percentile of numeric observations; metric distributions retain native semantics. |
p90 | p90(f) | spans, logs, metrics | field unit | Registered percentile of numeric observations; metric distributions retain native semantics. |
p95 | p95(f) | spans, logs, metrics | field unit | Registered percentile of numeric observations; metric distributions retain native semantics. |
p99 | p99(f) | spans, logs, metrics | field unit | Registered percentile of numeric observations; metric distributions retain native semantics. |
quantile | quantile(f, q) | spans, logs, metrics | field unit | Quantile q strictly between 0 and 1; numeric observations or native metric distribution semantics. |
rate | rate() | spans, logs, metrics | per_second | Spans/logs: rows per second over bucket/window. Metrics: coverage-weighted counter increase per second; counters only. |
share | share() | spans, logs, metrics | percent | Percent of scoped rows in the group; metrics use series population. Standalone only. |
sum | sum(f) | spans, logs, metrics | field unit | Numeric total; metric counters use increases instead of summing cumulative points. |
Metric aggregates require a standalone name:<metric> equality entry. Counters use per-series increases and reset handling; gauge/updown use point values; histograms use observation counts, sums and buckets; summaries use available stored aggregates. Unsupported kind/function combinations report a diagnosis instead of treating counters as gauges.
histogram(duration) on spans uses count with log2 duration buckets; histogram(value) on metrics uses explicit histogram buckets. Distributions stand alone, with at most one by field for a histogram; every selects a heatmap.
Expressions and ranking
Section titled “Expressions and ranking”count_if(filters) takes a FanQL filter list as one entry and compiles to checked count(*) FILTER (WHERE ...)::DOUBLE. Raw entries are allowed; variables are forbidden. Spans and logs support + - * /, constants, unary minus and parentheses on aggregate vizzes. Metrics, distributions and log_patterns do not support arithmetic. Every expression needs at least one aggregate. Other than a single call, an expression requires as alias; aliases are 1–40 lowercase letters/digits/underscores and start with a letter. share() stands alone. Division emits a / nullif(b, 0), so zero denominators yield NULL.
Count-family ratios use ratio units, * 100 makes percent, addition/subtraction retains equal units, scaling retains the measure unit, other combinations use none. Expressions are not additive. Affine transformations inherit the underlying ranking and flip direction for a negative scale. Binomial count_if/count ratios use Wilson ranking with a 20-sample floor; other expressions use value ranking with leaf/denominator support. Ineligible series follow eligible ones by support, then dimension; result metadata discloses them.
Apdex uses disjoint predicates: (count_if(duration<=500ms) + count_if(duration>500ms duration<=2s) / 2) / count() as apdex. Exactly 500 ms counts once; NULL duration counts in the population and neither numerator.
Each measure allows 256 tokens, 64 nodes, depth 8 and 8 aggregates. A query allows 256 expression nodes and 24 aggregates across at most 6 measures. A count_if filter is at most 500 bytes. Whitespace is not a token. Budgets apply to structured inputs too.
Filter functions and raw entries
Section titled “Filter functions and raw entries”| Function | Signature |
|---|---|
contains | contains(field, quoted_string) |
length | length(field) |
lower | lower(field) |
regexp_matches | regexp_matches(field, quoted_string) |
starts_with | starts_with(field, quoted_string) |
upper | upper(field) |
lower, upper and length need a comparison. starts_with, contains and regexp_matches are predicates alone. Calls have no space before (. Variables may compare call results, but remain forbidden inside count_if.
A backtick-delimited raw entry passes SQL to the existing filter gate; double a backtick to include it. A whole raw entry retains its stored spelling; raw fragments composed with other predicates are parenthesized. The gate accepts columns, literal attributes['k']/resource['k'], comparisons, boolean logic, IN, LIKE, IS NULL, BETWEEN, coalesce, variables, the filter functions above and casts to VARCHAR, BIGINT, INTEGER, DOUBLE, BOOLEAN, UBIGINT and TIMESTAMPTZ_NS. It rejects CASE, DISTINCT, ORDER BY and FILTER inside functions, qualified columns, subqueries and other functions. Raw cannot bypass the gate.
Variables and entry boundaries
Section titled “Variables and entry boundaries”$name is a dashboard variable; $__ names are reserved. It stays a parameter, never a string literal. k in $multi accepts multiple values; scalar operators reject multi variables. Without declarations, references produce notices so editing can continue; execution checks them.
All rule: a filter that mentions a variable set to All is removed; keep fixed conditions in their own terms. An empty variable list makes its entry FALSE. Top-level terms are separate entries; a parenthesized group, raw entry or OR chain is one entry. Stored order and boundaries survive formatting.
| Filters | Entries | $service = All |
|---|---|---|
service:$service status:error | 2 | Keep status. |
(service:$service status:error) | 1 | Drop both. |
service:$service or service:worker | 1 | Drop whole entry. |
service:$service (status:error or status:unset) | 2 | Keep status group. |
-service:$service | 1 | Drop entry; nothing excluded. |
Raw SQL mentioning $service | 1 | Drop whole entry. |
count_if(service:$service) | n/a | Rejected. |
Visualization and trace semantics
Section titled “Visualization and trace semantics”Logs/traces use logs/spans respectively and forbid measures, grouping, buckets and histogram. Log_patterns uses count() by body_template, optionally a bucket. Timeseries needs a bucket and at most one grouping field, with one measure when grouped. Tables allow three grouping fields; bars two, with one measure when grouped by two. Stat/gauge need one measure and no grouping/bucket; gauge needs min/max. Scatter needs two measures and one or two grouping fields. State_timeline needs one measure, one grouping field, a bucket and thresholds.
Traces are (namespace, trace_id) candidates with at least one matching span in the window, ranked by matching spans’ max(duration_ms), or min(start_time) for sort +start. Limit applies to candidates; the engine reads at most 1,001 for truncation detection. Each returns one root span, or its earliest span in the window if the root is outside it, with error status if any span in the trace errored. Displayed service and duration are root service and root duration. A child can match while the returned root has different values.
Trace sorts: descending duration (stored duration_ms), errors, or ascending +start. sort errors is the legacy combined key: it also filters to error spans, then orders by duration. Prefer status:error with sort duration. Logs sort by time or +time; log_patterns by count. Aggregate sorts name a measure alias. Default/desc is descending; +/asc is ascending; choose one direction. Bucketed families use time order and options.top for series selection; sort/limit are accepted with a notice when ignored.
Reserved words and foreign syntax
Section titled “Reserved words and foreign syntax”compare, correlate, eval, extend, fields, having, join, let, over, rename, since, top, until, window are reserved at clause positions. Quote them to search for the word. Foreign syntax gets a diagnostic and FanQL hint, never an alias; an ordinary foreign word in filter position stays free text.
Recognized foreign stages: eval, extend, fields, head, order, project, rename, select, sort, stats, summarize, take, top. Foreign call shapes: bin, cardinality, count_over_time, dc, distinct_count, mean, median, pct, perc95, percentile, quantile_over_time, span. from, index=, source=, first-token search, ago(, now(, earliest=, _time:, order by, group by, facet, ==, =~, |=, |~, ::, selectors and @field: get targeted hints. Time ranges belong to the time picker.
Bounds and diagnostics
Section titled “Bounds and diagnostics”Interactive text allows 4,000 code points and 8,192 bytes, 1,024 tokens, 512 AST nodes, nesting depth 16 and lists of 100 values. Queries lower to 16 entries of 500 bytes each, 6 measures and 3 grouping fields, further constrained by the viz. Completion returns at most 50 items. Problems have code-point spans and optional fix-its, follow source order and are capped at 20 plus a final more-problems diagnostic. Any problem prevents execution; requested repair previews are labeled not executable.
Format returns complete text and exactness exact or normalized, with notices for normalization. Raw preserves whole SQL entries. Editable canonical text reparses under its tier without merging entries. Beyond any interactive bound, editable: false carries a reason and complete text; the envelope must preserve the original query and consumers must not parse the oversized text.
Examples
Section titled “Examples”spans where kind:server | error_rate() by service every 1mspans where service:checkout duration>500ms sort duration limit 50spans where service:$service (status:error or status:unset) | count() by http_routespans where kind:server | count_if(http_status_code>=500) / count() * 100 as pct_5xx by http_routespans where kind:server | (count_if(duration<=500ms) + count_if(duration>500ms duration<=2s) / 2) / count() as apdexspans | histogram(duration) by servicespans where service:checkout status:error -http.target:/healthz | count() by http_routespans where user.id:* | count_distinct(user.id) by servicelogs where service:api severity:error timeout sort time limit 200logs where severity>=warn | count() by body_template sort count limit 10metrics where name:http.server.requests | rate() by service every 1mmetrics where name:k8s.container.restarts | last(value) by resource.k8s.pod.nameTransport
Section titled “Transport”Machine text inputs carry visualization, time, comparison, declarations, resolved values, namespace and options as needed. The structured query is required when editable is false. Panels keep storing queries, never text.
Completion accepts a relative window duration or absolute from and to timestamps. Absolute windows and durations over 24h use recent 24h schema, disclosed by schema.recent and schema.notice. With no selected window, the same recent fallback applies.
HTTP exposes /api/fanql/parse, /api/fanql/format, /api/fanql/complete and /api/fanql/reference, requiring telemetry:read. MCP query_telemetry accepts optional text beside panel; agents should prefer panel.query, or panel.sql for complex work. This reference is also fanout://fanql/reference.