Skip to content

Commit af46e20

Browse files
NikolaySSarumyan
authored andcommitted
fix(metrics): port "pgwatch postgresai edition"'s top-N + 'other' bucket to pg_stat/statio_all_*
1 parent ae1d192 commit af46e20

5 files changed

Lines changed: 804 additions & 47 deletions

File tree

.gitlab-ci.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,11 @@ reporter:tests:
194194
- su - postgres -c "psql -c 'SELECT version();'" || echo "PostgreSQL started"
195195
script:
196196
- chown -R postgres:postgres "$CI_PROJECT_DIR"
197-
- su - postgres -c "cd \"$CI_PROJECT_DIR\" && python -m pytest --run-integration --cov=reporter --cov-report=term --cov-report=xml:coverage/reporter-coverage.xml tests/reporter"
197+
# PGAI_USE_SYSTEM_PG tells tests/compliance_vectors/conftest.py to
198+
# connect to the cluster started by `service postgresql start` above
199+
# instead of letting pytest-postgresql spin up its own (which hangs
200+
# in this image — see tests/reporter/conftest.py for the same fix).
201+
- su - postgres -c "cd \"$CI_PROJECT_DIR\" && PGAI_USE_SYSTEM_PG=1 python -m pytest --run-integration --cov=reporter --cov-report=term --cov-report=xml:coverage/reporter-coverage.xml tests/reporter tests/compliance_vectors/test_mr262_pgwatch_topn_integration.py"
198202
# Fix ownership for artifact collection
199203
- chown -R root:root "$CI_PROJECT_DIR/coverage" || true
200204
coverage: '/TOTAL\s+\d+\s+\d+\s+(\d+)%/'

config/pgwatch-prometheus/metrics.yml

