Query reference: operators & syntax
How to filter — in both the Builder (structured rows) and SQL mode. The store is ClickHouse, so SQL mode is standard ClickHouse SQL. See Table & column reference for the columns.
Builder filter operators
Each + Add filter row is field · operator · value. Available operators:
| Operator | Meaning | Example value |
|---|---|---|
= / != | Exact match / not | level = error |
IN / NOT IN | One of a set | namespace IN (payments, checkout) |
LIKE / NOT LIKE | Wildcard (% = any run, _ = one char) | message LIKE %timeout% |
> >= < <= | Numeric / time comparison | duration_ns > 100000000 |
Filters combine with AND. For OR / regex / JSON-field logic, switch to SQL mode.
SQL mode — the patterns you'll actually use
Text search (substring, case-sensitive):
WHERE message LIKE '%connection refused%'
Case-insensitive: WHERE positionCaseInsensitive(message, 'refused') > 0.
Regex:
WHERE match(message, 'timeout|deadline exceeded')
Filter by a JSON attribute (labels, parsed_fields, span attributes are JSON strings):
-- spans where the HTTP status was 500 WHERE JSONExtractString(attributes, 'http.status_code') = '500' -- logs for pods labelled team=payments WHERE JSONExtractString(labels, 'team') = 'payments'
Time: write the range literally with ClickHouse intervals. The editor substitutes nothing into your SQL — there are no $from/$to placeholders, and using them is a syntax error:
WHERE timestamp > now() - INTERVAL 30 MINUTE -- logs/metrics WHERE start_time >= now() - INTERVAL 1 HOUR -- spans use start_time
Duration (nanoseconds!):
-- spans slower than 250 ms WHERE duration_ns > 250 * 1e6 SELECT name, duration_ns/1e6 AS ms FROM spans ORDER BY ms DESC
Aggregate:
SELECT service_name, quantile(0.99)(duration_ns)/1e6 AS p99_ms FROM spans WHERE start_time > now() - INTERVAL 1 HOUR GROUP BY service_name ORDER BY p99_ms DESC
Rules & limits
| Limit | Value | On hitting it |
|---|---|---|
| Statement type | SELECT and WITH only | 400. INSERT/UPDATE/DELETE/DROP/ALTER/CREATE/OPTIMIZE/SYSTEM/SET/USE and the rest are rejected outright. |
| Result rows | 10,000 | Silently capped. Aggregate or narrow the range rather than paging. |
| SQL length | 1 MiB | 400. |
| Timeout | 30 seconds | 504 — narrow the window, add filters, add LIMIT. |
| Concurrent queries | 5 across the whole deployment | 429 "too many concurrent queries". Retry shortly. |
10,000 rows is not the Logs export cap. The Log Explorer's CSV export is capped separately, at 100,000 rows. The query editor has no export at all — it is a result grid, and 10,000 is the hard ceiling on what a query returns.
Heavy queries run on a separate read-only connection pool, so they cannot stall the rest of the UI. Saving a Builder query writes to the URL — share the URL to reproduce it exactly.
Why a query gets refused
Two guards sit in front of the database, and both refuse rather than silently altering your results.
Rejected outright
- Quoted or escaped table names. Write
FROM logs, not `FROMlogs` — a quoted or backslash-escaped identifier is refused, because a name that has to be decoded before it can be recognised cannot be reliably matched against the tables that need scoping. - Database-qualified names.
observekit.logsis refused; use the barelogs. - Table functions that read outside the database —
url,file,s3,hdfs,remote,cluster,jdbc,odbc,mysql,postgresql,mongodb,redis,iceberg,executableand others. - Table functions that pick tables by pattern at runtime —
merge,loop,mergeTreeIndex,mergeTreeProjection. These name no table in the SQL text, so nothing can constrain what they read. (view()andviewIfPermitted()are fine — their subqueries are ordinary SQL.) system.*andinformation_schema.*** — server internals, not your telemetry.
Silently narrowed: source scoping
If you are not an admin, your query is rewritten before it runs so it can only see the sources you are scoped to. You do not see the rewrite and you get no warning.
This is the explanation for the confusing case: you and an admin run the identical query and get different row counts, or you get zero rows from a table you know has data. Nothing is broken — you are scoped to fewer sources. A non-admin with no sources in scope gets a 403 rather than an empty result, so "no access" never masquerades as "no data".
Because the rewrite works on table names in the SQL text, a query that hides a table name from it is refused rather than run unscoped — which is why the identifier rules above are strict.
Quick recipes
-- Top error messages, last hour SELECT message, count() AS n FROM logs WHERE level = 'error' AND timestamp > now() - INTERVAL 1 HOUR GROUP BY message ORDER BY n DESC LIMIT 20; -- All logs for one request (pivot from a trace) SELECT timestamp, level, message FROM logs WHERE trace_id = '<trace-id>' ORDER BY timestamp; -- Error spans by service SELECT service_name, count() AS errors FROM spans WHERE status_code = 'ERROR' AND start_time > now() - INTERVAL 1 HOUR GROUP BY service_name ORDER BY errors DESC;