Skip to content

platform-lite: serve the ClickHouse logs endpoint (/analytics/endpoints/logs) - #99

Open
barryroodt wants to merge 17 commits into
mainfrom
platform-lite-clickhouse-logs
Open

platform-lite: serve the ClickHouse logs endpoint (/analytics/endpoints/logs)#99
barryroodt wants to merge 17 commits into
mainfrom
platform-lite-clickhouse-logs

Conversation

@barryroodt

@barryroodt barryroodt commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

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 feat: add query_logs tool for custom log queries 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.

…ts/logs)

mcp >= the #326 migration queries /v1/projects/{ref}/analytics/endpoints/logs
with ClickHouse-dialect SQL over a unified 'logs' stream; platform-lite only
served the legacy BigQuery-era logs.all, so any logs eval against a current
mcp build 404s at the fixture.

- unified 'logs' VIEW over the seeded tables (source discriminator +
  log_attributes jsonb built from columns, metadata fallback)
- minimal dialect translation: log_attributes['k'] -> jsonb access (numeric
  cast for status/exec-time keys), countIf -> count(*) FILTER
- read-only enforced by a postgres read-only transaction (not regex): mutating
  SQL incl. data-modifying CTEs is rejected before touching fixture state
- iso_timestamp_start/end accepted but ignored (scenario seeds carry fixed
  dates; the legacy route makes the same choice)
- contract test: mcp edge-function preset, countIf aggregation, runtime source
Live A/B of mcp PR#333 showed claude-sonnet-5 emitting genuine ClickHouse
(countIf(toInt32OrZero(log_attributes['status']) >= 400)); the fixture rejected
it and the model adapted with postgres-only SQL that the hosted ClickHouse
endpoint would refuse — greening the eval by fixture-adaptation. Provide
toInt32OrZero/toInt64OrZero/toUInt32OrZero (text + numeric overloads, CH
0-on-garbage semantics) so the fixture accepts the model's natural dialect.
Contract test uses the verbatim model-emitted query.
Second fixture gap from the live PR#333 treatment rerun: the model nests
toString() inside toInt32OrZero(). One anyelement cast function covers it;
verbatim-model-SQL contract test added.
@vercel

vercel Bot commented Jul 21, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
evals Ready Ready Preview, Comment Jul 24, 2026 2:07pm

Request Review

The PR claims data-modifying CTEs are rejected, but the existing tests called
the translator/db directly. Exercise the actual HTTP route: normal ClickHouse
query returns the {result} shape; WITH x AS (DELETE ... RETURNING *) SELECT is
rejected by the read-only transaction with fixture rows provably unchanged;
plain non-SELECT hits the 400 prefix gate.
Comment thread packages/platform-lite/src/project/log-seeding.ts
Comment thread packages/platform-lite/src/management-api/debugging.ts Outdated
…face

Review question on #99: why are these SQL statements defined here instead of
imported? Answer, now in-code: the logs relation shape is the hosted
platform's Logflare/ClickHouse contract (supabase/platform#35096,
platform-internal, no npm artifact); source names track mcp's
logsServiceSchema under the pinned MCP_SERVER_VERSION; the OrZero/toString
family reimplements ClickHouse builtins; and the verbatim test SQL is frozen
observed output on purpose (importing live definitions would make the
contract tests tautological).
…t tripwire

Review follow-up on #99: the earlier provenance note (and my reply) claimed
nothing was importable - wrong on one count. The pinned mcp package DOES
export logsServiceSchema from its /platform entrypoint; what it enumerates is
the service-preset namespace, not the unified-stream source names the view
discriminates on (those exist only in preset SQL strings and the query_logs
description). Comment corrected, and the importable artifact is now used for
what it's genuinely good for: a drift tripwire that fails loudly when a
version bump changes the service enum, pointing at what to resync.
@barryroodt barryroodt self-assigned this Jul 21, 2026
…ovenance

Second thought on the tripwire: it asserted the service-PRESET enum, which is
a different namespace from the view's source names, so its failure could not
demonstrate view staleness. Worse, it imported the resolved devDependency
(^0.8.1 -> 0.8.2 today) while the harness runs the MCP_SERVER_VERSION pin
(0.8.1), so it guarded a version the fixture never exercises. The verbatim
frozen preset SQL in these tests remains the honest alignment contract. The
provenance comment now records the exported-schema nuance and why importing
it would track the wrong artifact.
Verified against supabase/platform directly: #35096 (getLogs -> logs.all.otel
unified stream, ClickHouse dialect) and #35970 (query_logs passthrough,
timestamps normalized platform-side) are both OPEN, so the contract this
fixture models is what mcp main is written against, not what hosted serves
today. The 35970 e2e spec uses the same source vocabulary as this view
(postgres_logs), which is a good consistency signal.
#35096 backs the getLogs PRESET path (logs.all.otel + CH dialect) that mcp
main emits post-#326; #35970 backs the custom-SQL passthrough that the still
open mcp#333 targets - main does not depend on it. Hosted serves the
/analytics/endpoints/logs route today; it is the per-PR capabilities that are
pending, not the route.
The header bundled query_logs into 'current mcp' while the provenance bullets
below correctly note #333 is still open; both now say: current main emits the
get_logs presets, the #333 branch emits query_logs, and the fixture models
both.
@barryroodt
barryroodt requested review from a team and Rodriguespn July 22, 2026 13:08
…ate test

