Unified host metrics (RDS, Supabase, ClickHouse)
What
RDS, Supabase and ClickHouse Managed Postgres host metrics share one vocabulary, one dashboard row and one way to get their labels.
- One vocabulary. A
vmalertservice evaluatesconfig/prometheus/host_rules.ymlagainstsink-prometheusand writes the results back. The rules map Supabasenode_*(the primary only) and ClickHousePostgresServer_*to thehost_*names, units and{cluster, node_name}labels thatrds-host-statswrites. vmalert runs under thehost-metricscompose profile, whichmon targets add/removekeep inCOMPOSE_PROFILESonly while a ClickHouse or Supabase scrape file exists (a stack with no such target, or RDS only, runs no vmalert). It is pinned to the stack's VictoriaMetrics release (v1.140.0), runs read-only with no capabilities, listens on loopback only and has no notifier.docs/host-metrics.mdis the single table of names, units and sources. - One row. Dashboard 1 has one collapsed Host row, the last on the page, with five panels (CPU utilization, Memory, Storage, Disk IOPS /s, Network /s). They read
host_*only, with the cluster and node selectors, and each charts a family every provider writes. A self-managed node sees one closed row. - One way to label.
mon targets add(andmon local-install --db-url, which goes through it) copies the target'scluster/node_nametags into what the provider's collector reads:- ClickHouse: the scrape file
host-metrics/clickhouse-<name>.yml(unchanged); - Supabase: the scrape file
host-metrics/supabase-<name>.ymlfor theinstance-jobsrelay, whilePGAI_SUPABASE_HOST_METRICSis true (environment or.env), removed when it is not. The relay serves one project, so a box has one such job, and it keeps only that project's series; - RDS:
rds-host-statspushes past samples and is not scraped, so it getsRDS_DB_INSTANCE_IDENTIFIER,AWS_REGION,PGAI_CLUSTERandPGAI_NODE_NAMEin.env, parsed from the instance endpoint, and is recreated if it is running. A cluster, reader or proxy endpoint names no instance and sets nothing.
- ClickHouse: the scrape file
- Only what a panel reads is collected.
rds-host-statsno longer requests disk latency, queue depth, burst and EBS balance, replica lag or Performance Insights DB load: GetMetricData bills per metric, so a poll drops from 13 to 7 metrics (Aurora 14 to 10), about $3.02 instead of $5.60 per instance and month. The three Supabase rules for the same families are gone too.
Merge path
This MR is the single mergeable one for the host metrics work. It is main plus !434 (closed) → !436 (closed) → !440 (closed) (merged in that order and kept current) plus the contents of !435 (closed) and !437 (closed), with their conflicts against main and each other resolved here. It merges cleanly into main (e61a8f3).
- Merge !434 (closed), then !436 (closed), then !440 (closed) (stacked, each retargets to
mainin turn), then this MR. Merging only this MR brings all of them in. - !435 (closed) and !437 (closed) each conflict with
mainand with each other and are fully contained here: superseded by this MR, proposed to close. - !433 now targets this branch and merges on top of it.
- The
mon updatePGAI_TAGchange moved from !440 (closed) to its own MR, !444. It is not in this branch.
Round 3 (review of 2026-09-29, round 2)
- Labels: the 80-line awk YAML parser in
generate-pgwatch-sources.sh, the staticsupabase-host-metricsjob with itsfile_sdtarget and%{PGAI_SUPABASE_HOST_METRICS}relabel toggle, and the manual RDS.envstep are replaced by themon targets addpath above.sink-prometheusno longer needsPGAI_SUPABASE_HOST_METRICS. The relay'sHandler(enabled bool), only ever called withtrue, lost its argument. - Cuts from !435 (closed) (six unused series and PI, with the
@aws-sdk/client-pidependency) and from this MR (three Supabase rules and their test cases). The recorded AWS replies are trimmed, not re-recorded. - The Docker scrape test that CI skipped is replaced by
cli/test/supabase-scrape.integration.test.ts, which runs the real VictoriaMetrics v1.140.0 binary in CI (job renamedcli:host-metrics-scrape:tests): it loads the scrape file the CLI writes, scrapes a relay stand-in, keeps only thenode_*families of the target's project, and labels them with the target'scluster/node_name. - Scrape-file helpers shared by ClickHouse and Supabase moved to
cli/lib/host-metrics.ts. - From an independent security review of this round:
targets removeno longer fails on a target name likeprod.db; a relay credential for another project cannot label its series with this target; a runningrds-host-statsis recreated with the values just written, passed explicitly because an exported variable would win over.env; a failed recreate on remove still applies the removal. Declined: migrating the static Supabase job on existing installs (it was never released), and themon restarthint aftermon update(that ismain's text; !444 changes it). - Own delta against
main: code +1572/−137 (was +1662/−100), tests +6984/−1 (was +8357/−1, mostly the trimmed replays).
Round 5 (review of 2026-09-30, round 4)
Commits 351cedd, 16dc9a0, f3d8e9b. readEnvValue drops an inline comment as compose does and syncVmalert reuses it; with the relay on, a Supabase target name that cannot be a file name is refused before the target is saved ("choose another name"); targets add says when the instance-jobs profile is off; targets remove reads the RDS host as add does; maskConnectionString hides a password with / or @ and sslpassword=. Rule tests for a missing, flat or replica-only clock; the scrape test feeds node_time_seconds; the scrape CI job fails without the binary. Docs and .env.example name the profile and the 7-metric poll.
CLI at f3d8e9b, docker replaced by the logging stand-in from host-metrics-providers.test.ts (no stack, no provider; commands as shown):
Round 6 (review of 2026-09-30, round 5)
Commit ba74624. maskConnectionString: the password runs to the last @, so digits followed by # or ? are hidden, and a host:port/...@... URL with no userinfo is printed as it is (a password made of digits and then / reads the same way and is the residual). An exported empty PGAI_SUPABASE_HOST_METRICS overrides .env, as it does for compose. readEnvValue takes single quotes and an escaped ". SUPABASE_TARGET_NAME_RE. Tests: seven .env shapes of the flag, three of COMPOSE_PROFILES, exported "" against .env true, targets remove for a conn_str new URL rejects (exit 0, the RDS keys stay: add never wrote them), the two mask shapes.
Round 7 (review of 2026-09-30, round 6)
Commit 12afd5f. maskConnectionString hides query passwords first, then a password to the last @ a / follows (@host/db), else to the first @ with no ?/# before it: a /-first password is hidden and ?application_name=me@corp is left alone (residual: digits then / with no /db path). .env inline comments start at # only, as compose reads them; readComposeProfiles shares the parser, so mon health agrees with targets add. Tests: five more .env flag shapes, COMPOSE_PROFILES rows with a negative, the mask shapes.
CLI at 12afd5f (working tree of the commit), docker stand-in (inspect exits 1, all else 0; no stack, no provider):
Round 8 (review of 2026-09-30, round 7)
Commit bd4bdfb. maskConnectionString finds the password's end procedurally on a copy with query values blanked. Without a /db path a password with ? or # now runs to the last @ (hidden, not printed): #S3cret, ?S3cret, Ab/S3cret#x, a?b@S3cret, S3cret?password=. A [ first is an IPv6 host and digits then /, ,, ? or # are a port, printed as given (the residual, in the code comment). The query mask runs after the userinfo mask, so ?password= inside a password keeps the host; a query value runs to &, so a # after it is hidden; pwd counts; the key is tested in a callback, so a 100 KB key with no = takes 1 ms (was 1.5 s). readRaw says why it is not parseEnvValue (the value is written back as it was). Tests: 11 rows red on 12afd5f, green at head; COMPOSE_PROFILES= # c pinned as # c, which is how compose 5.0.2 reads it (docker compose config).
CLI at bd4bdfb (the reviewer's round-7 shapes and 100 KB timings; no stack, no provider):
CLI at ba74624, docker replaced by a stand-in that exits 0 (no stack, no provider):
Tests (red, then green)
cli/test/host-metrics-providers.test.ts:mon targets addfor Supabase writes the goldensupabase-scrape.golden.yml, a second target takes the job over, the flag is read from.env, flagfalseor empty removes it,targets removedrops it; for RDS,.envgets the instance, region and labels and loses them on remove, cluster/reader/proxy endpoints set nothing, a runningrds-host-statsis recreated with the new values even when stale ones are exported, and an unknown compose state is reported.cli/test/supabase-scrape.integration.test.tsandclickhouse-scrape.integration.test.ts: real VictoriaMetrics binary.tests/host_rules/host_rules.test.yml(vmalert-tool unittest, CI jobquality:host-rules): SUCCESS.tests/grafana_dashboards: 405 pass (includingtest_host_row.py: one collapsed Host row, per-panel families, every panel has data from every provider, the doc table equals what the providers write).rds-host-stats: 39 pass;bunx tsc --noEmitclean.instance-jobs:go test ./...andgo vet -tags supabase_livepass.docker compose configvalidates with therdsandinstance-jobsprofiles.- Full CLI suite at
f3d8e9b(2026-09-30, fix box): 2173 pass, 16 skip, 9 fail. 7 aremon local-installcases ininit.test.tsthat fail identically at41a8395on that box, which runs another stack under the fixed container names; the other 2 are 5 s timeouts that pass when their files run alone. At41a8395on the author's machine: 2124 pass, 66 skip, 1 fail (the macOS-onlyCHECKUP_ALLOW_HTTPcase). - Red commits this round:
822a34a,1b8abb9,5b5b3d1,117f245(each followed by its fix).
Real runs
- ClickHouse Managed Postgres (r6gd.medium, PostgreSQL 17.11), 2026-09-30: the cloud suite in
clickhouse.cloud.test.tspassed from this branch: provider detected, scope printed,prepare-db --verify, and the service's/prometheusendpoint served every documented series. The service was created and deleted by the test (list empty afterwards). table_describeas SECURITY INVOKER and theexplain_genericremoval were checked on another real ClickHouse service the same day (see !434 (closed) and !433); it returns 404 after deletion.- Local stack at
41a8395(2026-09-30, images of pipeline 2896968060, fresh compose project) with one real ClickHouse Managed Postgres service (PG 17, test org, deleted afterwards):mon local-install -ycreates no vmalert;mon targets add <ClickHouse URL> r4-chwriteshost-metrics/clickhouse-r4-ch.yml, putshost-metricsinCOMPOSE_PROFILESand starts vmalert with groupshost-clickhouse,host-supabase; within ~4 minutesup{job="clickhouse-r4-ch"} = 1and the 10host_*series of thehost-clickhousegroup exist with{cluster="default", node_name="r4-ch"};mon targets remove r4-chstops and removes vmalert and the profile. Not a render of the Host row: see "What we did not check". - Local stack acceptance from round 2 (stub providers, Grafana 12.3.2) predates the label change; the label path is now covered by the real VictoriaMetrics tests above.
What we did not check
- Real RDS or Supabase host metrics end to end from this branch (the RDS poller change is a removal of queries; the Supabase job was checked against the real VictoriaMetrics binary with the recorded exposition).
- The ansible change (postgres_ai-ansible !49 (closed)) against the new Supabase job: provisioning runs
mon local-install --db-urlwithPGAI_SUPABASE_HOST_METRICSin the environment, which writes the job; turning the flag off takes a re-run ofmon local-installormon targets add. - The Helm chart: it shares the dashboard but has no vmalert or collectors, so its Host row is empty.
mon healthdoes not report vmalert. - A render of the Host row at the current head for Supabase, ClickHouse or RDS (the round-4 blocker): it needs a real project/service/instance per provider, and the round-5 to round-8 fix runs had no AWS, Supabase or ClickHouse credentials. The round-5 to round-8 shots above are CLI output with a docker stand-in, not a stack.
- Tags edited by hand in
instances.ymlreach the host metrics labels only aftermon targets addis run again for that target.
Part of postgres-ai/internal#354.



