Skip to content

Pg_exporter

Run pg_exporter, Pigsty’s declarative PostgreSQL metrics exporter, as a managed container — over pgcli instances or any remote PostgreSQL.

pg addon install pgexporter runs pg_exporter — Pigsty’s PostgreSQL metrics exporter — as a managed container from the official docker.io/pgsty/pg_exporter image. Where the node_exporter addon describes the machine, pg_exporter describes a database: it connects to a PostgreSQL server and publishes ~460 pg_* metrics (activity, locks, replication, bloat, WAL, per-database stats…) at /metrics. Like Prometheus, VictoriaMetrics, Grafana and node_exporter it is a top-level addon: it is attached to no instance of its own — instead it points at a target it monitors.

Its reason to exist alongside node_exporter is unified collection: the target can be a pgcli-managed instance or any reachable PostgreSQL server pgcli does not manage. That is the whole point — one exporter shape collects metrics from databases inside and outside pgcli alike.

Three target modes, mutually exclusive — pass exactly one. --pg-name <inst> monitors a pgcli-managed instance; its admin DSN is re-derived from pg.yaml on every start (so a rotated password or changed port never goes stale). For a server pgcli does not manage, the target is given as ONE URL — and which URL you pass decides whether pgcli touches the database:

  • --admin-dsn postgres://… — an admin-privileged URL. pgcli uses it once to create a low-privilege monitor role on the target, then discards it: only its credential-free address (host/port/database) is stored, and host/port/database for the exporter come from it. The exporter thereafter connects as the monitor role.
  • --dsn postgres://… — dial-as-is. The exporter connects with exactly this URL’s own credentials and pgcli runs no SQL on the target. Use it when a suitable role already exists, or when pgcli must not touch the database at all.

--dsn and --admin-dsn never appear together — there’s no second URL to give, because the target’s host/port/database come from whichever one you pass.

The exporter never dials as an admin in the local and --admin-dsn modes: pgcli creates the low-privilege monitor role (a pg_monitor member, CONNECTION LIMIT 5) and hands the container that role’s URL. To create it, pgcli needs admin rights once: for --pg-name the admin credentials are derived automatically; for --admin-dsn you supply an admin URL that runs the single CREATE ROLE/GRANT batch and is never stored in pg.yaml. The generated monitor password is printed once and retrievable via pg addon password pgexporter. In the dial-as-is --dsn mode pgcli creates nothing — it is your job (or a prior --admin-dsn install) that the role already exists.

There is NO authentication and no TLS on the exporter endpoint, and pg_exporter also serves admin endpoints (/reload, /explain, /stat) on the same port — anyone who can reach it can scrape metrics and trigger reloads. Because a Prometheus scraping it is usually on another host, Listen defaults to 0.0.0.0; the only protection is the network boundary. Restrict it to your monitoring network, or --listen 127.0.0.1 for a same-host Prometheus.

Platform: Linux is the tested platform (host networking, so the exporter binds the host port directly and can dial a local instance’s 127.0.0.1 target). On macOS the container runs on the pgcli-net bridge and cannot reach a local instance’s 127.0.0.1 DSN — there, use --dsn pointed at a reachable address (the remote path works; the local path does not).

Parameters

