Query log

Every statement that crosses the Gateway is written down once, in a fixed shape, into your own bucket. This page is the record itself: what an entry contains, how the objects are laid out, and the three ways to read them — raw objects, a columnar SQL view, or the pre-aggregated summaries built on top.

What is recorded

One entry per request. The entry describes the statement and its outcome — who ran it, what they ran, which rule matched, whether it was served from cache, how long it took, and how many bytes came back.

Result rows are never recorded. The contents of a response are not read and not retained. Neither are raw credentials: the caller’s own token passes through to your engine, and none of it is held. See Trust & security for the full statement and security posture for how that is enforced.

Cache status

Every entry carries a cache status. Four of them take part in hit-rate arithmetic, and the distinction matters when you aggregate — the summary endpoints count non-HIT as a miss, which is a wider set than the strict MISS.

StatusMeaning
HITServed from cache. No warehouse execution.
MISSA cache rule matched, but no valid entry existed — nothing cached yet, TTL expired, or invalidated by a marker or rule. Executed on the warehouse and stored.
BYPASSA rule matched and declined to serve or store this execution from cache.
PASSTHROUGHNo cache rule matched. Forwarded to the warehouse untouched.

Those four are not the whole vocabulary. The Gateway emits twelve statuses, and the other eight — SPOOFED, TELEMETRY, ERROR, INVALIDATION_ERROR, AUTH_ERROR, EXECUTOR, HOLD and BLOCKED — describe requests that were still served, or still refused. BLOCKED is a deny rule turning a statement away.

The summaries count only the first four, so a filter like cacheStatus IN ('HIT','MISS','BYPASS','PASSTHROUGH') in SQL is not the no-op it looks like — it drops every refusal and every error from your result.

Wherever a summary field is named cacheMisses, it counts MISS + BYPASS + PASSTHROUGH. If you want strict misses only, filter on the cache status yourself in a SQL query.

Storage layout

Raw logs are date-partitioned objects in your bucket, one JSON object per file, under an hourly prefix:

logs/YYYY/MM/DD/HH/<file>.json

The same tree is browsable over the API as a one-level listing. Any partial prefix lists its immediate children — a year lists its months, a month lists its days, a day lists its hours, and an hour lists that hour’s log files. A path ending in .json is an object read instead of a listing.

Reading raw objects

MethodPathReturns
GET/tenants/{tenant}/logs/{year}The months with data.
GET/tenants/{tenant}/logs/{year}/{month}The days with data.
GET/tenants/{tenant}/logs/{year}/{month}/{day}The hours with data.
GET/tenants/{tenant}/logs/{year}/{month}/{day}/{hour}The log files for that hour.
GET/tenants/{tenant}/logs/{year}/{month}/{day}/{hour}/{file}One raw log object.

Path segments are zero-padded strings: month 01–12, day 01–31, hour 00–23.

Example

curl -H "Authorization: Bearer $AIRBRX_PAT" \
  "https://api.airbrx.ai/tenants/your-slug/logs/2026/05/19/14"
{
  "tenantId": "your-slug",
  "_links": {
    "self":   { "href": "/tenants/your-slug/logs/2026/05/19/14" },
    "parent": { "href": "/tenants/your-slug/logs/2026/05/19" },
    "items": [
      { "name": "a1b2c3d4.json", "href": "/tenants/your-slug/logs/2026/05/19/14/a1b2c3d4.json" }
    ]
  }
}

Follow _links.items[].href down to a leaf and read the object:

curl -H "Authorization: Bearer $AIRBRX_PAT" \
  "https://api.airbrx.ai/tenants/your-slug/logs/2026/05/19/14/a1b2c3d4.json"

Access control

The caller must be a member of the tenant. Every read of a raw log is itself audit-logged — reading the record leaves a record. Mint a personal access token and scope it to GET on the log paths of the one tenant you mean to read.

Querying the log with SQL

Walking objects hour by hour is the wrong tool for a question like “which uncached statements ran most often last month.” For that, the same log is exposed as a columnar view named proxy_logs, queryable with read-only SQL:

POST /tenants/{tenant}/query

Queries execute on a locked-down DuckDB service against that one fixed view. The workflow — scoping, dialect, limits, and worked examples — is in Query the raw log.

Request

{ "sql": "SELECT * FROM proxy_logs LIMIT 10" }
FieldRequiredDescription
sqlYesA single read-only SELECT against proxy_logs. Multiple statements are rejected, as are file and extension functions. No other field is accepted.

Response

{
  "tenantId": "your-slug",
  "columns": [
    { "name": "cacheStatus", "type": "VARCHAR" },
    { "name": "executions",  "type": "BIGINT" }
  ],
  "rows": [
    { "cacheStatus": "HIT", "executions": 9234 }
  ],
  "rowCount": 1,
  "truncated": false
}
FieldTypeDescription
tenantIdstringTenant the query ran against.
columnsarrayResult columns in order, each with a name and a DuckDB type.
rowsarrayResult rows as objects keyed by column name.
rowCountintegerRows returned, after the enforced cap.
truncatedbooleantrue if the cap cut the result set short. Aggregate or narrow the query rather than paging blindly.

Columns

proxy_logs is a fixed view of 51 data columns plus the day partition. They are listed, with the units, enums and null conventions that decide whether a query is correct, in the proxy_logs column reference.

To see what your own tenant’s table holds right now, ask it for a row and read the columns array off the response:

curl -X POST \
  -H "Authorization: Bearer $AIRBRX_PAT" \
  -H "Content-Type: application/json" \
  -d '{"sql":"SELECT * FROM proxy_logs LIMIT 1"}' \
  "https://api.airbrx.ai/tenants/your-slug/query"

DESCRIBE proxy_logs does not work — the executor accepts SELECT and WITH only.

The per-statement fields on the daily summary query record — statement, hash, cache key, matched rule, cache status, response code, timings, bytes, warehouse execution time — are broadly the same facts, rolled up per statement per day. Broadly, because the two do not use the same names for them and do not always mean the same thing: the three logs maps them against each other.

Three logs, not three views

SurfaceGrainCostsReach for it when
Summaries APIDay, month, year, userOne fetchYou want a dashboard number or a trend, already aggregated.
proxy_logs SQLOne row per requestReal computeYou have a question no rollup answers, and would rather ask it in SQL.
Raw objectsOne object per requestOne fetch eachYou are exporting into your own pipeline, or need the untouched record.

These are three artifacts, not three lenses on one record. The summaries and proxy_logs are derived from the raw logs in parallel, so they do not share a vocabulary and do not always agree — cacheStatus in a summary is the most recent status for that statement that day, not a rollup of the rows beneath it. The three logs is the map: what each is for, what each costs, and exactly where they part company.

See also