Skip to content

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.

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: 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.

OperatorMeaning
: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.
inMembership in a nonempty list or multi-value variable; missing values are excluded.
not inSQL spelling, SQL meaning: missing values are excluded; prefix - includes them.
betweenInclusive lower and upper bounds separated by and; missing values are excluded.
not betweenSQL spelling, SQL meaning: missing values are excluded; prefix - includes them.
is nullMatches missing values, canonical -k:*.
is not nullMatches present values, canonical k:*.
likeCase-sensitive SQL pattern; requires quoted text; % and _ are wildcards.
ilikeCase-insensitive SQL pattern; requires quoted text.
not likeSQL spelling, SQL meaning: missing values are excluded; prefix - includes them.
not ilikeSQL 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.

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.

NameTypeUnitLow cardinalityDescription
namespacestringnoneOpenTelemetry service namespace
servicestringnoneyesService name
operationstringnoneSpan name
kindstringnoneyesSPAN_KIND_CLIENT, SPAN_KIND_CONSUMER, SPAN_KIND_INTERNAL, SPAN_KIND_PRODUCER, SPAN_KIND_SERVER, SPAN_KIND_UNSPECIFIED
statusstringnoneyesSTATUS_CODE_ERROR, STATUS_CODE_OK, STATUS_CODE_UNSET
status_messagestringnoneStatus description
trace_idstringnoneTrace identifier
span_idstringnoneSpan identifier
parent_span_idstringnoneParent span identifier; empty for roots
start_timetimenoneSpan start
end_timetimenoneSpan end
duration_msnumbermsSpan duration in milliseconds
http_methodstringnoneyesHTTP method
http_status_codestringnoneHTTP response status code as text
http_routestringnoneHTTP route template
db_systemstringnoneyesDatabase system, e.g. postgresql, redis
rpc_methodstringnoneRPC method
rpc_servicestringnoneRPC service
peer_servicestringnoneRemote service named by the caller
service_versionstringnoneyesservice.version resource attribute
deployment_envstringnoneyesdeployment.environment resource attribute
exception_typestringnoneException type from the span’s exception event
exception_messagestringnoneException message
messaging_systemstringnoneyesmessaging.system attribute
messaging_destinationstringnonemessaging.destination.name attribute
scope_namestringnoneInstrumentation scope
attributesmapnoneSpan attributes; use attributes[‘key’]
resourcemapnoneResource attributes; use resource[‘key’]
NameTypeUnitLow cardinalityDescription
namespacestringnoneOpenTelemetry service namespace
servicestringnoneyesService name
timetimenoneLog time
severitystringnoneyesSeverity text, e.g. ERROR, WARN, INFO
severity_numbernumbernoneOpenTelemetry severity number; 17 and above is an error
bodystringnoneRedacted log body
body_templatestringnoneBody with variable parts replaced by <*>; groups similar messages
trace_idstringnoneTrace identifier
span_idstringnoneSpan identifier
scope_namestringnoneInstrumentation scope
attributesmapnoneLog attributes; use attributes[‘key’]
resourcemapnoneResource attributes; use resource[‘key’]
NameTypeUnitLow cardinalityDescription
namespacestringnoneOpenTelemetry service namespace
servicestringnoneyesService name
timetimenoneData point time
namestringnoneMetric name
unitstringnoneMetric unit as reported
typestringnoneyescounter, gauge, histogram, summary, updown
temporalitystringnoneyescumulative or delta; empty for gauge and summary
start_timetimenoneData point start time; NULL when absent
valuenumbernoneGauge or sum value
hist_countnumbercountHistogram point count
hist_sumnumbernoneHistogram point sum
attributesmapnoneData point attributes; use attributes[‘key’]
resourcemapnoneResource 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'.

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.

InputStored
errorSTATUS_CODE_ERROR
okSTATUS_CODE_OK
unsetSTATUS_CODE_UNSET
InputStored
clientSPAN_KIND_CLIENT
consumerSPAN_KIND_CONSUMER
internalSPAN_KIND_INTERNAL
producerSPAN_KIND_PRODUCER
serverSPAN_KIND_SERVER
unspecifiedSPAN_KIND_UNSPECIFIED
InputStored
countercounter
gaugegauge
histogramhistogram
summarysummary
updownupdown
InputStored
alertFATAL
critFATAL
criticalFATAL
dbgDEBUG
debugDEBUG
emergFATAL
emergencyFATAL
errERROR
errorERROR
exceptionERROR
fatalFATAL
fineDEBUG
finestTRACE
infoINFO
informationINFO
informationalINFO
noticeINFO
panicFATAL
severeERROR
traceTRACE
verboseTRACE
warnWARN
warningWARN

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.