Lines changed: 230 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1552,8 +1552,36 @@ metrics:
15521552
- total_relation_size_bytes
15531553
statement_timeout_seconds: 15
15541554
pg_stat_all_indexes:
1555+
description: >
1556+
Retrieves index-level scan/tuple counters from `pg_stat_all_indexes`,
1557+
bounded to the top 100 by `idx_scan` per database. Adapts the top-N
1558+
+ `'$other$'` bucket pattern from pgwatch2 PostgresAI edition
1559+
(gitlab.com/postgres-ai/pgwatch2 — our fork of Cybertec's pgwatch2):
1560+
everything below the cap is summed into a single `'$other$'` row so
1561+
dashboard totals stay correct under the cap.
1562+
Reads pg_stat_all_indexes (not pg_stat_user_indexes) so pg_catalog,
1563+
pg_toast and _timescaledb_internal indexes stay visible: a hot
1564+
catalog index or a Timescale chunk index ranks in by activity, not
1565+
by schema membership. The `'$other$'` sentinel starts with `$`,
1566+
which is not legal as the first character of an unquoted Postgres
1567+
identifier, so it cannot collide with any real schema, table or
1568+
index name. Compatible with all Postgres versions.
15551569
sqls:
15561570
11: |
1571+
with ranked as (
1572+
select
1573+
row_number() over (
1574+
order by idx_scan desc nulls last,
1575+
schemaname, relname, indexrelname
1576+
) as rownum,
1577+
schemaname,
1578+
relname,
1579+
indexrelname,
1580+
idx_scan,
1581+
idx_tup_read,
1582+
idx_tup_fetch
1583+
from pg_stat_all_indexes
1584+
)
15571585
select /* pgwatch_generated */
15581586
current_database() as tag_datname,
15591587
schemaname as tag_schemaname,
@@ -1562,17 +1590,79 @@ metrics:
15621590
idx_scan,
15631591
idx_tup_read,
15641592
idx_tup_fetch
1565-
from pg_stat_all_indexes
1566-
order by idx_scan desc
1567-
limit 5000
1593+
from ranked
1594+
where rownum <= 100
1595+
union all
1596+
select
1597+
current_database() as tag_datname,
1598+
'$other$'::text as tag_schemaname,
1599+
'$other$'::text as tag_relname,
1600+
'$other$'::text as tag_indexrelname,
1601+
coalesce(sum(idx_scan), 0)::int8 as idx_scan,
1602+
coalesce(sum(idx_tup_read), 0)::int8 as idx_tup_read,
1603+
coalesce(sum(idx_tup_fetch), 0)::int8 as idx_tup_fetch
1604+
from ranked
1605+
where rownum > 100
1606+
group by ()
1607+
having count(*) > 0
15681608
gauges:
15691609
- idx_scan
15701610
- idx_tup_read
15711611
- idx_tup_fetch
15721612
statement_timeout_seconds: 15
15731613
pg_stat_all_tables:
1614+
description: >
1615+
Retrieves table-level activity counters from `pg_stat_all_tables`,
1616+
bounded to the top 100 per database. Adapts the top-N + `'$other$'`
1617+
bucket pattern from pgwatch2 PostgresAI edition
1618+
(gitlab.com/postgres-ai/pgwatch2): everything below the cap is summed
1619+
into a single `'$other$'` row so dashboard totals stay correct.
1620+
Reads pg_stat_all_tables (not pg_stat_user_tables) so pg_catalog,
1621+
pg_toast and _timescaledb_internal tables stay visible: a bloated
1622+
TOAST table or a huge Timescale chunk ranks in by size, not by
1623+
schema membership. Ranks by `pg_class.relpages` (8 KiB block count
1624+
cached in the catalog) joined on `relid` instead of calling
1625+
`pg_total_relation_size(relid)` per row: the catalog join is O(1)
1626+
lookup and cannot raise on a concurrently-dropped relation, whereas
1627+
the function opens the relation and toast index every call.
1628+
Large, infrequently-updated tables stay in scope. The `'$other$'`
1629+
sentinel starts with `$`, which is not legal as the first character
1630+
of an unquoted Postgres identifier, so it cannot collide with any
1631+
real schema or table name. `last_vacuum` / `last_analyze` on the
1632+
`'$other$'` row aggregate the most recent maintenance time across
1633+
the tail (not `0`), so dashboards don't render 1970-01-01 when the
1634+
tail has been vacuumed. Compatible with all Postgres versions.
15741635
sqls:
15751636
11: |
1637+
with ranked as (
1638+
select
1639+
row_number() over (
1640+
order by coalesce(c.relpages, 0) desc nulls last,
1641+
s.schemaname, s.relname
1642+
) as rownum,
1643+
s.schemaname,
1644+
s.relname,
1645+
s.seq_scan,
1646+
s.seq_tup_read,
1647+
s.idx_scan,
1648+
s.idx_tup_fetch,
1649+
s.n_tup_ins,
1650+
s.n_tup_upd,
1651+
s.n_tup_del,
1652+
s.n_tup_hot_upd,
1653+
s.n_live_tup,
1654+
s.n_dead_tup,
1655+
s.last_vacuum,
1656+
s.last_autovacuum,
1657+
s.last_analyze,
1658+
s.last_autoanalyze,
1659+
s.vacuum_count,
1660+
s.autovacuum_count,
1661+
s.analyze_count,
1662+
s.autoanalyze_count
1663+
from pg_stat_all_tables s
1664+
left join pg_class c on c.oid = s.relid
1665+
)
15761666
select /* pgwatch_generated */
15771667
current_database() as tag_datname,
15781668
schemaname as tag_schemaname,
@@ -1592,10 +1682,31 @@ metrics:
15921682
extract(epoch from greatest(last_autoanalyze, last_analyze, '1970-01-01Z'))::int8 as last_analyze,
15931683
(vacuum_count + autovacuum_count) as vacuum_count,
15941684
(analyze_count + autoanalyze_count) as analyze_count
1595-
from
1596-
pg_stat_all_tables
1597-
order by n_live_tup + n_dead_tup desc
1598-
limit 5000
1685+
from ranked
1686+
where rownum <= 100
1687+
union all
1688+
select
1689+
current_database() as tag_datname,
1690+
'$other$'::text as tag_schemaname,
1691+
'$other$'::text as tag_relname,
1692+
coalesce(sum(seq_scan), 0)::int8 as seq_scan,
1693+
coalesce(sum(seq_tup_read), 0)::int8 as seq_tup_read,
1694+
coalesce(sum(idx_scan), 0)::int8 as idx_scan,
1695+
coalesce(sum(idx_tup_fetch), 0)::int8 as idx_tup_fetch,
1696+
coalesce(sum(n_tup_ins), 0)::int8 as n_tup_ins,
1697+
coalesce(sum(n_tup_upd), 0)::int8 as n_tup_upd,
1698+
coalesce(sum(n_tup_del), 0)::int8 as n_tup_del,
1699+
coalesce(sum(n_tup_hot_upd), 0)::int8 as n_tup_hot_upd,
1700+
coalesce(sum(n_live_tup), 0)::int8 as n_live_tup,
1701+
coalesce(sum(n_dead_tup), 0)::int8 as n_dead_tup,
1702+
extract(epoch from greatest(max(last_autovacuum), max(last_vacuum), '1970-01-01Z'))::int8 as last_vacuum,
1703+
extract(epoch from greatest(max(last_autoanalyze), max(last_analyze), '1970-01-01Z'))::int8 as last_analyze,
1704+
coalesce(sum(vacuum_count + autovacuum_count), 0)::int8 as vacuum_count,
1705+
coalesce(sum(analyze_count + autoanalyze_count), 0)::int8 as analyze_count
1706+
from ranked
1707+
where rownum > 100
1708+
group by ()
1709+
having count(*) > 0
15991710
gauges:
16001711
- seq_scan
16011712
- seq_tup_read
@@ -2881,60 +2992,133 @@ metrics:
28812992
statement_timeout_seconds: 15
28822993
pg_statio_all_tables:
28832994
description: >
2884-
Retrieves table-level I/O statistics from the PostgreSQL `pg_statio_all_tables` view, providing insights into I/O operations for all tables.
2885-
It returns block-level read and hit statistics for heap, index, TOAST, and TOAST index operations broken down by schema and table.
2886-
Joined with pg_class for efficient ordering by table size.
2887-
This metric helps administrators monitor table-level I/O performance and identify which tables are generating the most I/O activity.
2888-
Compatible with all PostgreSQL versions.
2995+
Retrieves table-level I/O statistics from `pg_statio_all_tables`, returning
2996+
block-level read and hit counters for heap, index, TOAST and TOAST-index
2997+
pages. Adapts the top-N + `'$other$'` bucket pattern from pgwatch2
2998+
PostgresAI edition (gitlab.com/postgres-ai/pgwatch2): ranks tables by
2999+
heap_blks_read, keeps the top 100, and folds the tail into a single
3000+
`'$other$'` row so totals remain accurate while cardinality stays bounded.
3001+
Reads pg_statio_all_tables (not pg_statio_user_tables) so I/O on pg_catalog,
3002+
pg_toast and _timescaledb_internal stays visible — those tables enter the
3003+
top-N by activity, not by schema membership. The zero-counter row skip is
3004+
kept (those rows literally carry no information and are not identity-based).
3005+
The `'$other$'` sentinel starts with `$`, which is not legal as the first
3006+
character of an unquoted Postgres identifier, so it cannot collide with
3007+
any real schema or table name. Compatible with all Postgres versions.
28893008
sqls:
28903009
11: |-
3010+
with ranked as (
3011+
select
3012+
row_number() over (
3013+
order by heap_blks_read desc nulls last,
3014+
schemaname, relname
3015+
) as rownum,
3016+
schemaname,
3017+
relname,
3018+
heap_blks_read,
3019+
heap_blks_hit,
3020+
idx_blks_read,
3021+
idx_blks_hit,
3022+
toast_blks_read,
3023+
toast_blks_hit,
3024+
tidx_blks_read,
3025+
tidx_blks_hit
3026+
from pg_statio_all_tables
3027+
where
3028+
heap_blks_read > 0 or heap_blks_hit > 0
3029+
or idx_blks_read > 0 or idx_blks_hit > 0
3030+
or toast_blks_read > 0 or toast_blks_hit > 0
3031+
or tidx_blks_read > 0 or tidx_blks_hit > 0
3032+
)
28913033
select /* pgwatch_generated */
28923034
(extract(epoch from now()) * 1e9)::int8 as epoch_ns,
28933035
current_database() as tag_datname,
2894-
s.schemaname as tag_schemaname,
2895-
s.relname as tag_relname,
2896-
s.heap_blks_read,
2897-
s.heap_blks_hit,
2898-
s.idx_blks_read,
2899-
s.idx_blks_hit,
2900-
s.toast_blks_read,
2901-
s.toast_blks_hit,
2902-
s.tidx_blks_read,
2903-
s.tidx_blks_hit
2904-
from
2905-
pg_statio_all_tables as s
2906-
join pg_class as c on
2907-
s.relname = c.relname
2908-
and s.schemaname = c.relnamespace::regnamespace::name
2909-
order by c.relpages desc
2910-
limit 5000;
3036+
schemaname as tag_schemaname,
3037+
relname as tag_relname,
3038+
heap_blks_read,
3039+
heap_blks_hit,
3040+
idx_blks_read,
3041+
idx_blks_hit,
3042+
toast_blks_read,
3043+
toast_blks_hit,
3044+
tidx_blks_read,
3045+
tidx_blks_hit
3046+
from ranked
3047+
where rownum <= 100
3048+
union all
3049+
select
3050+
(extract(epoch from now()) * 1e9)::int8 as epoch_ns,
3051+
current_database() as tag_datname,
3052+
'$other$'::text as tag_schemaname,
3053+
'$other$'::text as tag_relname,
3054+
coalesce(sum(heap_blks_read), 0)::int8 as heap_blks_read,
3055+
coalesce(sum(heap_blks_hit), 0)::int8 as heap_blks_hit,
3056+
coalesce(sum(idx_blks_read), 0)::int8 as idx_blks_read,
3057+
coalesce(sum(idx_blks_hit), 0)::int8 as idx_blks_hit,
3058+
coalesce(sum(toast_blks_read), 0)::int8 as toast_blks_read,
3059+
coalesce(sum(toast_blks_hit), 0)::int8 as toast_blks_hit,
3060+
coalesce(sum(tidx_blks_read), 0)::int8 as tidx_blks_read,
3061+
coalesce(sum(tidx_blks_hit), 0)::int8 as tidx_blks_hit
3062+
from ranked
3063+
where rownum > 100
3064+
group by ()
3065+
having count(*) > 0;
29113066
gauges:
29123067
- '*'
29133068
statement_timeout_seconds: 15
29143069
pg_statio_all_indexes:
29153070
description: >
2916-
Retrieves index-level I/O statistics from the PostgreSQL `pg_statio_all_indexes` view, providing insights into I/O operations for all indexes.
2917-
It returns block-level read and hit statistics for index operations broken down by schema, table, and index name.
2918-
Joined with pg_class for efficient ordering by index size.
2919-
This metric helps administrators monitor index-level I/O performance and identify which indexes are generating the most I/O activity.
2920-
Compatible with all PostgreSQL versions.
3071+
Retrieves index-level I/O statistics from `pg_statio_all_indexes`, returning
3072+
block-level read and hit counters per index. Adapts the top-N + `'$other$'`
3073+
bucket pattern from pgwatch2 PostgresAI edition
3074+
(gitlab.com/postgres-ai/pgwatch2): ranks indexes by idx_blks_read, keeps the
3075+
top 100, folds the tail into a single `'$other$'` row, and drops indexes
3076+
with no I/O activity (zero-counter rows carry no information).
3077+
Reads pg_statio_all_indexes (not pg_statio_user_indexes) so catalog,
3078+
pg_toast and _timescaledb_internal indexes stay visible: a hot catalog
3079+
index will rank into the top-N by activity, not be hidden by schema name.
3080+
The `'$other$'` sentinel starts with `$`, which is not legal as the first
3081+
character of an unquoted Postgres identifier, so it cannot collide with
3082+
any real schema, table or index name. Compatible with all Postgres versions.
29213083
sqls:
29223084
11: |-
3085+
with ranked as (
3086+
select
3087+
row_number() over (
3088+
order by idx_blks_read desc nulls last,
3089+
schemaname, relname, indexrelname
3090+
) as rownum,
3091+
schemaname,
3092+
relname,
3093+
indexrelname,
3094+
idx_blks_read,
3095+
idx_blks_hit
3096+
from pg_statio_all_indexes
3097+
where idx_blks_read > 0 or idx_blks_hit > 0
3098+
)
29233099
select /* pgwatch_generated */
29243100
(extract(epoch from now()) * 1e9)::int8 as epoch_ns,
29253101
current_database() as tag_datname,
2926-
s.schemaname as tag_schemaname,
2927-
s.relname as tag_relname,
2928-
s.indexrelname as tag_indexrelname,
2929-
s.idx_blks_read,
2930-
s.idx_blks_hit
2931-
from
2932-
pg_statio_all_indexes as s
2933-
join pg_class as c on
2934-
s.indexrelname = c.relname
2935-
and s.schemaname = c.relnamespace::regnamespace::name
2936-
order by c.relpages desc
2937-
limit 5000;
3102+
schemaname as tag_schemaname,
3103+
relname as tag_relname,
3104+
indexrelname as tag_indexrelname,
3105+
idx_blks_read,
3106+
idx_blks_hit
3107+
from ranked
3108+
where rownum <= 100
3109+
union all
3110+
select
3111+
(extract(epoch from now()) * 1e9)::int8 as epoch_ns,
3112+
current_database() as tag_datname,
3113+
'$other$'::text as tag_schemaname,
3114+
'$other$'::text as tag_relname,
3115+
'$other$'::text as tag_indexrelname,
3116+
coalesce(sum(idx_blks_read), 0)::int8 as idx_blks_read,
3117+
coalesce(sum(idx_blks_hit), 0)::int8 as idx_blks_hit
3118+
from ranked
3119+
where rownum > 100
3120+
group by ()
3121+
having count(*) > 0;
29383122
gauges:
29393123
- '*'
29403124
statement_timeout_seconds: 15

0 commit comments

Comments
 (0)