Flag Default Description
--name pgexporter Instance name (map key)
--pg-name — Monitor a pgcli-managed instance by name (admin DSN re-derived each start; mutually exclusive with --dsn and --admin-dsn)
--admin-dsn — Remote, create-role mode: an admin-privileged URL for a server pgcli does not manage. Used once to create the monitor role, then discarded — only its credential-free address (host/port/database) is stored, and never in a form carrying the admin password. The exporter connects as the monitor role. Mutually exclusive with --dsn and --pg-name
--dsn — Remote, dial-as-is mode: the exporter connects with THIS URL’s own credentials and pgcli runs no SQL on the target — for a role that already exists or a database pgcli must not touch. Mutually exclusive with --admin-dsn and --pg-name
--monitor-user pgexporter Name of the low-privilege role pgcli creates (pg_monitor member; plain identifier only). Use a distinct value per instance when several exporters monitor the same target
--image docker.io/pgsty/pg_exporter:1.5.0 Image override
--port auto (base 9630) Host port; auto-allocated from pgexporter_start_port
--listen 0.0.0.0 Bind address — every interface by default (cross-host Prometheus); use 127.0.0.1 for loopback-only
--config-file — Path to a custom pg_exporter collector YAML, mounted read-only at /etc/pg_exporter.yml (the binary’s baked-in default set is used when unset)
--extra-arg — Extra CLI flag passed to the binary (repeatable); appended last so last-wins overrides earlier flags. E.g. --extra-arg --auto-discovery=false
--force false Recreate even when running — to apply changed port / listen / target / extra-args

pg_exporter is stateless — it stores nothing, so there is no data directory. pg addon remove deletes only the container (the monitor role on the target is kept, since it may be shared or hand-managed); reinstalling always starts fresh.

Install

Monitor a pgcli-managed instance (the monitor role is created automatically):

pg addon install pgexporter --pg-name demo

The command derives the local admin DSN, creates the monitor role on demo, pulls the image if needed, starts the container pointed at the monitor URL, and prints the scrape info:

✓ pg_exporter installed: "pgexporter"
  Container:  pgcli-pgexporter-pgexporter
  Image:      docker.io/pgsty/pg_exporter:1.5.0
  Metrics:    http://0.0.0.0:9630/metrics
  Target:     instance demo
  Monitor as: pgexporter (pg_monitor)

  Monitor password: pgx_3f9a…            # printed ONCE
  (printed once — retrieve it later with `pg addon password pgexporter --name pgexporter`)

Monitor a database outside pgcli’s management, letting pgcli create the monitor role — pass an admin URL alone; the target’s host/port/database come from it:

pg addon install pgexporter --name ext \
  --admin-dsn "postgres://admin:adminpw@db.example.com:5432/app"

The --admin-dsn runs the one-time CREATE ROLE/GRANT pg_monitor batch and is then discarded — only the credential-free address postgres://db.example.com:5432/app is kept in pg.yaml. Thereafter the exporter connects only as pgexporter, and pg addon password pgexporter --name ext returns that role’s password.

If the monitor role already exists and you’d rather pgcli not touch the target at all, pass a --dsn with usable credentials instead (mutually exclusive with --admin-dsn — you give one URL, not two):

pg addon install pgexporter --name ext \
  --dsn "postgres://pgexporter:existingpw@db.example.com:5432/app"

Here pgcli runs no SQL: the exporter dials exactly this URL with its own credentials.

(To create the role by hand instead, the minimum is CREATE ROLE pgexporter LOGIN CONNECTION LIMIT 5 PASSWORD '…'; GRANT pg_monitor TO pgexporter; — plus GRANT SELECT (oid) ON pg_subscription on PostgreSQL 10–13.)

Verify the exporter is serving real numbers for the target:

curl -s http://127.0.0.1:9630/metrics | grep -E '^pg_(up|version_info)'
curl -s http://127.0.0.1:9630/up        # 200 == the target is reachable

pg_up 1 means the monitor role connected. Cross-check pg_version_info against pg exec demo -c "SHOW server_version" — and confirm the role landed on the target with pg exec demo -c "\du pgexporter" (or query pg_roles / pg_auth_members for the pg_monitor grant). With the default --auto-discovery, metrics carry a datname label per database on the target.

Repeated installs and one monitor user

The monitor-role SQL is idempotent, so re-running install never damages the target’s role: a plain reinstall on a running, unchanged instance is a no-op (no SQL runs), and a --force reinstall ALTERs the existing role — its OID and grants survive and the pg_monitor membership is never duplicated.

What a --force / config-changed reinstall does do is rotate the generated password (that instance stays fine — the new password is stored and injected together — but it overwrites a password you may have set by hand or reused; pg addon password pgexporter retrieves the current one). Because every such install writes a fresh password onto the role, two exporter instances sharing one target and the default pgexporter user collide: installing the second rotates the shared role’s password, and the first keeps its now-stale one — its open pool works, but the next reconnect fails and it drops to pg_up 0. When you run more than one exporter against the same target, give each its own role:

pg addon install pgexporter --name pe2 --pg-name demo --monitor-user pgexporter2

Wire it into Prometheus

The exporter is only useful once something scrapes it. A pgcli Prometheus instance does, via a one-line --scrape job:

pg addon install prometheus --name p1 --scrape pg=127.0.0.1:9630
# remote Prometheus → use the host's LAN address instead of 127.0.0.1:
pg addon install prometheus --name p1 --scrape pg=<host-ip>:9630

To add this exporter to a pgcli Prometheus that is already running and already scraping other jobs, use --scrape-append — it folds the pg job into the stored scrape list (merging same-named jobs, appending unseen ones) and SIGHUP-reloads, so it never wipes the existing targets:

pg addon install prometheus --name p1 --scrape-append pg=127.0.0.1:9630

(A plain --scrape replaces the whole job set, so re-running install with it would drop any other jobs p1 was scraping.) Then in Prometheus, up{job="pg"} is 1 and count(pg_activity_count) is positive. See the Prometheus addon for the full --scrape / --scrape-append mechanics.

Reuse the health probes

pg_exporter’s /primary and /replica return 200 only when the target is in that role, so they double as HAProxy-style liveness probes for read/write routing — see the HAProxy addon for wiring a pgcli HAProxy to point-check a PostgreSQL. /up (target reachable at all) and /read are the other probes.

Re-running install on a running instance is a no-op without --force (config fields you pass are saved; the container keeps serving). With --force the container is recreated — and the role SQL re-runs (rotating the monitor password) in --pg-name mode, or in --admin-dsn mode only when a fresh --admin-dsn is passed (without one, the stored monitor password is reused, since pgcli deliberately kept no admin rights). A --dsn install never runs SQL at all. Changing port / listen / extra-args needs --force (container recreation) — only the collector config is hot-reloadable (see below).

Custom collectors (--config-file) and reload

pg_exporter ships a full default collector set baked into the binary and plans it at runtime by server version / role / extensions, so no config file is needed to get useful metrics — most installs omit --config-file. When you do want a custom set, point --config-file at a pg_exporter collector YAML; it is mounted read-only at /etc/pg_exporter.yml and selected via PG_EXPORTER_CONFIG. After editing the file on the host, hot-reload it in place with SIGHUP (no container restart):

pg addon install pgexporter --pg-name demo --config-file /srv/collectors.yml
# …edit /srv/collectors.yml, then:
pg addon pgexporter reload --name pgexporter

Reloading an instance started without --config-file is a harmless no-op — pgcli tells you so (the built-in default set has nothing on disk to re-read). URL / port / listen changes are not covered by SIGHUP; recreate with install --force.

Custom flags (--extra-arg)

pg_exporter has a rich flag surface beyond the env-configured URL. Toggle features via --extra-arg (repeatable):

pg addon install pgexporter --pg-name demo \
  --extra-arg --auto-discovery=false \
  --extra-arg --tag=env=prod

Extra args are appended last to the command line, so they override pgcli’s injected flags via last-wins. Common ones: --auto-discovery=false (stop adding a datname per database), --exclude-database (skip databases), --tag (add constant labels), --namespace (prefix every metric). After --force, confirm with curl -s http://127.0.0.1:9630/metrics that a --auto-discovery=false target reports only the postgres database’s labels.

Listening and exposure

