Skip to content

Latest commit

 

History

History
30 lines (19 loc) · 5.99 KB

postgres-logs.md

File metadata and controls

30 lines (19 loc) · 5.99 KB

Managed Postgres logs contract

Captured: 2026-07-15 · Implemented: w3/m28 · Severity/filter follow-up: w8/m48, 2026-10-02

Source evidence

Render's current List logs API uses one repeatable resource filter for every log-producing resource and explicitly includes Postgres alongside servers, cron jobs, jobs, Key Value, and workflows. Its documented controls include startTime, endTime, direction, resource, instance, level, text, and limit. The 2026-10-02 API-reference recheck confirms that severity filters apply to this shared resource query. Render's logging guide shows a default recent window, time-range selection, search, and per-instance filtering. The managed Postgres guide states that database and slow-query output is available in logs; queries longer than two seconds are logged by default.

The 2026-07-15 live dashboard walk captured a dedicated Postgres Logs tab. The baseline screenshots are render-walk-postgres-logs.png and bex-walk-postgres.png in the walk capture set; at that point bex had no database-log source or tab. After implementation, a fresh Chrome capture against the deterministic local bex fixture is .playwright-mcp/bex-walk-postgres-logs-current.png: it shows the directly linked active Logs tab, Postgres-attributed lines, Last hour, All instances, and search controls. Backend adapter tests and dashboard component/route tests cover the nonvisual behavior; the shared walk cluster was not an implementation dependency.

bex contract

  • REST GET /v1/logs?resource=dpg-…, GraphQL logs(resource: "dpg-…"), and MCP list_logs(resource: ["dpg-…"]) all call the same database-scoped core query. The dedicated Postgres REST, GraphQL, and MCP compatibility adapters remain available and delegate typed ids to that same production core.
  • Supported Postgres filters are RFC3339 start/end time, direction, text search, instance, severity level, and limit. Level values use the same normalized Render aliases and OR/* wildcard semantics as App queries: ERROR/FATAL/PANIC records match error. Service/request concepts (type, host, HTTP method/status/path, build, and pre-deploy) return a named 400 instead of being ignored. Instance discovery retains its authorized-pod fallback; level-value discovery requires the durable label source.
  • GET /v1/logs (and GraphQL logs, MCP list_logs) pages follow the chosen direction exactly like service logs: backward (the default) lists the most recent first, forward the oldest first (w8/m116). The dedicated Postgres adapters keep their unpaged list oldest-first after the direction applies the limit. Each line carries resource=dpg-…, type=postgres, the CNPG pod as instance, and container=postgres when supplied by the source.
  • The dashboard detail route owns its selected tab in the URL: /databases/<dpg-id>?tab=logs. Its controls are the shared range ladder (Last hour by default, w5/030), observed CNPG instance, and debounced text search. Loading, empty, filtered empty, unauthorized, unconfigured-source, and upstream-error states are distinct. The first page holds the newest 100 lines of the range; scrolling the pane to its top (or Load older in the truncation notice) follows nextStartTime/nextEndTime back until hasMore is false, and a capped view says so, the same paging as the service Logs tab (w4/m107, extended to datastores in w4/m136). REST, GraphQL, and MCP return the same envelope for dpg- resources, and one adapter test walks the cursor chain on all three.

Attribution and authorization invariants

  1. AuthorizeDatabase(ctx, can_view_logs, dpg-id) completes before a Loki or Kubernetes pod selector is built. The Database CR's tenant label, not its namespace or display name, selects the authorization object.
  2. The durable selector is exact equality on {namespace="…", database="dpg-…"}. database comes from the already authorized Database CR's immutable metadata name, never a free-form caller selector.
  3. The live fallback selects pods by CNPG's exact cnpg.io/cluster=dpg-… label and reads only the postgres container.
  4. The operator stamps app.bex.co/component=database into tenant Database CNPG pod metadata. Alloy requires that marker before ingesting the stream, so platform/auth/control-plane CNPG clusters are excluded. It derives the Loki database label from cnpg.io/cluster; it never uses the mutable display name.
  5. Tests seed two database pod sets and prove one query cannot mix the other, and prove REST, GraphQL, and MCP reject a foreign-workspace database before the history source is called.

Durability and degraded behavior

With BEX_LOKI_URL, Loki history survives CNPG pod restarts. Without Loki, bex reads the current CNPG pod log buffer: search, range, direction, limit, instance, and parsed CNPG severity still work within the bounded raw tail, but older and restarted-pod history is unavailable. A severity filter runs before the result cap; it does not enlarge the pod read into an unbounded scan. With neither history nor pod-log source the APIs return 503 logs source not configured, and the dashboard explains that the installation has no database-log source. An unreachable configured Loki surfaces its error; bex does not silently switch sources and disguise an outage.

Live subscription (w5/m139). Render's /logs/subscribe takes the same resource as /logs ("the id of a server, cronjob, job, postgres, redis…"), so render logs -r dpg-… --tail streams: bex follows the CNPG pods' postgres container with this page's parsing (instance-manager JSON dropped, records unwrapped with their level) and the same instance/text/level filters, over WebSocket, SSE or NDJSON. The dashboard Logs tab has a Live switch (off by default) that merges the tail into the history beside it. Walked on dev-5 with the pinned CLI: a forced select 1/0 arrived as ERROR: division by zero, with no instance-manager lines.