proxy_logs columns
proxy_logs is the queryable log — one row per request,
51 data columns plus the day partition, read with read-only
SQL over
POST /tenants/{tenant}/query. This page is the column list,
and the handful of conventions that decide whether your query is
correct or merely plausible.
New to the three logs and not sure this is the one you want? Start with the three logs.
Read this first
Five conventions account for almost every wrong answer this table produces. None of them are guessable from a column name.
-
dayis the only column that prunes. It is the partition, aVARCHARof the form'YYYY-MM-DD'. Compare it as a string, not aDATE. Every query should carry adaypredicate — without one, evenSELECT count(*)reads the tenant’s entire history and will likely hit the 15-second timeout. -
Times are epoch milliseconds, not
TIMESTAMP.timestampandcacheExpiresAtboth. Wrap them:to_timestamp(timestamp / 1000). -
responseSizeandresponseTimeMsreport “unmeasured” as a number, not null.0when nothing could be measured, and-1onresponseSizewhen the Gateway explicitly said so. Filter them with> 0— not>= 0, and notIS NOT NULL. Every other numeric column uses null, which aggregates skip on their own. -
tablesandparameterValuesare JSON text, not arrays. Open them withjson_extract. The row shape is deliberately flat so nothing reading it needs nested-type support. - Every column is nullable, including on rows written before that column existed. A null can mean “the request carried no value” or “this field did not exist yet.”
Confirming the list yourself
The table below is generated from the same definition the query service
serves, but a tenant’s table picks up new columns on its own
schedule. To see exactly what yours holds, 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, and
rejects everything else with a 400.
The columns
Partition
| Column | Type | What it holds |
|---|---|---|
day | VARCHAR | Partition column: UTC date as an ISO string 'YYYY-MM-DD'. Filter on this for time ranges (e.g. day >= '2026-06-01') so DuckDB prunes partitions; compare as a string, not a DATE. |
Request
| Column | Type | What it holds |
|---|---|---|
timestamp | BIGINT | Request time as epoch milliseconds. Use to_timestamp(timestamp / 1000) for a readable time. For date-range filters prefer the partition column day. |
userName | VARCHAR | Display name of the data consumer who issued the request. |
statusCode | INTEGER | HTTP-style status code returned to the consumer. |
responseSize | BIGINT | Size of the response body returned to the consumer. Never null: 0 when the field was absent, -1 when the Gateway reported it unmeasured. Use responseSize > 0 to exclude non-measurements. Unit: bytes. |
requestSize | BIGINT | Measured size of the request body. Null when unmeasured. Unit: bytes. |
responseTimeMs | BIGINT | End-to-end time to serve the request. This is the field to use for "slow query" questions. Never null: 0 when nothing could be measured. Unit: ms. |
warehouseExecutionTimeMs | INTEGER | Time the warehouse itself spent on the request. This is what a HIT avoided; responseTimeMs is wall-clock through the proxy and cannot answer that. Null when unmeasured, including requests the warehouse never saw. Unit: ms. |
Cache decision
| Column | Type | What it holds |
|---|---|---|
cacheStatus | VARCHAR | Outcome of the proxy cache lookup. Only HIT, MISS, BYPASS and PASSTHROUGH count toward hit rate; the rest are error, hold and telemetry outcomes that still served a request. BLOCKED is a deny-rule refusal. One of: HIT, MISS, BYPASS, PASSTHROUGH, SPOOFED, TELEMETRY, ERROR, INVALIDATION_ERROR, AUTH_ERROR, EXECUTOR, HOLD, BLOCKED. |
missReason | VARCHAR | Why a MISS missed. Null when the request did not miss, or when the Gateway recorded no reason. One of: no-cache-entry, ttl-expired, invalidation-marker. |
cacheKey | VARCHAR | Cache key the proxy computed for the request. Null for BYPASS and PASSTHROUGH, where no cache entry exists, so cacheKey IS NOT NULL selects the requests that touched the cache. |
cacheKeyVersion | INTEGER | Version of the cache-key scheme used to compute cacheKey. |
matchedRule | VARCHAR | Identifier of the cache/routing rule that matched, if any. Stable across edits; matchedRuleName is what a human recognizes. |
matchedRuleName | VARCHAR | Human-readable name of the rule identified by matchedRule. |
ruleVersion | VARCHAR | Version of the matched rule as it stood when the request was served. |
cacheTtlSeconds | INTEGER | Time-to-live the matched rule applied to the cache entry. Unit: s. |
cacheAgeSeconds | INTEGER | Age of the cache entry when it was served. Unit: s. |
cacheExpiresAt | BIGINT | When the cache entry expires, as epoch milliseconds — the same units as timestamp. |
The SQL
| Column | Type | What it holds |
|---|---|---|
standardizedSql | VARCHAR | Normalized SQL statement the consumer ran (parameters removed). |
queryHash | VARCHAR | Stable hash identifying the statement. Cached paths carry the Gateway hash, which covers the statement and its bound parameters; BYPASS and PASSTHROUGH rows are re-hashed from standardizedSql alone, so their variants collapse to one template hash. |
parameterValues | VARCHAR | JSON-encoded bound parameter values for the statement, held as text — open it with json_extract. |
statementType | VARCHAR | Statement kind the parser identified, such as SELECT or INSERT. |
isReadOnly | BOOLEAN | True when the statement only reads. |
isDataChange | BOOLEAN | True when the statement modifies data. |
isDDL | BOOLEAN | True when the statement changes schema rather than data. |
cacheOverride | VARCHAR | Caching forced by a directive in a SQL comment on the statement. Null when the statement carried no directive. One of: cache, nocache. |
hasParameters | BOOLEAN | True when the statement was parameterized. |
hasNonDeterministicFunctions | BOOLEAN | True when the statement calls a function whose result can differ between runs, which is why an otherwise repeatable statement may not be cacheable. |
tableCount | INTEGER | Number of tables the statement references. |
tables | VARCHAR | JSON-encoded array of the tables the statement references, held as text — open it with json_extract. Null when it references none. |
Correlation
| Column | Type | What it holds |
|---|---|---|
logId | VARCHAR | Identifier of the raw log record this row was derived from. |
queryId | VARCHAR | The warehouse own query ID. Join key to warehouse query history, so a HIT can be tied to the cost it avoided. |
sessionId | VARCHAR | Consumer session the request belonged to. |
operationId | VARCHAR | Identifier of the protocol operation the request carried. |
operationType | VARCHAR | Kind of protocol operation the request carried. |
adapterType | VARCHAR | Warehouse adapter that handled the request. One of: databricks, snowflake, postgresql. |
serverHostname | VARCHAR | Server hostname the Gateway recorded for the request. |
Caller identity and request headers
These are here so a security review is a query rather than a grep over raw JSON. Every value except clientIp is supplied by the caller and copied down verbatim — inspect it, never trust it. clientIp is the address the Gateway resolved through its trusted-proxy rules, which is why it, and not xForwardedFor, is the one that agrees with the audit log.
| Column | Type | What it holds |
|---|---|---|
clientIp | VARCHAR | Caller address as the Gateway resolved it through its trusted-proxy rules, so this agrees with the audit log rather than forming a separate opinion about which hop was the client. |
userTokenHash | VARCHAR | Hash of the caller token. Identifies a caller across requests without carrying the token itself. |
userAgent | VARCHAR | User-Agent header, typically identifying the driver and its version. |
host | VARCHAR | Host header. |
origin | VARCHAR | Origin header. |
referer | VARCHAR | Referer header. |
connection | VARCHAR | Connection header. |
contentType | VARCHAR | Content-Type header. |
contentLength | VARCHAR | Content-Length header verbatim, as text — see requestSize for measured bytes. |
contentEncoding | VARCHAR | Content-Encoding header. |
acceptEncoding | VARCHAR | Accept-Encoding header. |
xForwardedFor | VARCHAR | X-Forwarded-For header. |
xForwardedProto | VARCHAR | X-Forwarded-Proto header. |
xForwardedHost | VARCHAR | X-Forwarded-Host header. |
xRealIp | VARCHAR | X-Real-IP header. |
Worked queries
Each of these carries a day predicate. Copy that habit
before you copy anything else.
What is the cache actually doing?
SELECT cacheStatus, count(*) AS executions
FROM proxy_logs
WHERE day >= '2026-06-01'
GROUP BY ALL
ORDER BY executions DESC
Expect more than four rows. 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. BLOCKED is a
deny rule refusing a
statement, not an error.
Which uncached statements repeat the most?
SELECT queryHash,
any_value(standardizedSql) AS statement,
count(*) AS executions,
count(DISTINCT userName) AS users,
sum(warehouseExecutionTimeMs) AS warehouse_ms
FROM proxy_logs
WHERE day >= '2026-06-01'
AND cacheStatus = 'PASSTHROUGH'
GROUP BY queryHash
HAVING count(*) > 1
ORDER BY warehouse_ms DESC
LIMIT 25
The ranked list of work your warehouse did more than once and did not
need to. Sorting by warehouseExecutionTimeMs rather than
execution count puts the expensive repeats first, which is the order you
want to write rules in — and it is the right column for the job,
because responseTimeMs is wall-clock through the Gateway
and cannot tell you what the warehouse itself spent.
Is a rule earning its keep?
SELECT matchedRuleName,
ruleVersion,
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 day >= '2026-06-01'
AND matchedRule IS NOT NULL
GROUP BY ALL
ORDER BY executions DESC
High executions with a low hit rate usually means a cache key that is
too specific — every execution computes a different key, so
nothing is ever read back. missReason will tell you which
kind of miss it is.
Everything one caller did
SELECT to_timestamp(timestamp / 1000) AS at,
userName, userAgent, cacheStatus, statusCode, standardizedSql
FROM proxy_logs
WHERE day >= '2026-06-01'
AND clientIp = '203.0.113.24'
ORDER BY timestamp
Security review is a first-class use of this table, which is why the
caller columns are here: clientIp,
userTokenHash, userAgent and the forwarded
header set. They are already in the raw logs this table derives from,
under the same tenant prefix and the same retention — a column
adds query reach, not exposure.
What did the cache save us?
SELECT day,
count(*) FILTER (WHERE cacheStatus = 'HIT') AS hits,
sum(warehouseExecutionTimeMs) FILTER (WHERE cacheStatus <> 'HIT') AS warehouse_ms
FROM proxy_logs
WHERE day >= '2026-06-01'
GROUP BY day
ORDER BY day
For tying a HIT to the cost it avoided,
queryId is the join key into your warehouse’s own
query history.
What is not here
-
originalSql. This table carriesstandardizedSql, which has literal values removed. The statement with its literals inline exists only in the raw logs. -
Response headers, and any fingerprint of the result
data. Neither is captured anywhere. Every hash the Gateway
computes —
queryHash,userTokenHash,cacheKey— covers request-side input. Two rows with the samequeryHashasked the same question; the table cannot say whether they got the same answer. - Result rows. Never read, never retained. No query you write here can reach the contents of anyone’s data.
See also
- The three logs — which log answers which question, and where they disagree.
- Query the raw log — the workflow: scoping a token, the limits, what the executor allows.
- Query log reference — the record itself, and the raw storage layout.
- Analytics API — the pre-aggregated summaries, when a rollup will do.