feat!: add query_logs tool for custom log queries - #333
Conversation
barryroodt
left a comment
There was a problem hiding this comment.
Really clean addition, Jordi. It mirrors the merged get_logs sibling almost exactly (same endpoint, same untrusted-data wrapping, same 24h defaulting), and I reproduced the checks against the PR head: tsc --noEmit, biome ci ., and the server.test.ts + logs.test.ts suites (111 tests) all pass. Nothing here blocks merge. A couple of small things worth a look:
The sql description is missing function_edge_logs. The source list guides the model on what to filter by, and right now it lists function_logs (edge-function runtime console output) but not function_edge_logs (the invocation/request logs the get_logs edge-function preset queries, see logs.ts:37). Since that description is effectively the contract the model writes SQL against, a model following it can't reach invocation logs and will filter on the wrong source. Everything else in the list looks right.
query_logs has no execution test yet. The test change adds it to the tool-listing assertion, which is exactly right for registration, but the new timestamp-defaulting / passthrough / wrapping logic in execute is the bug-prone part and it's currently untested (whereas get_logs has three). server.test.ts:1567 is a ready-made template and the existing /endpoints/logs mock already covers it, so no new mock needed. Happy to leave this as a fast-follow if you'd rather not expand scope here.
Tiny nit: sql: z.string() could take .min(1) to match executeSqlOptionsSchema.query, so an empty query gives a clear validation message instead of an opaque backend error. Very much a nice-to-have.
None of this is load-bearing for shipping. The function_edge_logs line is the one I'd genuinely want fixed before merge; the rest are optional.
|
Thanks for the thorough review! Addressed all three in f37cfc5:
🤖 Addressed by Claude Code |
barryroodt
left a comment
There was a problem hiding this comment.
Thanks for the quick turnaround @jordienr
function_edge_logsadded to the source list.min(1)on the schema- behavioral tests for
query_logs
LGTM
@mattrossman, @Rodriguespn - since I'm still the new guy, perhaps a quick scan and thumbs-up from either of you would be advisable 😁
commit: |
|
on further thought, let's leave both tools and potentially deprecate and remove getLogs in favour of queryLogs. |
|
Addressed the deprecation + platform-availability guidance in a4d7228:
🤖 Addressed by Claude Code |
|
Following up with fresh numbers against the current head (ecc456d) Rerun of our eval A/B against the new head: passed all 3 eval checks on the first attempt, and the a4d7228 deprecation note fully flipped tool selection, 8 One finding: the 24 hour default still silently widens narrow questions. The eval asks about the last 15 minutes; all 8 Suggestion: same mechanism as a4d7228 - make the line directive, something like "when the user asks about a specific time range, always pass iso_timestamp_start/iso_timestamp_end to match it". Unrelated side effect: this exercise surfaced drift in our own evals fixture, fixed in supabase/evals#99. |
|
Thanks for the fresh eval run — great to see the deprecation note flip selection cleanly, and good catch on the time-range default. Fixed in ea6d163: the timestamp guidance is now a directive rather than a permissive mention, using the same mechanism as the deprecation line. Both
Applied to 🤖 Addressed by Claude Code |
|
Nice! Reran the A/B against ea6d163 and the directive line does exactly what we hoped: the model now passes an explicit 15 minute window matching the question on every analytical |
…s split this committ assume we will have merged or should be merged to gether with cahnges on skills and MCP - supabase/agent-skills#112 - supabase/mcp#333 query_logs (hosted only, ClickHouse SQL) is now the preferred tool for production projects; get_logs remains the only option for local and self-hosted. Updates the MCP tools table, log-querying section, and skill step 3 in ai-agents.mdx, and the tip admonition in debugging.mdx. logs.mdx already had this right at line 321.
…s split this committ assume we will have merged or should be merged to gether with cahnges on skills and MCP - supabase/agent-skills#112 - supabase/mcp#333 query_logs (hosted only, ClickHouse SQL) is now the preferred tool for production projects; get_logs remains the only option for local and self-hosted. Updates the MCP tools table, log-querying section, and skill step 3 in ai-agents.mdx, and the tip admonition in debugging.mdx. logs.mdx already had this right at line 321.
…s split this committ assume we will have merged or should be merged to gether with cahnges on skills and MCP - supabase/agent-skills#112 - supabase/mcp#333 query_logs (hosted only, ClickHouse SQL) is now the preferred tool for production projects; get_logs remains the only option for local and self-hosted. Updates the MCP tools table, log-querying section, and skill step 3 in ai-agents.mdx, and the tip admonition in debugging.mdx. logs.mdx already had this right at line 321.
Adds a query_logs debugging tool that runs a custom read-only ClickHouse SQL query against a project's unified logs stream, for cases where the get_logs service presets are too coarse. Reuses the existing analytics logs endpoint and validates that queries are SELECT/WITH only.
- add function_edge_logs to the sql source-hint list so models can reach edge function invocation logs - require a non-empty sql query (.min(1)), matching execute_sql - add execution tests for query_logs: sql passthrough + timestamp defaulting, custom window forwarding, and empty-query rejection
The description promises iso_timestamp_start defaults to 24h before the end, but the handler always computed start from now(), so supplying only iso_timestamp_end produced an inverted/empty window. Derive the end first (supplied or now), then default start to end - 24h, shared by get_logs and query_logs.
resolveLogWindow now rejects a malformed iso_timestamp_start/end with a clear error instead of throwing a raw "Invalid time value", and rejects a start at or after the end. Also rebases onto main to pick up the regenerated management API types.
c1bdc7f to
09e521e
Compare
Both tools currently ship an identical description to every client regardless of platform (hosted vs local/self-hosted), so labeling get_logs "Deprecated" risked a client universally hiding or deprioritizing it, which would break self-hosted users since get_logs is their only working logs tool. Reframe as environment-scoped preference (prefer query_logs on hosted, use get_logs on local/self-hosted) instead of an unqualified deprecation.
…registration DebuggingOperations.queryLogs is used by external SupabasePlatform implementers (CLI, studio, mcp.supabase.com controller) outside this repo. Making it required would break them on upgrade: a stale implementer still passes the existing `if (debugging)` group check, so query_logs gets listed in tools/list and then crashes with "debugging.queryLogs is not a function" at call time. Make queryLogs optional and only register the query_logs tool when the platform actually implements it, so an implementer without ClickHouse support (self-hosted/CLI today) simply doesn't get the tool listed instead of erroring. This makes the DebuggingOperations change purely additive.
resolveLogWindow now:
- enforces the 24h API cap client-side with a clear error, instead of
relying on an unvalidated description promise
- normalizes accepted timestamps to canonical UTC ISO strings before
forwarding them, instead of passing the original strings through verbatim
- is exported and unit-tested directly (default anchoring, offset
normalization, malformed/inverted/oversized-window rejection), covering
get_logs and query_logs' shared behavior in one place
Also enforces ISO 8601 with an explicit UTC "Z" suffix or offset at the
schema level via z.iso.datetime({ offset: true }), so offset-less
timestamps (ambiguous local-time interpretation) are rejected before
reaching resolveLogWindow, and the constraint shows up in the tool's JSON
schema.
|
Since this adds a tool and rewords the
Also linking the eval evidence here: the pkg.pr.new preview build of this branch passed the full supabase/evals regression suite (8/8, identical to the published-version baseline - CI run) and locally against the three debugging evals that exercise the analytics logs endpoint (evals#79, with transcripts showing agents successfully using Conditions of the run: model Thanks for taking care of this @jordienr! |
- add an equal-timestamps case to the start-at-or-after-end rejection test - assert on the actual rejection message (Invalid ISO datetime, must be before, min-length) instead of a bare rejects.toThrow() - assert the exact default window (end near now, start = end - 24h) instead of just checking the params are truthy
Rodriguespn
left a comment
There was a problem hiding this comment.
LGTM, thanks for taking another look @jordienr. Could you please address the nit comments I left, whether implementing them or closing them. Up to you 🙁
Important
One last thing before merging, this is also a breaking change as we changed get_logs description and inputSchema.
Can you change the title of this PR to include feat!: and also make sure that the merged commit that lands on main starts with feat!: please?
Check CONTRIBUTING's expand/contract rule for more details.
Rodriguespn
left a comment
There was a problem hiding this comment.
Please check this slack message and the previous ones in that thread for more context on the step back
…ts/logs) (#99) ## What Teaches platform-lite the ClickHouse logs endpoint that current mcp actually calls: `GET /v1/projects/{ref}/analytics/endpoints/logs`, taking ClickHouse-dialect SQL over the unified `logs` stream. ## Why Since supabase/mcp#326, `get_logs` (and the proposed `query_logs` in supabase/mcp#333) query `/analytics/endpoints/logs` with ClickHouse SQL. platform-lite only served the legacy BigQuery-era `logs.all`, so **any logs eval against a locally built mcp 404s at the fixture**. The gap is masked today because evals pin a published `MCP_SERVER_VERSION`; it bites the moment anyone points the harness at an mcp checkout (which is how we validated mcp#333). ## How - **Unified `logs` VIEW** over the existing seeded tables: a `source` discriminator plus a jsonb `log_attributes` map built from the flat columns, with seeded `metadata` as fallback. ClickHouse-shaped SQL runs against it with minimal translation. - **Minimal, observed-only dialect translation** (`compileClickHouseLogsSql`): `log_attributes['k']` to jsonb access (numeric cast for status/exec-time keys so `>= 500` comparisons work) and `countIf(...)` to `count(*) FILTER (WHERE ...)`. A small shim family (`toInt32OrZero`/`toInt64OrZero`/`toUInt32OrZero`/`toString`) covers casts models genuinely emitted during live runs. Anything else surfaces the raw SQL error to the model, which is deliberate: the supported surface is documented and only grows from observed model output. - **Read-only is enforced by a postgres read-only transaction**, not regex: mutating SQL, including data-modifying CTEs (`WITH x AS (DELETE ...) SELECT`), is rejected before it can touch shared fixture state. - **`iso_timestamp_start`/`end` are accepted but ignored**, matching the legacy route: scenario seeds carry fixed dates while mcp defaults windows from the current clock, so a faithful filter would empty every scenario. Documented in-code as a known limitation; window-correctness needs relative-time seeding and a discriminating eval (follow-up). - Contract tests use **verbatim SQL captured from live claude-sonnet-5 runs** (the mcp `edge-function` preset, a `countIf` aggregation, and the exact `toInt32OrZero(toString(...))` query the model emitted), plus the runtime source and the legacy route untouched. ## Verification - `pnpm typecheck` clean, `pnpm vitest run src/management-api/debugging.test.ts` 8/8: five translator/view contract tests plus three route-level tests at the HTTP boundary (normal ClickHouse query returns the `{result}` shape; `WITH x AS (DELETE ... RETURNING *) SELECT` is rejected by the read-only transaction with fixture rows asserted unchanged; plain non-SELECT hits the 400 prefix gate). - Live: `investigate-logs-001-top-error-function` passes against a locally built mcp `main` (3/3 checks) where it previously 404'd, and against an mcp checkout of supabase/mcp#333 the model's first genuine ClickHouse aggregation succeeds end to end. Found while running an A/B validation of supabase/mcp#333 through the eval workspace; the run details are in that PR's thread.
Per team alignment: on platforms that support ClickHouse-backed querying (hosted/production), query_logs is the tool that shows up in tools/list and get_logs is hidden from discovery (but remains callable via tools/call for compatibility). On platforms without it (CLI/self-hosted), the reverse holds: get_logs is listed and query_logs is not registered at all. Also strips environment-specific wording (hosted, production, self-hosted, CLI) from both tool descriptions, since the description is a frozen field for some clients and shouldn't encode environment context the model can't verify; the routing is now handled structurally via registration/hiding instead of prose.
There was a problem hiding this comment.
Ran the CI locally and it's green. In general LGTM, thanks for the patience @jordienr. I'm approving this PR to unblock you, but wanted to ask if we can consider implementing the logsDialect + dynamic description approach that Matt suggested.
Seems like a low-hanging fruit to roll out query_logs across all envs without having to juggle multiple conditions depending on the environment. We're not suggesting implementing ClickHouse for logs on self-hosted/CLI for this — self-hosted/CLI would keep using its existing BigQuery-dialect logs endpoint (Logflare logs.all). The idea is just to hide get_logs on all envs and change query_logs's description/behavior depending on the env's dialect, using the strategy Matt described.
Essentially, what I'm suggesting is the following:
1. Add a logsDialect to DebuggingOperations so each platform can declare which SQL its logs endpoint speaks — platform/types.ts#L220-L223:
export type DebuggingOperations = {
logsDialect?: 'clickhouse' | 'bigquery'; // <- new
getLogs(projectId: string, options: GetLogsOptions): Promise<unknown>;
queryLogs?(projectId: string, options: QueryLogsOptions): Promise<unknown>;
...
};2. Each platform hardcodes its dialect and implements queryLogs. Hosted/platform-API already implements queryLogs (ClickHouse) — just add the dialect at platform/api-platform.ts#L255:
const debugging: DebuggingOperations = {
logsDialect: 'clickhouse', // <- new
...
};Self-hosted/CLI sets logsDialect: 'bigquery' and implements queryLogs by passing the model's SQL straight through to the same BigQuery-backed analytics endpoint getLogs already uses (the endpoint takes an arbitrary sql param), in supabase/apps/studio/lib/api/self-hosted/mcp.ts getDebuggingOperations:
return {
logsDialect: 'bigquery', // <- new
async getLogs(projectRef, options) { /* unchanged: getLogQuery(service) */ },
async queryLogs(projectRef, options) { // <- new, mirrors getLogs
const { data, error } = await retrieveAnalyticsData({
name: 'logs.all',
projectRef,
params: {
sql: options.sql, // pass the model's SQL instead of a preset
iso_timestamp_start: options.iso_timestamp_start,
iso_timestamp_end: options.iso_timestamp_end,
},
})
if (error) throw error
return data
},
...
}3. query_logs's description switches on the dialect — no string juggling, just pick the whole description. When the dialect is bigquery, reuse the existing get_logs description; when it's clickhouse, use the one this PR already writes for query_logs. In getDebuggingTools, at the query_logs registration:
query_logs: injectableTool({
...debuggingToolDefs.query_logs,
description:
debugging.logsDialect === 'bigquery'
? debuggingToolDefs.get_logs.description
: debuggingToolDefs.query_logs.description,
inject: { project_id },
execute: async (...) => { ... },
}),The same whole-string switch applies to any other dialect-gated copy (e.g. the sql param hint, which today hardcodes the ClickHouse logs table / log_attributes[...] schema).
4. The gating you already wrote does the rest. hidden: Boolean(queryLogs) stays exactly as-is — once every platform implements queryLogs, get_logs is hidden everywhere and query_logs is the single listed logs tool on all envs, each with a dialect-appropriate description. No new environment conditionals beyond the one logsDialect field.
Not blocking on this, happy to ship #333 as-is to keep the ball rolling. Just flagging it now since it looks like a small, self-contained change that removes the hosted-vs-self-hosted split entirely. Wdyt?
Adds an optional `logsDialect` (`'clickhouse' | 'bigquery'`) to `DebuggingOperations` so each platform declares which SQL its logs endpoint speaks. `query_logs` then picks its whole dialect-appropriate description and `sql` param hint from a single lookup table, instead of hardcoding ClickHouse copy — no environment-conditional prose the model has to reason about. Defaults to `'clickhouse'` when unset, so existing platform implementers are unaffected. Implements [Matt's `logsDialect` suggestion](https://supabase.slack.com/archives/C08N7894QTG/p1784660787397029?thread_ts=1784624411.491679&cid=C08N7894QTG) raised in the #333 review. **Why**: it lets `query_logs` roll out across every environment without env-specific branching. Hosted stays ClickHouse; self-hosted/CLI can keep its BigQuery-backed logs endpoint and declare `logsDialect: 'bigquery'`. This unblocks shipping the MCP server to self-hosted (studio) regardless of whether it's on BigQuery or ClickHouse, rather than holding the studio image back until self-hosted moves to ClickHouse. > [!NOTE] > The comment sketched "reuse the `get_logs` description for `bigquery`" — I instead give `query_logs` a proper BigQuery-dialect description, since `get_logs` describes a `service` param while `query_logs` takes `sql`. The BigQuery `sql` hint mirrors the canonical Logflare self-hosted schema (`cross join unnest(metadata)`); worth confirming when studio wires up the platform. Refs AI-1046 --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
🤖 I have created a release *beep* *boop* --- <details><summary>mcp-utils: 0.6.0</summary> ## [0.6.0](mcp-utils-v0.5.1...mcp-utils-v0.6.0) (2026-08-10) ### Features * hide tools from tools/list ([#334](#334)) ([d80471a](d80471a)) </details> <details><summary>mcp-server-supabase: 0.10.0</summary> ## [0.10.0](mcp-server-supabase-v0.9.0...mcp-server-supabase-v0.10.0) (2026-08-10) ### ⚠ BREAKING CHANGES * add query_logs tool for custom log queries ([#333](#333)) ### Features * add --content-api-url flag and SUPABASE_CONTENT_API_URL env var ([#343](#343)) ([6fcaaa3](6fcaaa3)) * add query_logs tool for custom log queries ([#333](#333)) ([798806b](798806b)) * hide tools from tools/list ([#334](#334)) ([d80471a](d80471a)) ### Bug Fixes * hide read-only mode ([#349](#349)) ([5cda067](5cda067)) * **pg-meta:** pair composite FK columns positionally to avoid cartesi… ([#317](#317)) ([10af00b](10af00b)) * select query_logs dialect via logsDialect ([#357](#357)) ([80ff453](80ff453)) </details> --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: supabase-releaser[bot] <223506987+supabase-releaser[bot]@users.noreply.github.com>
What
Adds a new
query_logstool to thedebuggingfeature group. It runs a custom ClickHouse SQL query against a project's unified logs stream, for cases where theget_logsservice presets are too coarse (filtering, aggregating, or joining across log fields).Tracks AI-701. Builds on the ClickHouse logs endpoint work in O11Y-1813 and the
get_logsClickHouse migration (#326).Breaking change
This ships as
feat!:per team alignment (see PR discussion): on platforms that implement ClickHouse-backed querying (hosted/production),query_logsis the tool discoverable viatools/list, andget_logsis hidden from discovery — though it remains callable viatools/callfor any client still holding an older tool list. On platforms without ClickHouse support (CLI/self-hosted), the reverse holds:get_logsstays listed andquery_logsis not registered at all.DebuggingOperations.queryLogsis optional on theSupabasePlatformtype (published via the./platformsubpath export). Implementers outside this repo (CLI, studio, the hosted controller) are unaffected if they don't implement it — they simply keepget_logsas their only logs tool.How it addresses the observability team's concerns
execute_sql), so the result is wrapped inwrapWithUntrustedDataBoundary, the same best-effort guardrailexecute_sqluses. Not foolproof, but consistent with the existing arbitrary-query tool.resolveLogWindow) and the analytics endpoint cap the requested range at 24h. No unbounded queries.GET /v1/projects/{ref}/analytics/endpoints/logs(ClickHouse / logs.all.otel) and takes raw ClickHouse-dialect SQL. Does not touch the deprecated BigQuery-backedlogs.all.GETwithsqlas a query param. No new POST handler.query_logsis only registered when the platform implementsqueryLogs; CLI/self-hosted platforms don't, so they keepget_logsas their only logs tool and never seequery_logsat all (see Breaking change above).Details
query_logsparams:project_id,sql, optionaliso_timestamp_start/iso_timestamp_end.Zsuffix or offset (z.iso.datetime({ offset: true })), so the constraint is visible in the tool's JSON schema and offset-less (ambiguous local-time) timestamps are rejected up front.resolveLogWindow(shared byget_logsandquery_logs) anchors the default start to 24h before the resolved end, normalizes accepted timestamps to canonical UTC ISO strings, and rejects an inverted/equal window or one exceeding 24h — unit-tested directly indebugging-tools.test.ts.Verification
tsc --noEmitclean,biome ciclean.logs.test.ts,server.test.ts,debugging-tools.test.ts). The 5 failing|e2e|checks need liveSUPABASE_ACCESS_TOKEN/ANTHROPIC_API_KEY, which this fork PR doesn't receive — confirmed passing on an identical-commit mirror pushed directly to the repo (feat: add query_logs tool for custom log queries (CI mirror of #333) #341).query_logsend-to-end against a real staging project (ClickHouse query executed, results returned, untrusted-data wrapping applied).@supabase/mcp-server-supabaseversion) verifies the agent successfully usesquery_logson a hosted-shaped fixture where it's the only logs tool listed.Post-merge follow-ups
Per Rodriguespn — this adds a tool and changes
get_logs's discoverability, so per CONTRIBUTING's breaking-change guidance:@supabase/mcp-server-supabaseversion (automatic via release-please once merged asfeat!:)MCP_SERVER_VERSIONinsupabase/evalsand un-draft evals#100platformto addquery_logsto the remote MCP server@supabase/mcp-server-supabaseversion pin insidestudioget_logsdescription (frozen fields)Note
Per CONTRIBUTING, feature PRs should track an accepted issue — this tracks AI-701.