Query the raw log
The dashboard answers the questions we anticipated. For everything
else, every statement that crossed the Gateway is one row in a columnar
view called proxy_logs, and you can ask it anything in
read-only SQL — no export, no
parsing project, no warehouse credit spent.
The endpoint
POST /tenants/{tenant}/query
The body is a single field. The response is columns, rows, a count, and a truncation flag. Full shapes are in the query log reference.
{ "sql": "SELECT * FROM proxy_logs LIMIT 10" }
Scope a token first
This runs outside the App, so it needs a non-interactive credential. Mint a personal access token and scope it tightly — the log is the most sensitive thing the API will hand back, because it contains the SQL your people and your models actually wrote.
Scope, in plain language for this use case:
- Method:
POST, for this one path. - Path:
/tenants/{tenant}/query. - Tenant: the single tenant you are investigating.
- Account: the account that owns it.
The caller must be a member of the tenant regardless of scope, and reads against the raw log are audit-logged.
Find out what the columns are
proxy_logs is a fixed view. Rather than trusting a list
that can drift, ask the service what it holds — this is the first
query to run against a new tenant:
curl -X POST \
-H "Authorization: Bearer $AIRBRX_PAT" \
-H "Content-Type: application/json" \
-d '{"sql":"DESCRIBE proxy_logs"}' \
"https://api.airbrx.ai/tenants/your-slug/query"
The facts available are the ones on the record: who ran the statement, the standardized SQL and its hash, the cache key, the rule that matched, the cache status, the response code, the timings (Gateway-side and warehouse-side), and the bytes returned. Result rows are never recorded, so no query you write here can reach the contents of anyone’s data.
What the executor allows
Queries run on a locked-down DuckDB service, so the dialect is DuckDB
— window functions, QUALIFY, GROUP BY ALL
and the rest are available. Four limits apply:
- One statement. A body containing multiple statements is rejected, not silently truncated to the first.
-
Read-only.
SELECTonly. There is nothing to write to. -
No file or extension functions. Anything that would
read outside the view —
read_csv,read_parquet,INSTALL,LOADand friends — is rejected. -
One table.
proxy_logs, scoped to the tenant in the path. There is no cross-tenant query.
Results are capped
Every result set carries an enforced LIMIT. The response
tells you whether you hit it:
"rowCount": 500,
"truncated": true
truncated: true means you are looking at an arbitrary
subset, not the top of a ranking — a fact worth being blunt about,
because a truncated result still looks like an answer. The fix is
almost never to page through raw rows; it is to make the database do the
work. Aggregate, filter to a narrower window, or rank and take the top
N, so the rows you get back are the rows you meant to see.
Worked questions
Column names below follow the record as described above; confirm them
against DESCRIBE proxy_logs for your tenant before saving
anything into a pipeline.
What is the cache actually doing?
SELECT cacheStatus, count(*) AS executions
FROM proxy_logs
GROUP BY ALL
ORDER BY executions DESC
Four rows, and the shape of your traffic.
PASSTHROUGH is the interesting one: those statements
matched no rule at all, so they are the population your next
cache rule
would be drawn from. Note that this is stricter than the summary
endpoints, which fold MISS, BYPASS and
PASSTHROUGH together as misses.
Which uncached statements repeat the most?
SELECT queryHash,
any_value(statement) AS statement,
count(*) AS executions,
count(DISTINCT userId) AS users,
sum(warehouseExecutionTimeMs) AS warehouse_ms
FROM proxy_logs
WHERE cacheStatus = 'PASSTHROUGH'
GROUP BY queryHash
HAVING count(*) > 1
ORDER BY warehouse_ms DESC
LIMIT 25
This is the ranked list of work your warehouse did more than once and did not need to. Sorting by warehouse time rather than execution count puts the expensive repeats first, which is the order you want to write rules in.
What has AI been asking our data?
Agents, copilots and text-to-SQL tools are just another client, so they
land in the same record. Whichever field identifies the caller in your
setup — a service account, a dedicated user, an application name
— the question becomes a WHERE clause rather than a
project:
SELECT userId,
count(*) AS executions,
count(DISTINCT queryHash) AS distinct_statements,
min(timestamp) AS first_seen,
max(timestamp) AS last_seen
FROM proxy_logs
GROUP BY ALL
ORDER BY executions DESC
LIMIT 50
Is a rule earning its keep?
SELECT matchedRule,
count(*) AS executions,
count(*) FILTER (WHERE cacheStatus = 'HIT') AS hits,
round(100.0 * count(*) FILTER (WHERE cacheStatus = 'HIT') / count(*), 1) AS hit_rate_pct
FROM proxy_logs
WHERE matchedRule IS NOT NULL
GROUP BY ALL
ORDER BY executions DESC
A rule with high executions and a low hit rate is usually a cache key that is too specific — every execution computes a different key, so nothing is ever read back. Investigate one of its statements to see the key inputs, then narrow the key.
When to use something else
- A number for a dashboard. The summaries API already has it, pre-aggregated and cheap. Build custom reports covers wiring those into a BI tool.
-
One statement, in detail. The App’s traffic
page shows the key inputs and the miss reason in a form no
SELECTwill match. Investigate a statement. - Everything, into your own pipeline. Read the raw objects from your bucket rather than paging a capped result set.
Where to go next
- Query log reference — the record, the storage layout, and the request and response shapes.
- Scope a token — least-privilege scoping for a log-reading PAT.
- Rule cookbook — turning a repeat you found into a cache rule.