Execute a read-only QuerySQL SELECT against the observability data.
QuerySQL is standard SQL (MySQL-compatible syntax, backtick-quoted identifiers)
with automatic tenant isolation. Write normal SQL — most standard features work:
WHERE, GROUP BY, HAVING, ORDER BY, LIMIT, DISTINCT, CASE WHEN, LIKE, ILIKE,
BETWEEN, IN, !=, <>, IS NULL, IS NOT NULL, NOT, OR, AND, subqueries, derived tables,
JOINs, aliases, COALESCE, IF. Also =~ 'pattern' (case-insensitive match, * wildcard);
= / != with a *-wildcard string value behave as ILIKE / NOT ILIKE.
Free-text search: matches('text') in WHERE searches the message, all attributes,
and service case-insensitively (substring match; trace/span ids by exact match), e.g.
SELECT * FROM logs WHERE matches('connection refused').
Call describe_schema first to discover available fields and dynamic attributes
for your data.
Sources: logs, spans, metrics. Dynamic attributes are queryable directly by
name, dots included: http.request.method. Resource attributes need the
resource. prefix: resource.service.name (logs and spans only; metrics does not
expose resource attributes). Missing attributes read as NULL.
Common fields per source:
logs: timestamp, service, level, message, trace_id, span_id,
parent_span_id, source_instance_id, log_id
spans: timestamp, service, name, kind, status_code, status_message,
trace_id, span_id, parent_span_id, source_instance_id, duration_ms
metrics: metric_name, service, source_instance_id, timestamp, value
Custom functions:
count(), count_distinct(field), countIf(condition), sum(field), avg(field),
min(field), max(field), p50(field), p95(field), p99(field),
contains(field, 'token'), error_rate() (percentage, 0-100), request_count(), error_burn_rate(budget),
latency_burn_rate(field, threshold, budget), rate(field), value(field),
bucket(field, 'interval'), now(), regexp_extract(field, 'pattern' [, group]).
bucket(timestamp, '5m') groups by time. Intervals: <number><unit> with unit
m, h, or d (e.g. 1m, 5m, 30m, 1h, 6h, 1d).
For a query that selects a single aliased bucket, groups by it alone, orders by it, and has no LIMIT,
interior gaps between the first and last returned bucket are zero-filled in the response
(numeric columns 0, others null). Buckets outside the data range are not invented; other
query shapes still return only non-empty buckets.
count(DISTINCT field) is accepted and is the same as count_distinct(field).
DISTINCT inside any other aggregate (sum, avg, p95, ...) is rejected with an
error rather than ignored.
regexp_extract returns the first regex match (or capture group if specified).
Returns null on no match. Example: regexp_extract(message, 'status=(\d+)', 1).
Burn-rate rules (declared SLO): error_burn_rate(budget) is the error share divided by
your budget (0.001 = 99.9% SLO); latency_burn_rate(duration_ms, 500, 0.03) is the share
of requests over 500ms divided by a 3% budget. Alert when the result exceeds a burn
multiple (e.g. GT 6 over a 60-minute window).
Limitations:
- Read-only SELECT only (no INSERT/UPDATE/DELETE/UNION).
- No CROSS JOIN (use explicit JOIN ... ON).
- No SYMMETRIC BETWEEN (order the bounds and use plain BETWEEN).
- JOINs require qualified field references (e.g. l.service, s.name).
- contains(field, 'token') matches whole alphanumeric tokens only (inverted index);
it raises an error on non-string fields.
Prefer purpose-built tools when they fit: use correlate when you have a trace id
(returns spans, logs, and metric exemplars in one call), get_trace for the span tree
alone, and aggregate_spans to find where errors or latency are concentrated before
drilling in. Use run_sql for ad-hoc analysis that the other tools don't cover.
Examples:
SELECT service, count(*) FROM logs WHERE level = 'ERROR' GROUP BY service
SELECT service, p95(duration_ms) FROM spans GROUP BY service
SELECT bucket(timestamp, '5m') AS t, count(*) FROM logs GROUP BY t ORDER BY t
SELECT http_method, count(*) FROM logs GROUP BY http_method
SELECT http.response.status_code, count(*) FROM logs GROUP BY http.response.status_code
SELECT s.name, l.message FROM spans s JOIN logs l ON s.trace_id = l.trace_id
SELECT service FROM logs WHERE service IN (SELECT DISTINCT service FROM spans)
SELECT error_burn_rate(0.001) AS value FROM spans WHERE service = 'my-svc'
Each successful logs-only query also returns an explorerUrl opening the same
query in the Fixter logs explorer (grid view; trace_id/span_id cells link to
the trace waterfall). Attach it when citing rows as evidence to the user. The
link's time window is derived from the returned rows' timestamps (or defaults
to the last 30 days). explorerUrl is absent when the query errored, referenced
spans or metrics anywhere (the logs page renders only logs), or contained
double quotes (use single quotes for string literals), or used a query shape
the explorer cannot reproduce.