LiteralRule
StringBare words or single/double quotes; double a quote to escape it. Quoted values are strings and bypass aliases.
NumberIntegers, 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.
BooleanBare true/false, case-insensitive; attributes retain boolean type, string columns compare text, number columns reject them.
Durationus, ms, s, m, h, only on duration; converted to milliseconds. Examples: 500ms, 1.5s, 250us, 2m, 1h. Negative, compound and overflowing durations are rejected.
TimestampQuoted RFC 3339 or YYYY-MM-DD HH:MM:SS, optional fraction/offset; no offset means UTC. Time comparisons retain nanoseconds through TIMESTAMPTZ_NS.
Existencek:* is IS NOT NULL; -k:* is IS NULL.
Free textCase-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.

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.

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().

NameFormSignalsUnitSemantics
avgavg(f)spans, logs, metricsfield unitNumeric average. Metrics: gauge/updown point average; histogram sum/count; counter has no value and reports a diagnosis.
countcount()spans, logs, metricscountRows; metric observation/point count according to kind.
count_distinctcount_distinct(f)spans, logs, metricscountDistinct values of any field, including string attributes.
error_rateerror_rate()spans, logspercentSpans: percent status:error. Logs: percent severity>=error on normalized rows. Unavailable for metrics.
lastlast(f)metricsfield unitMetrics only: kind-aware last value.
maxmax(f)spans, logs, metricsfield unitMaximum numeric value, retaining metric-kind semantics.
minmin(f)spans, logs, metricsfield unitMinimum numeric value, retaining metric-kind semantics.
p50p50(f)spans, logs, metricsfield unitRegistered percentile of numeric observations; metric distributions retain native semantics.
p75p75(f)spans, logs, metricsfield unitRegistered percentile of numeric observations; metric distributions retain native semantics.
p90p90(f)spans, logs, metricsfield unitRegistered percentile of numeric observations; metric distributions retain native semantics.
p95p95(f)spans, logs, metricsfield unitRegistered percentile of numeric observations; metric distributions retain native semantics.
p99p99(f)spans, logs, metricsfield unitRegistered percentile of numeric observations; metric distributions retain native semantics.
quantilequantile(f, q)spans, logs, metricsfield unitQuantile q strictly between 0 and 1; numeric observations or native metric distribution semantics.
raterate()spans, logs, metricsper_secondSpans/logs: rows per second over bucket/window. Metrics: coverage-weighted counter increase per second; counters only.
shareshare()spans, logs, metricspercentPercent of scoped rows in the group; metrics use series population. Standalone only.
sumsum(f)spans, logs, metricsfield unitNumeric 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.

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.

FunctionSignature
containscontains(field, quoted_string)
lengthlength(field)
lowerlower(field)
regexp_matchesregexp_matches(field, quoted_string)
starts_withstarts_with(field, quoted_string)
upperupper(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.

$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.

FiltersEntries$service = All
service:$service status:error2Keep status.
(service:$service status:error)1Drop both.
service:$service or service:worker1Drop whole entry.
service:$service (status:error or status:unset)2Keep status group.
-service:$service1Drop entry; nothing excluded.
Raw SQL mentioning $service1Drop whole entry.
count_if(service:$service)n/aRejected.

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.

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.

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.

spans where kind:server | error_rate() by service every 1m
spans where service:checkout duration>500ms sort duration limit 50
spans where service:$service (status:error or status:unset) | count() by http_route
spans where kind:server | count_if(http_status_code>=500) / count() * 100 as pct_5xx by http_route
spans where kind:server | (count_if(duration<=500ms) + count_if(duration>500ms duration<=2s) / 2) / count() as apdex
spans | histogram(duration) by service
spans where service:checkout status:error -http.target:/healthz | count() by http_route
spans where user.id:* | count_distinct(user.id) by service
logs where service:api severity:error timeout sort time limit 200
logs where severity>=warn | count() by body_template sort count limit 10
metrics where name:http.server.requests | rate() by service every 1m
metrics where name:k8s.container.restarts | last(value) by resource.k8s.pod.name

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.