Listen defaults to 0.0.0.0: on Linux the container shares the host network namespace, so /metrics is reachable on every interface at the assigned port. This is deliberate — a Prometheus scraping it is usually on another machine. But there is no authentication, and pg_exporter also serves /reload, /explain and /stat on the same port, so anyone who can reach it can read config and trigger reloads, not just scrape. For a same-host Prometheus pass --listen 127.0.0.1 (recreate with --force); otherwise restrict the port at the firewall. (Under the macOS bridge build, pgcli widens the bind to 0.0.0.0 automatically so the published port stays reachable through the podman VM.)

Ports

Each pg_exporter instance owns a single port — the metrics endpoint — allocated from a pool with base pgexporter_start_port (default 9630, pg_exporter’s conventional port), incrementing for each additional instance and skipping ports taken by any managed container or used_ports. The allocation is saved to pg.yaml, so a reinstall never re-hands-out a port another addon holds. 9630 shares no number with node_exporter (9100), Prometheus (9090), VictoriaMetrics (8428), Grafana (3000) or Alertmanager (9093); an explicit --port still wins.

pg addon install pgexporter --name pe2 --monitor-user pgexporter2 --pg-name demo   # second instance: next free port (9631)
pg addon install pgexporter --name pe3 --port 9700 --monitor-user pgexporter3 --pg-name demo  # explicit port takes precedence

(Each extra instance on the same target needs its own --monitor-user — see Repeated installs and one monitor user.)

Configuration

addons:
  pgexporter:
    pgexporter:
      name: pgexporter
      image_tag: docker.io/pgsty/pg_exporter:1.5.0
      host_port: 9630
      listen: 0.0.0.0
      target_instance: demo              # local mode
      monitor_user: pgexporter
      monitor_dsn: postgres://pgexporter:…@127.0.0.1:5432/demo?sslmode=disable   # what the container dials
      monitor_password: "…"              # A SECRET — the generated monitor password
      autostart: false
    ext:
      name: ext
      host_port: 9631
      listen: 0.0.0.0
      admin_dsn: postgres://db.example.com:5432/app   # --admin-dsn mode: credential-FREE address only
      monitor_user: pgexporter
      monitor_dsn: postgres://pgexporter:…@db.example.com:5432/app
      monitor_password: "…"

The three target fields are mutually exclusive — set exactly one: target_instance (a pgcli-managed instance, DSN re-derived every start), admin_dsn (a remote server pgcli created a monitor role on — pgcli keeps only its credential-free address; the admin URL you passed is never persisted), or dsn (a remote server dialed as given, stored verbatim with its own credentials, and no role SQL ever runs). monitor_dsn / monitor_password are what pgcli generates and injects into the container (absent in dsn mode, where the exporter dials the dsn itself). extra_args is a list of flags appended last; config_file is an optional host path mounted read-only.

List

pg addon list
Web add-ons (pgexporter):
  pgexporter (name: pgexporter)
    Status:      running
    Metrics:     http://0.0.0.0:9630/metrics
    Target:      instance demo
    Monitor as:  pgexporter (pg_monitor)
    Extra args:  --auto-discovery=false
    Image:       docker.io/pgsty/pg_exporter:1.5.0
    Container:   pgcli-pgexporter-pgexporter

The Target line is redacted in remote mode (the DSN’s password never appears). The monitor password is not shown — retrieve it with pg addon password pgexporter.

Password

pg addon password pgexporter          # print the stored monitor-role password

This is the password of the pgexporter role the exporter connects with — not a PostgreSQL admin password.

Start and stop

Containers run with podman’s unless-stopped restart policy, so they come back with podman after a host reboot. start/stop manage them explicitly:

pg addon stop pgexporter
pg addon start pgexporter

start re-derives the monitor DSN first (for a --pg-name target, so a rotated instance password or changed port takes effect on recreation).

Auto-start on Boot

pg autostart enable --pgexporter --name pgexporter

This flags the instance in pg.yaml and installs/refreshes a boot service (a systemd --user unit on Linux, a LaunchAgent on macOS) that runs pg start --autostart. A boot-time start only brings the container up with the stored monitor credentials — it does not re-run the role-creation SQL (that happens at install). See Autostart for the full mechanism, status and disabling.