- 400 prefix-reject body now carries a message key: mcp's assertSuccess
  parses non-2xx bodies as {message}, so the informative read-only text
  was collapsing to the generic 'Failed to fetch logs' fallback (error
  kept for shape consistency with the 200 SQL-error path)
- CTE-reject test pins status 200: the prefix gate's 400 message also
  matches /read-only/i, so unifying the rejection paths would otherwise
  leave the read-only transaction guard silently untested
- move src/management-api/debugging.test.ts -> test/clickhouse-logs.test.ts:
  platform-lite tests live under test/, and the src placement collided on
  basename with the existing test/debugging.test.ts
@barryroodt
barryroodt force-pushed the platform-lite-clickhouse-logs branch from 1970f54 to b759ca2 Compare July 23, 2026 13:14
Proposal 4 (review): drop the implicit numeric cast on
response.status_code/status_code/execution_time_ms map access. Hosted
ClickHouse map values are String, so a bare comparison like
log_attributes['response.status_code'] >= 500 errors there — the fixture
now errors identically instead of silently accepting SQL that would fail
hosted (eval-greens-locally hazard). Models adapt by wrapping in
toInt32OrZero, exactly as the frozen fixtures show; a new test pins the
error friction, and the constructed query_logs-style test now wraps its
comparison like a hosted-correct query must.

Minors: type the read-only transaction result (cast gone); parametrize
the two verbatim PR-333 fixtures with it.each; assert function_id/level
values in the runtime-preset test instead of bare row count; typed
Pick<> partial for the fake store; document the two unmodeled preset
sources (workflow_run_logs, realtime_logs) in the view header.
@barryroodt
barryroodt force-pushed the platform-lite-clickhouse-logs branch from ba53d15 to 9d164ad Compare July 23, 2026 13:30
- openapi.json: advertise /analytics/endpoints/logs — spliced the single
  generated path entry (upstream does advertise it; AnalyticsResponse ref
  already present) instead of taking the full regen's unrelated drift;
  pinned alongside logs.all in openapi.test.ts
- unmodeled sources now error loudly: compileClickHouseLogsSql rejects
  queries naming workflow_run_logs/realtime_logs (no backing table) so a
  branch-action/realtime eval fails visibly instead of reading a silent
  0-row result as 'no logs'; tested at translator and HTTP level
- route test store: real init-free ProjectInstance in a real Map — the
  exact ProjectStore shape, both casts gone
@barryroodt
barryroodt force-pushed the platform-lite-clickhouse-logs branch from 9d164ad to 89b814d Compare July 23, 2026 13:34
Comment thread packages/platform-lite/src/management-api/debugging.ts
@mattrossman

Copy link
Copy Markdown
Collaborator

Surfacing these threads for awareness: (1) (2)

I'm wondering how this approach of translating CH dialect for our PGLite logs backend compares with running actual ClickHouse separate from the PGLite logs backend, using their in-memory chdb and chdb-node bindings? That might get us closer feature parity w/o manual translation for each new syntax the agent tries using, though I'm not sure about the integration lift.

storage_logs was half-modeled: the table exists and the logs VIEW serves a
'storage_logs' source (which mcp's storage preset filters on), but
seedLogRow silently dropped 'storage' seeds — a storage eval would read the
resulting empty result as 'no logs', the exact false-green the
unmodeled-source guard exists to prevent. Add seedStorageLog (base columns;
the preset selects only id/timestamp/event_message) and a verbatim
storage-preset test.

seedLogRow's fall-through was the same bug at the seed layer: any unknown
source (typo or unsupported service) silently seeded nothing. It now throws
at seed time, naming the supported sources; tested.
…o shims

Two more hosted-parity closures:

- Restrict /analytics/endpoints/logs to the 'logs' relation (mattrossman's
  review question). ENFORCEMENT is DB-level: the route transaction runs
  SET LOCAL ROLE logs_reader, granted SELECT only on the logs view, so
  postgres name resolution denies backing-table access under any spelling
  (edge_logs, public.edge_logs, "edge_logs"). The FROM/JOIN regex remains
  as best-effort message shaping pointing the model at the source-filter
  idiom. The legacy logs.all route sets no role and keeps table access for
  its BigQuery-era dialect. The CTE read-only test now uses an INSERT CTE
  (passes prefix gate and regex) so the transaction stays the tested last
  line of defense; qualified/quoted bypass spellings are pinned in tests.
- Drop the numeric *OrZero overloads: ClickHouse's toInt32OrZero family
  takes String only, so toInt32OrZero(42) must error here as it does
  hosted. They existed for the translator's implicit numeric casts, which
  are already gone. Negative parity test added.
@barryroodt
barryroodt force-pushed the platform-lite-clickhouse-logs branch from e546500 to c3dfccf Compare July 23, 2026 13:51
@barryroodt

Copy link
Copy Markdown
Contributor Author

chdb could be a good fit eventually, yeah, thanks for the threads. Today it'd be a second engine (legacy logs.all stays on PGlite) and we'd still hand-model the private logs schema, so it removes the smaller half of the work. I'd keep the current two-rewrite translator while it stays this small, and spike chdb if the shim surface starts growing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants