observability.svc.plus/roles/pgsql/templates/pg-init-template.sql
2026-02-01 20:53:55 +08:00

694 lines
37 KiB
PL/PgSQL
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

----------------------------------------------------------------------
-- File : pg-init-template.sql
-- Desc : init templates for postgres cluster {{ pg_cluster }}
-- Time : {{ '%Y-%m-%d %H:%M' | strftime }}
-- Host : {{ pg_instance }} @ {{ inventory_hostname }}:{{ pg_port }}
-- Path : /pg/tmp/pg-init-template.sql
-- Note : ANSIBLE MANAGED, DO NOT CHANGE!
-- License : Apache-2.0 @ https://pigsty.io/docs/about/license/
-- Copyright : 2018-2026 Ruohang Feng / Vonng (rh@vonng.com)
----------------------------------------------------------------------
--==================================================================--
-- Executions --
--==================================================================--
-- {{ pg_bin_dir}}/psql template1 -AXtwqf /pg/tmp/pg-init-template.sql
-- this sql scripts is responsible for post-init procedure
-- it will
-- * create system users such as replicator, monitor user, admin user
-- * create system default roles
-- * create schema, extensions in template1 & postgres
-- * create monitor views in template1 & postgres
--==================================================================--
-- Default Privileges --
--==================================================================--
{% for priv in pg_default_privileges %}
ALTER DEFAULT PRIVILEGES FOR ROLE {{ pg_dbsu }} {{ priv }};
{% endfor %}
{% for priv in pg_default_privileges %}
ALTER DEFAULT PRIVILEGES FOR ROLE {{ pg_admin_username }} {{ priv }};
{% endfor %}
-- for additional business admin, they can SET ROLE to dbrole_admin
{% for priv in pg_default_privileges %}
ALTER DEFAULT PRIVILEGES FOR ROLE "dbrole_admin" {{ priv }};
{% endfor %}
--==================================================================--
-- Schemas --
--==================================================================--
{% for schema in pg_default_schemas %}
{% if schema is string %}
CREATE SCHEMA IF NOT EXISTS "{{ schema }}";
{% elif schema is mapping and 'name' in schema %}
{% if schema.state is defined and schema.state == 'absent' %}
DROP SCHEMA IF EXISTS "{{ schema.name }}" CASCADE;
{% else %}
CREATE SCHEMA IF NOT EXISTS "{{ schema.name }}"{% if 'owner' in schema and schema.owner is not none and schema.owner != '' %} AUTHORIZATION "{{ schema.owner }}"{% endif %};
{% endif %}
{% endif %}
{% endfor %}
-- revoke public creation
REVOKE CREATE ON SCHEMA public FROM PUBLIC;
--==================================================================--
-- Extensions --
--==================================================================--
{% for extension in pg_default_extensions %}
{% if extension is string %}
CREATE EXTENSION IF NOT EXISTS "{{ extension }}" CASCADE;
{% elif extension is mapping and 'name' in extension %}
{% if extension.state is defined and extension.state == 'absent' %}
DROP EXTENSION IF EXISTS "{{ extension.name }}" CASCADE;
{% else %}
CREATE EXTENSION IF NOT EXISTS "{{ extension.name }}"{% if 'schema' in extension %} WITH SCHEMA "{{ extension.schema }}"{% endif %}{% if 'version' in extension %} VERSION '{{ extension.version }}'{% endif %} CASCADE;
{% endif %}
{% endif %}
{% endfor %}
-- always enable file_fdw and default server fs
CREATE EXTENSION IF NOT EXISTS file_fdw;
CREATE SERVER IF NOT EXISTS fs FOREIGN DATA WRAPPER file_fdw;
--==================================================================--
-- Backup Privileges --
--==================================================================--
-- grant backup privileges to replication user
GRANT USAGE ON SCHEMA pg_catalog TO "{{ pg_replication_username }}";
GRANT EXECUTE ON FUNCTION pg_catalog.current_setting(text) TO "{{ pg_replication_username }}";
GRANT EXECUTE ON FUNCTION pg_catalog.set_config(text, text, boolean) TO "{{ pg_replication_username }}";
GRANT EXECUTE ON FUNCTION pg_catalog.pg_is_in_recovery() TO "{{ pg_replication_username }}";
{% if pg_version|int < 15 %}
GRANT EXECUTE ON FUNCTION pg_catalog.pg_start_backup(text, boolean, boolean) TO "{{ pg_replication_username }}";
GRANT EXECUTE ON FUNCTION pg_catalog.pg_stop_backup(boolean, boolean) TO "{{ pg_replication_username }}";
{% endif %}
GRANT EXECUTE ON FUNCTION pg_catalog.pg_create_restore_point(text) TO "{{ pg_replication_username }}";
GRANT EXECUTE ON FUNCTION pg_catalog.pg_switch_wal() TO "{{ pg_replication_username }}";
GRANT EXECUTE ON FUNCTION pg_catalog.pg_last_wal_replay_lsn() TO "{{ pg_replication_username }}";
GRANT EXECUTE ON FUNCTION pg_catalog.txid_current() TO "{{ pg_replication_username }}";
GRANT EXECUTE ON FUNCTION pg_catalog.txid_current_snapshot() TO "{{ pg_replication_username }}";
GRANT EXECUTE ON FUNCTION pg_catalog.txid_snapshot_xmax(txid_snapshot) TO "{{ pg_replication_username }}";
GRANT EXECUTE ON FUNCTION pg_catalog.pg_control_checkpoint() TO "{{ pg_replication_username }}";
--==================================================================--
-- Monitor Schema --
--==================================================================--
----------------------------------------------------------------------
-- cleanse & create monitor schema
----------------------------------------------------------------------
CREATE SCHEMA IF NOT EXISTS monitor;
GRANT USAGE ON SCHEMA monitor TO "{{ pg_monitor_username }}";
GRANT USAGE ON SCHEMA monitor TO "{{ pg_admin_username }}";
GRANT USAGE ON SCHEMA monitor TO "{{ pg_replication_username }}";
ALTER USER "{{ pg_monitor_username }}" SET search_path TO monitor,public;
--==================================================================--
-- Heartbeat Table --
--==================================================================--
-- table to hold heartbeat records
DROP TABLE IF EXISTS monitor.heartbeat CASCADE;
CREATE TABLE IF NOT EXISTS monitor.heartbeat
(
id VARCHAR(64) PRIMARY KEY,
ts TIMESTAMPTZ,
lsn BIGINT,
txid BIGINT
);
COMMENT ON TABLE monitor.heartbeat IS 'heartbeat table, contains one row only';
REVOKE INSERT,UPDATE,DELETE ON TABLE monitor.heartbeat FROM dbrole_readwrite;
GRANT SELECT,INSERT,UPDATE,DELETE ON TABLE monitor.heartbeat TO pg_monitor;
-- function to generate & return generated heartbeat record
CREATE OR REPLACE FUNCTION monitor.upsert_heartbeat()
RETURNS monitor.heartbeat SET search_path = '' AS $$
INSERT INTO monitor.heartbeat(id, ts, lsn, txid) VALUES (coalesce(current_setting('cluster_name', true), 'unknown'), now(), pg_current_wal_lsn() - '0/0'::PG_LSN, pg_current_xact_id()::text::BIGINT)
ON CONFLICT(id) DO UPDATE SET ts=EXCLUDED.ts, lsn=EXCLUDED.lsn, txid=EXCLUDED.txid RETURNING *;
$$ LANGUAGE SQL VOLATILE;
COMMENT ON FUNCTION monitor.upsert_heartbeat() IS 'upsert a heartbeat record';
REVOKE ALL ON FUNCTION monitor.upsert_heartbeat() FROM PUBLIC;
REVOKE ALL ON FUNCTION monitor.upsert_heartbeat() FROM dbrole_readonly;
REVOKE ALL ON FUNCTION monitor.upsert_heartbeat() FROM dbrole_offline;
GRANT EXECUTE ON FUNCTION monitor.upsert_heartbeat() TO pg_monitor;
-- function to be used by monitor tools
CREATE OR REPLACE FUNCTION monitor.beating() RETURNS TABLE (cls TEXT, ts TIMESTAMPTZ, lsn PG_LSN, lsn_int BIGINT, txid BIGINT, status TEXT) SET search_path = '' AS $$ SELECT id AS cls, ts , '0/0'::PG_LSN + lsn AS lsn, lsn AS lsn_int, txid, CASE WHEN pg_is_in_recovery() THEN 'recovery' ELSE 'leading' END AS status FROM
(SELECT (CASE WHEN pg_is_in_recovery() THEN (SELECT h FROM monitor.heartbeat h) ELSE monitor.upsert_heartbeat() END).*) d;
$$ LANGUAGE SQL VOLATILE;
COMMENT ON FUNCTION monitor.beating() IS 'refresh heartbeat on leader and return status';
REVOKE ALL ON FUNCTION monitor.beating() FROM PUBLIC;
REVOKE ALL ON FUNCTION monitor.beating() FROM dbrole_readonly;
REVOKE ALL ON FUNCTION monitor.beating() FROM dbrole_offline;
GRANT EXECUTE ON FUNCTION monitor.beating() TO pg_monitor;
-- function to return explain plan of given query
CREATE OR REPLACE FUNCTION monitor.explain(query TEXT) RETURNS JSON
SET search_path = '' AS $$
DECLARE result JSON;
BEGIN
EXECUTE format('EXPLAIN (ANALYZE, COSTS, VERBOSE, BUFFERS, FORMAT JSON) %s', query) INTO result;
RETURN result;
END; $$ LANGUAGE plpgsql;
COMMENT ON FUNCTION monitor.explain(TEXT) IS 'return verbose explain json plan for given query text';
--==================================================================--
-- Monitor Views --
--==================================================================--
----------------------------------------------------------------------
-- Table bloat estimate : monitor.pg_table_bloat
----------------------------------------------------------------------
DROP FUNCTION IF EXISTS monitor.pg_table_bloat() CASCADE;
DROP FUNCTION IF EXISTS monitor.pg_index_bloat() CASCADE;
DROP VIEW IF EXISTS monitor.pg_table_bloat CASCADE;
DROP VIEW IF EXISTS monitor.pg_index_bloat CASCADE;
-- table bloat func
CREATE OR REPLACE FUNCTION monitor.pg_table_bloat() RETURNS TABLE(datname TEXT,nspname TEXT,relname TEXT,tblid OID,size BIGINT,ratio FLOAT) SET search_path = '' AS
$$SELECT CURRENT_CATALOG AS datname, nspname, relname , tblid , bs * tblpages AS size,
CASE WHEN tblpages - est_tblpages_ff > 0 THEN (tblpages - est_tblpages_ff)/tblpages::FLOAT ELSE 0 END AS ratio
FROM (
SELECT ceil( reltuples / ( (bs-page_hdr)*fillfactor/(tpl_size*100) ) ) + ceil( toasttuples / 4 ) AS est_tblpages_ff,
tblpages, fillfactor, bs, tblid, nspname, relname, is_na
FROM (
SELECT
( 4 + tpl_hdr_size + tpl_data_size + (2 * ma)
- CASE WHEN tpl_hdr_size % ma = 0 THEN ma ELSE tpl_hdr_size % ma END
- CASE WHEN ceil(tpl_data_size)::INT % ma = 0 THEN ma ELSE ceil(tpl_data_size)::INT % ma END
) AS tpl_size, (heappages + toastpages) AS tblpages, heappages,
toastpages, reltuples, toasttuples, bs, page_hdr, tblid, nspname, relname, fillfactor, is_na
FROM (
SELECT
tbl.oid AS tblid, ns.nspname , tbl.relname, tbl.reltuples,
tbl.relpages AS heappages, coalesce(toast.relpages, 0) AS toastpages,
coalesce(toast.reltuples, 0) AS toasttuples,
coalesce(substring(array_to_string(tbl.reloptions, ' ') FROM 'fillfactor=([0-9]+)')::smallint, 100) AS fillfactor,
current_setting('block_size')::numeric AS bs,
CASE WHEN version()~'mingw32' OR version()~'64-bit|x86_64|ppc64|ia64|amd64' THEN 8 ELSE 4 END AS ma,
24 AS page_hdr,
23 + CASE WHEN MAX(coalesce(s.null_frac,0)) > 0 THEN ( 7 + count(s.attname) ) / 8 ELSE 0::int END
+ CASE WHEN bool_or(att.attname = 'oid' and att.attnum < 0) THEN 4 ELSE 0 END AS tpl_hdr_size,
sum( (1-coalesce(s.null_frac, 0)) * coalesce(s.avg_width, 0) ) AS tpl_data_size,
bool_or(att.atttypid = 'pg_catalog.name'::regtype)
OR sum(CASE WHEN att.attnum > 0 THEN 1 ELSE 0 END) <> count(s.attname) AS is_na
FROM pg_attribute AS att
JOIN pg_class AS tbl ON att.attrelid = tbl.oid
JOIN pg_namespace AS ns ON ns.oid = tbl.relnamespace
LEFT JOIN pg_stats AS s ON s.schemaname=ns.nspname AND s.tablename = tbl.relname AND s.inherited=false AND s.attname=att.attname
LEFT JOIN pg_class AS toast ON tbl.reltoastrelid = toast.oid
WHERE NOT att.attisdropped AND tbl.relkind = 'r' AND nspname NOT IN ('pg_catalog','information_schema')
GROUP BY tbl.oid, ns.nspname, tbl.relname, tbl.reltuples, tbl.relpages, toast.relpages, toast.reltuples, tbl.reloptions
) AS s
) AS s2
) AS s3
WHERE NOT is_na;
$$ LANGUAGE SQL SECURITY DEFINER;
-- index bloat func
CREATE OR REPLACE FUNCTION monitor.pg_index_bloat() RETURNS TABLE(datname TEXT,nspname TEXT,relname TEXT,tblid OID,idxid OID,size BIGINT,ratio FLOAT) SET search_path = '' AS $$
SELECT CURRENT_CATALOG AS datname, nspname, idxname AS relname, tblid, idxid,
relpages::BIGINT * bs AS size,
COALESCE((relpages - ( reltuples * (6 + ma - (CASE WHEN index_tuple_hdr % ma = 0 THEN ma ELSE index_tuple_hdr % ma END)
+ nulldatawidth + ma - (CASE WHEN nulldatawidth % ma = 0 THEN ma ELSE nulldatawidth % ma END))
/ (bs - pagehdr)::FLOAT + 1 )), 0) / relpages::FLOAT AS ratio
FROM (
SELECT nspname, idxname, indrelid AS tblid, indexrelid AS idxid,
reltuples, relpages,
current_setting('block_size')::INTEGER AS bs,
(CASE WHEN version() ~ 'mingw32' OR version() ~ '64-bit|x86_64|ppc64|ia64|amd64' THEN 8 ELSE 4 END) AS ma,
24 AS pagehdr,
(CASE WHEN max(COALESCE(pg_stats.null_frac, 0)) = 0 THEN 2 ELSE 6 END) AS index_tuple_hdr,
sum((1.0 - COALESCE(pg_stats.null_frac, 0.0)) *
COALESCE(pg_stats.avg_width, 1024))::INTEGER AS nulldatawidth
FROM pg_attribute
JOIN (
SELECT pg_namespace.nspname, ic.relname AS idxname, ic.reltuples, ic.relpages, pg_index.indrelid,
pg_index.indexrelid, tc.relname AS tablename,
regexp_split_to_table(pg_index.indkey::TEXT, ' ')::INTEGER AS attnum,
pg_index.indexrelid AS index_oid
FROM pg_index
JOIN pg_class ic ON pg_index.indexrelid = ic.oid
JOIN pg_class tc ON pg_index.indrelid = tc.oid
JOIN pg_namespace ON ic.relnamespace = pg_namespace.oid
JOIN pg_am ON ic.relam = pg_am.oid
WHERE pg_am.amname = 'btree' AND ic.relpages > 0 AND pg_namespace.nspname NOT IN ('pg_catalog', 'information_schema')
) ind_atts ON pg_attribute.attrelid = ind_atts.indexrelid AND pg_attribute.attnum = ind_atts.attnum
JOIN pg_stats ON pg_stats.schemaname = ind_atts.nspname
AND ((pg_stats.tablename = ind_atts.tablename AND pg_stats.attname = pg_get_indexdef(pg_attribute.attrelid, pg_attribute.attnum, TRUE))
OR (pg_stats.tablename = ind_atts.idxname AND pg_stats.attname = pg_attribute.attname))
WHERE pg_attribute.attnum > 0
GROUP BY nspname, idxname, indrelid, indexrelid, reltuples, relpages
) est
$$ LANGUAGE SQL SECURITY DEFINER;
CREATE OR REPLACE VIEW monitor.pg_table_bloat AS SELECT * FROM monitor.pg_table_bloat();
COMMENT ON VIEW monitor.pg_table_bloat IS 'postgres table bloat estimate';
CREATE OR REPLACE VIEW monitor.pg_index_bloat AS SELECT * FROM monitor.pg_index_bloat();
COMMENT ON VIEW monitor.pg_index_bloat IS 'postgres index bloat estimate (btree)';
GRANT EXECUTE ON FUNCTION monitor.pg_table_bloat() TO pg_monitor;
GRANT EXECUTE ON FUNCTION monitor.pg_index_bloat() TO pg_monitor;
GRANT SELECT ON monitor.pg_table_bloat TO pg_monitor;
GRANT SELECT ON monitor.pg_index_bloat TO pg_monitor;
----------------------------------------------------------------------
-- Relation Bloat : monitor.pg_bloat
----------------------------------------------------------------------
DROP VIEW IF EXISTS monitor.pg_bloat CASCADE;
CREATE OR REPLACE VIEW monitor.pg_bloat AS
SELECT coalesce(ib.datname, tb.datname) AS datname,
coalesce(ib.nspname, tb.nspname) AS nspname,
coalesce(ib.tblid, tb.tblid) AS tblid,
coalesce(tb.nspname || '.' || tb.relname, ib.nspname || '.' || ib.tblid::RegClass) AS tblname,
tb.size AS tbl_size,
CASE WHEN tb.ratio < 0 THEN 0 ELSE round(tb.ratio::NUMERIC, 6) END AS tbl_ratio,
(tb.size * (CASE WHEN tb.ratio < 0 THEN 0 ELSE tb.ratio::NUMERIC END)) ::BIGINT AS tbl_wasted,
ib.idxid,
ib.nspname || '.' || ib.relname AS idxname,
ib.size AS idx_size,
CASE WHEN ib.ratio < 0 THEN 0 ELSE round(ib.ratio::NUMERIC, 5) END AS idx_ratio,
(ib.size * (CASE WHEN ib.ratio < 0 THEN 0 ELSE ib.ratio::NUMERIC END)) ::BIGINT AS idx_wasted
FROM monitor.pg_index_bloat ib
FULL OUTER JOIN monitor.pg_table_bloat tb ON ib.tblid = tb.tblid;
COMMENT ON VIEW monitor.pg_bloat IS 'postgres relation bloat detail';
GRANT SELECT ON monitor.pg_bloat TO pg_monitor;
----------------------------------------------------------------------
-- monitor.pg_index_bloat_human
----------------------------------------------------------------------
DROP VIEW IF EXISTS monitor.pg_index_bloat_human CASCADE;
CREATE OR REPLACE VIEW monitor.pg_index_bloat_human AS
SELECT idxname AS name,
tblname,
idx_wasted AS wasted,
pg_size_pretty(idx_size) AS idx_size,
round(100 * idx_ratio::NUMERIC, 2) AS idx_ratio,
pg_size_pretty(idx_wasted) AS idx_wasted,
pg_size_pretty(tbl_size) AS tbl_size,
round(100 * tbl_ratio::NUMERIC, 2) AS tbl_ratio,
pg_size_pretty(tbl_wasted) AS tbl_wasted
FROM monitor.pg_bloat
WHERE idxname IS NOT NULL;
COMMENT ON VIEW monitor.pg_index_bloat_human IS 'postgres index bloat info in human-readable format';
GRANT SELECT ON monitor.pg_index_bloat_human TO pg_monitor;
----------------------------------------------------------------------
-- monitor.pg_table_bloat_human
----------------------------------------------------------------------
DROP VIEW IF EXISTS monitor.pg_table_bloat_human CASCADE;
CREATE OR REPLACE VIEW monitor.pg_table_bloat_human AS
SELECT tblname AS name,
idx_wasted + tbl_wasted AS wasted,
pg_size_pretty(idx_wasted + tbl_wasted) AS all_wasted,
pg_size_pretty(tbl_wasted) AS tbl_wasted,
pg_size_pretty(tbl_size) AS tbl_size,
tbl_ratio,
pg_size_pretty(idx_wasted) AS idx_wasted,
pg_size_pretty(idx_size) AS idx_size,
round(idx_wasted::NUMERIC * 100.0 / idx_size, 2) AS idx_ratio
FROM (SELECT datname,
nspname,
tblname,
coalesce(max(tbl_wasted), 0) AS tbl_wasted,
coalesce(max(tbl_size), 1) AS tbl_size,
round(100 * coalesce(max(tbl_ratio), 0)::NUMERIC, 2) AS tbl_ratio,
coalesce(sum(idx_wasted), 0) AS idx_wasted,
coalesce(sum(idx_size), 1) AS idx_size
FROM monitor.pg_bloat
WHERE tblname IS NOT NULL
GROUP BY datname,nspname,tblname
) d;
COMMENT ON VIEW monitor.pg_table_bloat_human IS 'postgres table bloat info in human-readable format';
GRANT SELECT ON monitor.pg_table_bloat_human TO pg_monitor;
----------------------------------------------------------------------
-- Activity Overview: monitor.pg_session
----------------------------------------------------------------------
DROP VIEW IF EXISTS monitor.pg_session CASCADE;
CREATE OR REPLACE VIEW monitor.pg_session AS
SELECT coalesce(datname, 'all') AS datname, numbackends, active, idle, ixact, max_duration, max_tx_duration, max_conn_duration
FROM (
SELECT datname,
count(*) AS numbackends,
count(*) FILTER ( WHERE state = 'active' ) AS active,
count(*) FILTER ( WHERE state = 'idle' ) AS idle,
count(*) FILTER ( WHERE state = 'idle in transaction'
OR state = 'idle in transaction (aborted)' ) AS ixact,
max(extract(epoch from now() - state_change))
FILTER ( WHERE state = 'active' ) AS max_duration,
max(extract(epoch from now() - xact_start)) AS max_tx_duration,
max(extract(epoch from now() - backend_start)) AS max_conn_duration
FROM pg_stat_activity
WHERE backend_type = 'client backend'
AND pid <> pg_backend_pid()
GROUP BY ROLLUP (datname)
ORDER BY 1 NULLS FIRST
) t;
COMMENT ON VIEW monitor.pg_session IS 'postgres activity group by session';
GRANT SELECT ON monitor.pg_session TO pg_monitor;
----------------------------------------------------------------------
-- Sequential Scan: monitor.pg_seq_scan
----------------------------------------------------------------------
DROP VIEW IF EXISTS monitor.pg_seq_scan CASCADE;
CREATE OR REPLACE VIEW monitor.pg_seq_scan AS
SELECT schemaname AS nspname,
relname,
seq_scan,
seq_tup_read,
seq_tup_read / seq_scan AS seq_tup_avg,
idx_scan,
n_live_tup + n_dead_tup AS tuples,
round(n_live_tup * 100.0::NUMERIC / (n_live_tup + n_dead_tup), 2) AS live_ratio
FROM pg_stat_user_tables
WHERE seq_scan > 0
and (n_live_tup + n_dead_tup) > 0
ORDER BY seq_scan DESC;
COMMENT ON VIEW monitor.pg_seq_scan IS 'table that have seq scan';
GRANT SELECT ON monitor.pg_seq_scan TO pg_monitor;
----------------------------------------------------------------------
-- Lock Waiting: monitor.pg_lock_waiting
----------------------------------------------------------------------
DROP VIEW IF EXISTS monitor.pg_lock_waiting CASCADE;
CREATE VIEW monitor.pg_lock_waiting AS
WITH RECURSIVE
activity as (select pg_blocking_pids(pid) blocked_by, *,
extract(seconds FROM age(clock_timestamp(), xact_start)) as xact_wait,
extract(seconds FROM age(clock_timestamp(),
{% if pg_version|int >= 14 %}
(select max(l.waitstart) from pg_locks l where a.pid = l.pid) -- PG 14 and newer
{% else %}
state_change -- PG 13 and below
{% endif %}
)) as lock_wait
-- "pg_locks.waitstart" PG14+ only; for older versions: age(clock_timestamp(), state_change) as wait_age
from pg_stat_activity a
where state is distinct from 'idle'),
blockers as (select array_agg(distinct c order by c) as pids
from (select unnest(blocked_by)
from activity) as dt(c)),
tree as (select activity.*,
1 as level,
activity.pid as top_blocker_pid,
array [activity.pid] as path,
array [activity.pid]::int[] as all_blockers_above
from activity, blockers
where array [pid] <@ blockers.pids and blocked_by = '{}'::int[]
union all
select activity.*,
tree.level + 1 as level,
tree.top_blocker_pid,
path || array [activity.pid] as path,
tree.all_blockers_above || array_agg(activity.pid) over () as all_blockers_above
from activity,
tree
where not array [activity.pid] <@ tree.all_blockers_above
and activity.blocked_by <> '{}'::int[]
and activity.blocked_by <@ tree.all_blockers_above)
SELECT pid,
blocked_by,
case when wait_event_type = 'Lock' then 'waiting' else state end as state,
wait_event_type,
wait_event,
xact_wait,
lock_wait,
age(backend_xid) AS xid_age,
-- 2147483647 - age(backend_xmin) as xmin_ttf,
datname, usename, level - 1 AS level,
(select count(distinct t1.pid) from tree t1 where array [tree.pid] <@ t1.path and t1.pid <> tree.pid) as blkd,
format('%s %s%s', lpad('[' || pid::text || ']', 9, ' '), repeat('.', level - 1) || case when level > 1 then ' ' end, left(query, 1000) ) as query
FROM tree
WHERE datname = CURRENT_DATABASE()
ORDER BY top_blocker_pid, level, pid;
COMMENT ON VIEW monitor.pg_lock_waiting IS 'lock waiting tree';
GRANT SELECT ON monitor.pg_lock_waiting TO pg_monitor;
--==================================================================--
-- Functions --
--==================================================================--
{% if pg_version|int >= 13 %}
----------------------------------------------------------------------
-- pg_shmem auxiliary function (PG13+ only)
----------------------------------------------------------------------
DROP FUNCTION IF EXISTS monitor.pg_shmem() CASCADE;
CREATE OR REPLACE FUNCTION monitor.pg_shmem() RETURNS SETOF pg_shmem_allocations SET search_path = '' AS
$$SELECT * FROM pg_shmem_allocations;$$ LANGUAGE SQL SECURITY DEFINER;
COMMENT ON FUNCTION monitor.pg_shmem() IS 'security wrapper for system view pg_shmem';
REVOKE ALL ON FUNCTION monitor.pg_shmem() FROM PUBLIC;
REVOKE ALL ON FUNCTION monitor.pg_shmem() FROM dbrole_readonly;
REVOKE ALL ON FUNCTION monitor.pg_shmem() FROM dbrole_offline;
GRANT EXECUTE ON FUNCTION monitor.pg_shmem() TO pg_monitor;
{% endif %}
----------------------------------------------------------------------
-- monitor.pgbouncer_auth for pgbouncer_auth_query
----------------------------------------------------------------------
{% if pgbouncer_enabled|bool %}
CREATE OR REPLACE FUNCTION monitor.pgbouncer_auth(p_username TEXT) RETURNS TABLE(username TEXT, password TEXT) SET search_path = '' AS
$$BEGIN
RAISE WARNING 'PgBouncer auth request: %', p_username;
RETURN QUERY SELECT rolname::TEXT, rolpassword::TEXT FROM pg_authid WHERE NOT rolsuper AND rolname = p_username;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
REVOKE ALL ON FUNCTION monitor.pgbouncer_auth(p_username TEXT) FROM PUBLIC;
REVOKE ALL ON FUNCTION monitor.pgbouncer_auth(p_username TEXT) FROM dbrole_readonly;
REVOKE ALL ON FUNCTION monitor.pgbouncer_auth(p_username TEXT) FROM dbrole_offline;
{% endif %}
--==================================================================--
-- Foreign Tables --
--==================================================================--
----------------------------------------------------------------------
-- current log
----------------------------------------------------------------------
CREATE TYPE monitor.log_level AS ENUM (
'LOG','INFO','NOTICE','WARNING','ERROR','FATAL','PANIC','DEBUG'
);
COMMENT ON TYPE monitor.log_level IS 'PostgreSQL Log Level';
-- current log
DROP FOREIGN TABLE IF EXISTS monitor.pg_log;
CREATE FOREIGN TABLE monitor.pg_log
(
ts TIMESTAMPTZ, -- ts
username TEXT, -- user name
datname TEXT, -- database name
pid INTEGER, -- process_id
conn TEXT, -- connect_from
sid TEXT, -- session id
sln BIGINT, -- session line number
cmd_tag TEXT, -- command tag
stime TIMESTAMPTZ, -- session start time
vxid TEXT, -- virtual transaction id
txid BIGINT, -- transaction id
level monitor.log_level, -- log level
code VARCHAR(5), -- sql state error code
msg TEXT, -- message
detail TEXT, -- detail
hint TEXT, -- hint
iq TEXT, -- internal query
iqp INTEGER, -- internal query position
context TEXT, -- context
q TEXT, -- query
qp INTEGER, -- query position
location TEXT, -- location
appname TEXT -- application name
{% if pg_version|int >= 13%}
,backend TEXT -- backend_type (PG13)
{% endif %}
{% if pg_version|int >= 14%}
,leader_pid INTEGER -- parallel group leader pid, if this is worker
{% endif %}
{% if pg_version|int >= 14%}
,query_id BIGINT -- query id of the current query
{% endif %}
) SERVER fs OPTIONS (program $$cat $(cat /pg/data/current_logfiles | awk '{print $2}');$$, format 'csv');
COMMENT ON FOREIGN TABLE monitor.pg_log IS 'current log file foreign table';
REVOKE ALL ON monitor.pg_log FROM PUBLIC;
REVOKE ALL ON monitor.pg_log FROM dbrole_offline;
REVOKE ALL ON monitor.pg_log FROM dbrole_readonly;
REVOKE ALL ON monitor.pg_log FROM dbrole_readwrite;
REVOKE ALL ON monitor.pg_log FROM dbrole_admin;
GRANT SELECT ON monitor.pg_log TO pg_monitor;
----------------------------------------------------------------------
-- pgbackrest information
----------------------------------------------------------------------
DROP FOREIGN TABLE IF EXISTS monitor.pgbackrest_info CASCADE;
CREATE FOREIGN TABLE IF NOT EXISTS monitor.pgbackrest_info (data JSONB)
SERVER fs OPTIONS (PROGRAM $$pgbackrest --stanza={{ pg_cluster }} --output=json info$$ , FORMAT 'text');
REVOKE ALL ON monitor.pgbackrest_info FROM PUBLIC;
REVOKE ALL ON monitor.pgbackrest_info FROM dbrole_offline;
REVOKE ALL ON monitor.pgbackrest_info FROM dbrole_readonly;
REVOKE ALL ON monitor.pgbackrest_info FROM dbrole_readwrite;
GRANT SELECT ON monitor.pgbackrest_info TO pg_monitor;
DROP VIEW IF EXISTS monitor.pgbackrest;
CREATE OR REPLACE VIEW monitor.pgbackrest AS
SELECT name,
value ->> 'type' AS bk_type,
(value ->> 'error')::BOOLEAN AS bk_error,
current_archive ->> 'min' as wal_min,
current_archive ->> 'max' as wal_max,
value ->> 'label' AS bk_label,
value ->> 'prior' AS bk_prior,
(value -> 'timestamp' ->> 'start')::NUMERIC AS bk_start_ts,
(value -> 'timestamp' ->> 'stop')::NUMERIC AS bk_stop_ts,
to_timestamp((value -> 'timestamp' ->> 'start')::NUMERIC) AS bk_start_at,
to_timestamp((value -> 'timestamp' ->> 'stop')::NUMERIC) AS bk_stop_at,
value -> 'lsn' ->> 'start' AS bk_start_lsn,
value -> 'lsn' ->> 'stop' AS bk_stop_lsn,
(value -> 'info' ->> 'size')::BIGINT AS bk_size,
(value -> 'info' ->> 'delta')::BIGINT AS bk_delta,
(value -> 'info' -> 'repo' ->> 'size')::BIGINT AS bk_repo_size,
(value -> 'info' -> 'repo' ->> 'delta')::BIGINT AS bk_repo_delta,
value -> 'reference' AS bk_reference,
value -> 'annotation' AS bk_annotation
FROM (SELECT value ->> 'name' AS name,
value -> 'backup' AS backups,
value -> 'archive' -> (jsonb_array_length(value -> 'archive') - 1) AS current_archive
FROM monitor.pgbackrest_info i, jsonb_array_elements(i.data)) z, jsonb_array_elements(z.backups);
REVOKE ALL ON monitor.pgbackrest FROM PUBLIC;
REVOKE ALL ON monitor.pgbackrest FROM dbrole_offline;
REVOKE ALL ON monitor.pgbackrest FROM dbrole_readonly;
REVOKE ALL ON monitor.pgbackrest FROM dbrole_readwrite;
REVOKE ALL ON monitor.pgbackrest FROM dbrole_admin;
GRANT SELECT ON monitor.pgbackrest TO pg_monitor;
----------------------------------------------------------------------
-- patroni information
----------------------------------------------------------------------
DROP FOREIGN TABLE IF EXISTS monitor.patroni_info CASCADE;
CREATE FOREIGN TABLE IF NOT EXISTS monitor.patroni_info (data JSONB)
SERVER fs OPTIONS (PROGRAM $$curl http{% if patroni_ssl_enabled|bool %}s{% endif %}://127.0.0.1:{{ patroni_port }}/cluster$$ , FORMAT 'text');
REVOKE ALL ON monitor.patroni_info FROM PUBLIC;
REVOKE ALL ON monitor.patroni_info FROM dbrole_offline;
REVOKE ALL ON monitor.patroni_info FROM dbrole_readonly;
REVOKE ALL ON monitor.patroni_info FROM dbrole_readwrite;
GRANT SELECT ON monitor.patroni_info TO pg_monitor;
DROP FOREIGN TABLE IF EXISTS monitor.patroni_conf CASCADE;
CREATE FOREIGN TABLE IF NOT EXISTS monitor.patroni_conf (data JSONB)
SERVER fs OPTIONS (PROGRAM $$cat patroni.dynamic.json$$ , FORMAT 'text');
REVOKE ALL ON monitor.patroni_conf FROM PUBLIC;
REVOKE ALL ON monitor.patroni_conf FROM dbrole_offline;
REVOKE ALL ON monitor.patroni_conf FROM dbrole_readonly;
REVOKE ALL ON monitor.patroni_conf FROM dbrole_readwrite;
GRANT SELECT ON monitor.patroni_conf TO pg_monitor;
DROP VIEW IF EXISTS monitor.patroni;
CREATE OR REPLACE VIEW monitor.patroni AS
SELECT value ->> 'name' AS name,
CASE value ->> 'role' WHEN 'leader' THEN 'primary' ELSE value ->> 'role' END AS role,
value ->> 'host' AS host,
value ->> 'port' AS port,
value ->> 'state' AS state,
(value ->> 'timeline')::INTEGER AS timeline,
(value ->> 'lag')::BIGINT AS lag,
value ->> 'api_url' AS url,
value -> 'tags' ->> 'replicatefrom' AS replicatefrom,
coalesce((value -> 'tags' -> 'nofailover') ::BOOLEAN, false)::BOOLEAN AS nofailover,
coalesce((value -> 'tags' -> 'clonefrom') ::BOOLEAN, false)::BOOLEAN AS clonefrom,
coalesce((value -> 'tags' -> 'noloadbalance') ::BOOLEAN, false)::BOOLEAN AS noloadbalance,
coalesce((value -> 'tags' -> 'nosync') ::BOOLEAN, false)::BOOLEAN AS nosync,
value -> 'tags' AS tags
FROM monitor.patroni_info i, jsonb_array_elements(data -> 'members');
REVOKE ALL ON monitor.patroni FROM PUBLIC;
REVOKE ALL ON monitor.patroni FROM dbrole_offline;
REVOKE ALL ON monitor.patroni FROM dbrole_readonly;
REVOKE ALL ON monitor.patroni FROM dbrole_readwrite;
GRANT SELECT ON monitor.patroni TO pg_monitor;
-- get process status
DROP FOREIGN TABLE IF EXISTS monitor.process_status CASCADE;
CREATE FOREIGN TABLE monitor.process_status (
username TEXT,
pid INTEGER,
cpu NUMERIC,
mem NUMERIC,
vsz BIGINT,
rss BIGINT,
tty TEXT,
stat TEXT,
start TEXT,
uptime TEXT,
command TEXT
) SERVER fs OPTIONS (PROGRAM $$
ps aux | awk '{print $1,$2,$3,$4,$5,$6,$7,$8,$9,$10,substr($0,index($0,$11))}' OFS='\037'
$$ , FORMAT 'csv', DELIMITER E'\037', HEADER 'TRUE');
REVOKE ALL ON monitor.process_status FROM PUBLIC;
REVOKE ALL ON monitor.process_status FROM dbrole_offline;
REVOKE ALL ON monitor.process_status FROM dbrole_readonly;
REVOKE ALL ON monitor.process_status FROM dbrole_readwrite;
GRANT SELECT ON monitor.process_status TO pg_monitor;
-- get disk usage
DROP FOREIGN TABLE IF EXISTS monitor.disk_free CASCADE;
CREATE FOREIGN TABLE monitor.disk_free (
fsname TEXT,
fstype TEXT,
total_1m BIGINT,
used_1m BIGINT,
avail_1m BIGINT,
percent TEXT,
itotal BIGINT,
iused BIGINT,
ifree BIGINT,
ipercent TEXT,
mountpoint TEXT
) SERVER fs OPTIONS (PROGRAM $$
df -ml --output=source,fstype,size,used,avail,pcent,itotal,iused,iavail,ipcent,target | tail -n +2 | awk '{print $1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11}' OFS='\037'
$$,
FORMAT 'csv', DELIMITER E'\037'
);
REVOKE ALL ON monitor.disk_free FROM PUBLIC;
REVOKE ALL ON monitor.disk_free FROM dbrole_offline;
REVOKE ALL ON monitor.disk_free FROM dbrole_readonly;
REVOKE ALL ON monitor.disk_free FROM dbrole_readwrite;
GRANT SELECT ON monitor.disk_free TO pg_monitor;
--==================================================================--
-- Customize Logic --
--==================================================================--
-- This script will be executed on primary instance among a newly created
-- postgres cluster. it will be executed as dbsu on template1 database
-- add your customization logic here (make sure they are idempotent)