Remove

pg addon remove pgexporter   # deletes the container; there is no data to clean

pg_exporter keeps nothing on disk, so --clean-data has no effect beyond an informational note. The monitor role created on the target is not dropped — it may be shared or hand-managed. To remove it yourself, first revoke its per-database grant (a bare DROP ROLE fails while the pg_subscription column grant exists in any database it touched): REVOKE SELECT (oid) ON pg_catalog.pg_subscription FROM "pgexporter"; in each monitored database, then DROP ROLE "pgexporter";.

Logs

pg logs addon pgexporter            # show recent console output
pg logs addon pgexporter -f         # follow
pg logs addon pgexporter -n 200     # last 200 lines (also -i / --tail)

Troubleshooting

  • pg_up 0 / no pg_* metrics — the monitor role can’t connect. Check /up returns 200, that the target’s pg_hba.conf admits the container’s source address, and that the stored password matches the role (pg addon password pgexporter; if they diverged, reinstall to rotate it). On a --dsn target, confirm the DSN’s host/port is reachable from the exporter host (a 127.0.0.1 DSN means the target is local to the exporter).
  • pg_up went 0 right after installing a second exporter on the same target — both share the default pgexporter role, and the second install rotated its password, staling the first. Recreate the first (install … --force) to re-sync it, or give each instance its own --monitor-user (see Repeated installs and one monitor user).
  • Metrics present but thin/empty — a missing GRANT pg_monitor yields a connected exporter with partial collectors. Reinstall (the role setup is idempotent) and confirm membership: pg exec <inst> -c "SELECT r.rolname, m.rolname FROM pg_auth_members a JOIN pg_roles r ON r.oid=a.member JOIN pg_roles m ON m.oid=a.roleid WHERE r.rolname='pgexporter'".
  • Prometheus shows the target DOWN — the exporter is running (pg addon list), but the scrape address/port don’t match (--scrape pg=<host>:<port>), or a loopback --listen 127.0.0.1 exporter is being scraped from another host.
  • --dsn and --admin-dsn are mutually exclusive — you passed two remote URLs; give exactly one: --admin-dsn to have pgcli create the monitor role (its host/port/database become the target), or --dsn to dial a URL with credentials you already manage.
  • has an admin_dsn target but no stored monitor password — a --force reinstall of an --admin-dsn instance after the secret was lost (never stored by pgcli). Re-pass --admin-dsn postgres://… so pgcli can create the role again.
  • A flag / discovery change didn’t take effect — --extra-arg and target/URL changes apply at container creation; reinstall with --force. Only the collector config (--config-file) reloads via SIGHUP.
  • macOS local mode serves nothing — a bridge container can’t reach the host’s 127.0.0.1 PG; use --dsn at a reachable address on Mac.
  • Port already allocated — free it with pg addon remove first, or pick another with --port N.

Known limitations

  • No authentication, no TLS, and admin endpoints exposed. pg_exporter serves plain-text /metrics plus /reload, /explain, /stat; the only protection for a 0.0.0.0 bind is the network boundary. Front with nginx or restrict by firewall if it must leave a trusted network.
  • The monitor DSN (with its password) is stored in pg.yaml. That is the low-privilege pg_monitor role’s credential, not an admin one; the --admin-dsn used to create it is never persisted. Protect pg.yaml accordingly.
  • The official image is scratch-based — it carries no system CA bundle, so a TLS (sslmode=require) --dsn to an external server may fail certificate verification; supply trust via the image or connect over a trusted network.
  • Stateless — no data dir, no retention; nothing survives beyond the running process. Reinstall always starts fresh, and the target-side monitor role is never auto-dropped.
  • macOS local-instance mode is not usable (bridge container can’t reach host loopback); remote --dsn mode works there. Linux is the tested platform.
  • pgcli does not validate --extra-arg values; a bad flag surfaces only in the container logs on startup.