The three logs
Every statement that crosses the Gateway is written down three times: once as a raw log, once into a summary, and once as a row in a queryable columnar view. Same event, three artifacts — and they are not three lenses on one record. They differ in grain, in vocabulary, and above all in what they cost to read. Picking the right one is most of the skill.
Start with the cost
None of the three is gated. All are open to any member of the tenant, which means the only thing steering you to the right one is knowing what each does when you ask it a question.
Summarized
Already computed
Raw
Cheap per call, costly in calls
Queryable
Real compute
Precomputed, then round trips, then compute. If a
summary can answer your question, let it. Reach for
proxy_logs when the answer genuinely isn’t in a
rollup — that is what it is for, and it is very good at it.
Summarized — the answer, already computed
A scheduled run rolls each day up and writes it as JSON: totals, hit and miss counts, response-time statistics, an hour-by-hour breakdown, and one record per distinct statement. Months and years are rolled up the same way, with a per-user cut alongside.
GET /tenants/{tenant}/summaries/{year}/{month}/{day}
The grain is one record per distinct statement per day. Ten thousand executions of the same query are one record with a count of ten thousand. That is what makes it cheap, and it is also the limit: a summary cannot tell you about one execution, only about a day of them.
This is what the dashboard reads, and what a BI tool should read. Full field list in the Analytics API reference.
Raw — the untouched record
The Gateway writes one JSON object per request into your own bucket,
under an hourly prefix. This is the source of truth — the other
two are derived from it — and the only one carrying every field
the Gateway captured, including originalSql with its
literal values still inline.
logs/YYYY/MM/DD/HH/<file>.json
The same tree is browsable over the API one level at a time: a year lists its months, a month its days, a day its hours, an hour its files. Which is exactly the cost — finding one request means walking down, and reading a day means fetching every object in it, one by one. Every read of a raw log is itself audit-logged.
Reach for raw when you are exporting into your own pipeline, or when you need a field that exists nowhere else. Don’t reach for it to answer a question — that is what the other two are for. Paths and examples in the query log reference.
Queryable — one row per request, in SQL
The same raw logs are also written into a columnar table, exposed as a
single fixed view called proxy_logs and read with read-only
SQL:
POST /tenants/{tenant}/query
The grain is one row per request — the same grain as raw, but columnar, so you can ask questions across millions of them without fetching any. It carries far more per request than the summaries do: the cache decision and why it went that way, the shape of the SQL, the warehouse’s own query ID, and the caller’s address, user agent and forwarded headers. A security review is a query here rather than a grep over raw JSON.
It is also the one that costs something to run. Three limits shape every query you write:
-
Filter on
day, always. It is the partition column, and the only thing that lets the engine skip files. Without it, a query reads the tenant’s entire history —SELECT count(*)included. - Results are capped at 1,000 rows. The cap bounds what comes back, not what gets scanned. Aggregate rather than paging.
-
15 seconds, then it is cancelled with a
408. A query that times out has usually forgotten the first rule.
The workflow, with worked examples, is
Query the
raw log; the columns are in the
proxy_logs
column reference.
How they fit together
The Gateway writes the raw log. A scheduled run then reads those raw objects and writes the other two side by side — the summaries and the columnar table are siblings, derived in parallel from the same source. Neither is built from the other.
Gateway ──► raw logs ──┬──► summaries (rolled up per day)
└──► proxy_logs (one row per request)
That shape is worth holding onto, because it explains the next section. Two independent derivations of the same event will not agree about everything, and knowing where they part company is the difference between a correct number and a plausible one.
Where they disagree
| Field | In a summary | In proxy_logs |
|---|---|---|
cacheStatus |
The status of the most recent execution of that statement that day — a sample, not a rollup. | The status of that one request. |
| Miss arithmetic | cacheMisses counts MISS + BYPASS + PASSTHROUGH. |
Twelve distinct statuses. You decide what counts as a miss. |
cacheKey, when there is none |
Empty string. | NULL, so cacheKey IS NOT NULL selects the requests that touched the cache. |
parameterValues |
An array. | JSON held as text — open it with json_extract. |
The first row catches people out most often. A daily summary’s
cacheStatus looks like it summarizes that statement’s
day. It doesn’t — it is whichever status was seen last. If
you need the distribution, count it in proxy_logs.
Twelve statuses, not four
HIT, MISS, BYPASS and
PASSTHROUGH are the four that take part in hit-rate
arithmetic, and the four the summaries count. They are not the only four
the Gateway emits. proxy_logs also carries
SPOOFED, TELEMETRY, ERROR,
INVALIDATION_ERROR, AUTH_ERROR,
EXECUTOR, HOLD and BLOCKED
— that last one being a
deny rule refusing a
statement.
So a filter like
cacheStatus IN ('HIT','MISS','BYPASS','PASSTHROUGH') is not
the no-op it looks like. It silently drops every refusal and every error
from your result.
The same fact, different names
The two derived logs were built at different times for different readers, and they do not share a vocabulary. When you carry a field name from one page to another, translate it:
| The fact | In a summary | In proxy_logs |
|---|---|---|
| The SQL, normalized | statement | standardizedSql |
| Who ran it | userId | userName |
| Which rule matched | matchedRule (the ID) | matchedRule (the ID), plus matchedRuleName |
| When it ran | ISO timestamps | timestamp as epoch milliseconds; day as 'YYYY-MM-DD' |
What lives in only one place
-
originalSql— raw only. The statement with its literal values still inline.standardizedSql, which both derived logs carry, has them removed. -
Caller identity, request headers, and SQL shape —
proxy_logsonly. The summaries never gained them. -
Response headers, and any fingerprint of the result data
— nowhere. The Gateway captures request headers only,
and every hash it computes covers request-side input. Two rows with
the same
queryHashasked the same question; no log can tell you whether they got the same answer.
Result rows are never recorded anywhere. The contents of a response are not read and not retained, in any of the three. See security posture.
Which one do I want?
| The question | The log |
|---|---|
| A number for a dashboard, or a trend over months. | Summarized |
| Our hit rate last quarter, by user. | Summarized |
| Which uncached statements repeat the most, and what do they cost? | Queryable |
| Which addresses hit this tenant last week, and what did they run? | Queryable |
| Why did this particular execution miss? | Queryable, or the App |
| Everything, into our own pipeline or SIEM. | Raw |
| The literal values in a statement. | Raw |
Where to go next
- Analytics API — the summaries, endpoint by endpoint.
- Query log reference — what a record contains, and the raw storage layout.
proxy_logscolumns — all 51, grouped by what they answer.- Query the raw log — the SQL workflow, with worked examples.
- Security posture — what is retained, and what never is.
Ask the log something
Querying proxy_logs starts with a least-privilege token.