pg_reports 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +22 -0
- data/LICENSE.txt +21 -0
- data/README.md +335 -0
- data/app/controllers/pg_reports/dashboard_controller.rb +133 -0
- data/app/views/layouts/pg_reports/application.html.erb +594 -0
- data/app/views/pg_reports/dashboard/index.html.erb +435 -0
- data/app/views/pg_reports/dashboard/show.html.erb +481 -0
- data/config/routes.rb +13 -0
- data/lib/pg_reports/annotation_parser.rb +114 -0
- data/lib/pg_reports/configuration.rb +83 -0
- data/lib/pg_reports/dashboard/reports_registry.rb +89 -0
- data/lib/pg_reports/engine.rb +22 -0
- data/lib/pg_reports/error.rb +15 -0
- data/lib/pg_reports/executor.rb +51 -0
- data/lib/pg_reports/modules/connections.rb +106 -0
- data/lib/pg_reports/modules/indexes.rb +111 -0
- data/lib/pg_reports/modules/queries.rb +140 -0
- data/lib/pg_reports/modules/system.rb +148 -0
- data/lib/pg_reports/modules/tables.rb +113 -0
- data/lib/pg_reports/report.rb +228 -0
- data/lib/pg_reports/sql/connections/active_connections.sql +20 -0
- data/lib/pg_reports/sql/connections/blocking_queries.sql +35 -0
- data/lib/pg_reports/sql/connections/connection_stats.sql +13 -0
- data/lib/pg_reports/sql/connections/idle_connections.sql +19 -0
- data/lib/pg_reports/sql/connections/locks.sql +20 -0
- data/lib/pg_reports/sql/connections/long_running_queries.sql +21 -0
- data/lib/pg_reports/sql/indexes/bloated_indexes.sql +36 -0
- data/lib/pg_reports/sql/indexes/duplicate_indexes.sql +38 -0
- data/lib/pg_reports/sql/indexes/index_sizes.sql +14 -0
- data/lib/pg_reports/sql/indexes/index_usage.sql +19 -0
- data/lib/pg_reports/sql/indexes/invalid_indexes.sql +15 -0
- data/lib/pg_reports/sql/indexes/missing_indexes.sql +27 -0
- data/lib/pg_reports/sql/indexes/unused_indexes.sql +18 -0
- data/lib/pg_reports/sql/queries/all_queries.sql +20 -0
- data/lib/pg_reports/sql/queries/expensive_queries.sql +22 -0
- data/lib/pg_reports/sql/queries/heavy_queries.sql +17 -0
- data/lib/pg_reports/sql/queries/low_cache_hit_queries.sql +19 -0
- data/lib/pg_reports/sql/queries/missing_index_queries.sql +25 -0
- data/lib/pg_reports/sql/queries/slow_queries.sql +17 -0
- data/lib/pg_reports/sql/system/activity_overview.sql +29 -0
- data/lib/pg_reports/sql/system/cache_stats.sql +19 -0
- data/lib/pg_reports/sql/system/database_sizes.sql +10 -0
- data/lib/pg_reports/sql/system/extensions.sql +12 -0
- data/lib/pg_reports/sql/system/settings.sql +33 -0
- data/lib/pg_reports/sql/tables/bloated_tables.sql +23 -0
- data/lib/pg_reports/sql/tables/cache_hit_ratios.sql +24 -0
- data/lib/pg_reports/sql/tables/recently_modified.sql +20 -0
- data/lib/pg_reports/sql/tables/row_counts.sql +18 -0
- data/lib/pg_reports/sql/tables/seq_scans.sql +26 -0
- data/lib/pg_reports/sql/tables/table_sizes.sql +16 -0
- data/lib/pg_reports/sql/tables/vacuum_needed.sql +22 -0
- data/lib/pg_reports/sql_loader.rb +35 -0
- data/lib/pg_reports/telegram_sender.rb +83 -0
- data/lib/pg_reports/version.rb +5 -0
- data/lib/pg_reports.rb +114 -0
- metadata +184 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
-- Expensive queries: queries consuming most total time
|
|
2
|
+
-- Requires pg_stat_statements extension
|
|
3
|
+
|
|
4
|
+
WITH total AS (
|
|
5
|
+
SELECT SUM(total_exec_time) AS total_time
|
|
6
|
+
FROM pg_stat_statements
|
|
7
|
+
WHERE calls > 0
|
|
8
|
+
)
|
|
9
|
+
SELECT
|
|
10
|
+
s.query,
|
|
11
|
+
s.calls,
|
|
12
|
+
ROUND((s.total_exec_time)::numeric, 2) AS total_time_ms,
|
|
13
|
+
ROUND((s.total_exec_time * 100.0 / t.total_time)::numeric, 2) AS percent_of_total,
|
|
14
|
+
ROUND((s.mean_exec_time)::numeric, 2) AS mean_time_ms,
|
|
15
|
+
s.rows
|
|
16
|
+
FROM pg_stat_statements s, total t
|
|
17
|
+
WHERE s.calls > 0
|
|
18
|
+
AND s.query NOT LIKE '%pg_stat_statements%'
|
|
19
|
+
AND s.query NOT LIKE 'COMMIT%'
|
|
20
|
+
AND s.query NOT LIKE 'BEGIN%'
|
|
21
|
+
ORDER BY s.total_exec_time DESC
|
|
22
|
+
LIMIT 100;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
-- Heavy queries: queries called most frequently
|
|
2
|
+
-- Requires pg_stat_statements extension
|
|
3
|
+
|
|
4
|
+
SELECT
|
|
5
|
+
query,
|
|
6
|
+
calls,
|
|
7
|
+
ROUND((total_exec_time)::numeric, 2) AS total_time_ms,
|
|
8
|
+
ROUND((mean_exec_time)::numeric, 2) AS mean_time_ms,
|
|
9
|
+
rows,
|
|
10
|
+
ROUND((shared_blks_hit * 100.0 / NULLIF(shared_blks_hit + shared_blks_read, 0))::numeric, 2) AS cache_hit_ratio
|
|
11
|
+
FROM pg_stat_statements
|
|
12
|
+
WHERE calls > 0
|
|
13
|
+
AND query NOT LIKE '%pg_stat_statements%'
|
|
14
|
+
AND query NOT LIKE 'COMMIT%'
|
|
15
|
+
AND query NOT LIKE 'BEGIN%'
|
|
16
|
+
ORDER BY calls DESC
|
|
17
|
+
LIMIT 100;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
-- Queries with low cache hit ratio
|
|
2
|
+
-- Requires pg_stat_statements extension
|
|
3
|
+
|
|
4
|
+
SELECT
|
|
5
|
+
query,
|
|
6
|
+
calls,
|
|
7
|
+
ROUND((shared_blks_hit * 100.0 / NULLIF(shared_blks_hit + shared_blks_read, 0))::numeric, 2) AS cache_hit_ratio,
|
|
8
|
+
shared_blks_hit,
|
|
9
|
+
shared_blks_read,
|
|
10
|
+
ROUND((total_exec_time)::numeric, 2) AS total_time_ms,
|
|
11
|
+
ROUND((mean_exec_time)::numeric, 2) AS mean_time_ms
|
|
12
|
+
FROM pg_stat_statements
|
|
13
|
+
WHERE calls > 10
|
|
14
|
+
AND (shared_blks_hit + shared_blks_read) > 0
|
|
15
|
+
AND query NOT LIKE '%pg_stat_statements%'
|
|
16
|
+
AND query NOT LIKE 'COMMIT%'
|
|
17
|
+
AND query NOT LIKE 'BEGIN%'
|
|
18
|
+
ORDER BY (shared_blks_hit * 1.0 / NULLIF(shared_blks_hit + shared_blks_read, 0)) ASC
|
|
19
|
+
LIMIT 100;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
-- Queries potentially missing indexes
|
|
2
|
+
-- Identifies queries with patterns suggesting sequential scans
|
|
3
|
+
-- Requires pg_stat_statements extension
|
|
4
|
+
|
|
5
|
+
SELECT
|
|
6
|
+
query,
|
|
7
|
+
calls,
|
|
8
|
+
ROUND((total_exec_time)::numeric, 2) AS total_time_ms,
|
|
9
|
+
ROUND((mean_exec_time)::numeric, 2) AS mean_time_ms,
|
|
10
|
+
rows,
|
|
11
|
+
-- Heuristic: high rows examined per call may indicate missing index
|
|
12
|
+
ROUND((rows / NULLIF(calls, 0))::numeric, 0) AS rows_per_call,
|
|
13
|
+
-- High read/hit ratio suggests disk access (possible seq scan)
|
|
14
|
+
ROUND((shared_blks_read * 100.0 / NULLIF(shared_blks_hit + shared_blks_read, 0))::numeric, 2) AS disk_read_ratio
|
|
15
|
+
FROM pg_stat_statements
|
|
16
|
+
WHERE calls > 10
|
|
17
|
+
AND (rows / NULLIF(calls, 0)) > 100
|
|
18
|
+
AND mean_exec_time > 10
|
|
19
|
+
AND query NOT LIKE '%pg_stat_statements%'
|
|
20
|
+
AND query NOT LIKE 'COMMIT%'
|
|
21
|
+
AND query NOT LIKE 'BEGIN%'
|
|
22
|
+
-- Focus on SELECT statements
|
|
23
|
+
AND (query ILIKE 'SELECT%' OR query ILIKE '%WHERE%')
|
|
24
|
+
ORDER BY (rows / NULLIF(calls, 0)) * calls DESC
|
|
25
|
+
LIMIT 100;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
-- Slow queries: queries with high mean execution time
|
|
2
|
+
-- Requires pg_stat_statements extension
|
|
3
|
+
|
|
4
|
+
SELECT
|
|
5
|
+
query,
|
|
6
|
+
calls,
|
|
7
|
+
ROUND((mean_exec_time)::numeric, 2) AS mean_time_ms,
|
|
8
|
+
ROUND((total_exec_time)::numeric, 2) AS total_time_ms,
|
|
9
|
+
ROUND((rows / NULLIF(calls, 0))::numeric, 2) AS rows_per_call,
|
|
10
|
+
ROUND((shared_blks_hit * 100.0 / NULLIF(shared_blks_hit + shared_blks_read, 0))::numeric, 2) AS cache_hit_ratio
|
|
11
|
+
FROM pg_stat_statements
|
|
12
|
+
WHERE calls > 0
|
|
13
|
+
AND query NOT LIKE '%pg_stat_statements%'
|
|
14
|
+
AND query NOT LIKE 'COMMIT%'
|
|
15
|
+
AND query NOT LIKE 'BEGIN%'
|
|
16
|
+
ORDER BY mean_exec_time DESC
|
|
17
|
+
LIMIT 100;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
-- Database activity overview
|
|
2
|
+
-- Summary metrics for current database activity
|
|
3
|
+
|
|
4
|
+
WITH stats AS (
|
|
5
|
+
SELECT
|
|
6
|
+
(SELECT count(*) FROM pg_stat_activity WHERE datname = current_database()) AS total_connections,
|
|
7
|
+
(SELECT count(*) FROM pg_stat_activity WHERE datname = current_database() AND state = 'active') AS active_queries,
|
|
8
|
+
(SELECT count(*) FROM pg_stat_activity WHERE datname = current_database() AND state = 'idle') AS idle_connections,
|
|
9
|
+
(SELECT count(*) FROM pg_stat_activity WHERE datname = current_database() AND state = 'idle in transaction') AS idle_in_transaction,
|
|
10
|
+
(SELECT count(*) FROM pg_stat_activity WHERE datname = current_database() AND wait_event IS NOT NULL) AS waiting_connections,
|
|
11
|
+
(SELECT count(*) FROM pg_locks WHERE NOT granted) AS blocked_queries,
|
|
12
|
+
(SELECT setting::int FROM pg_settings WHERE name = 'max_connections') AS max_connections,
|
|
13
|
+
(SELECT ROUND(pg_database_size(current_database()) / 1024.0 / 1024.0, 2)) AS database_size_mb
|
|
14
|
+
)
|
|
15
|
+
SELECT 'Total Connections' AS metric, total_connections::text AS value FROM stats
|
|
16
|
+
UNION ALL
|
|
17
|
+
SELECT 'Active Queries', active_queries::text FROM stats
|
|
18
|
+
UNION ALL
|
|
19
|
+
SELECT 'Idle Connections', idle_connections::text FROM stats
|
|
20
|
+
UNION ALL
|
|
21
|
+
SELECT 'Idle in Transaction', idle_in_transaction::text FROM stats
|
|
22
|
+
UNION ALL
|
|
23
|
+
SELECT 'Waiting Connections', waiting_connections::text FROM stats
|
|
24
|
+
UNION ALL
|
|
25
|
+
SELECT 'Blocked Queries', blocked_queries::text FROM stats
|
|
26
|
+
UNION ALL
|
|
27
|
+
SELECT 'Max Connections', max_connections::text FROM stats
|
|
28
|
+
UNION ALL
|
|
29
|
+
SELECT 'Database Size (MB)', database_size_mb::text FROM stats;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
-- Database cache statistics
|
|
2
|
+
-- Shows cache hit ratios for each database
|
|
3
|
+
|
|
4
|
+
SELECT
|
|
5
|
+
datname AS database,
|
|
6
|
+
ROUND(
|
|
7
|
+
CASE
|
|
8
|
+
WHEN blks_hit + blks_read > 0 THEN
|
|
9
|
+
(blks_hit * 100.0 / (blks_hit + blks_read))
|
|
10
|
+
ELSE 100
|
|
11
|
+
END::numeric,
|
|
12
|
+
2
|
|
13
|
+
) AS heap_hit_ratio,
|
|
14
|
+
blks_hit,
|
|
15
|
+
blks_read
|
|
16
|
+
FROM pg_stat_database
|
|
17
|
+
WHERE datname NOT LIKE 'template%'
|
|
18
|
+
AND datname IS NOT NULL
|
|
19
|
+
ORDER BY datname;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
-- Database sizes
|
|
2
|
+
-- Shows size of all databases
|
|
3
|
+
|
|
4
|
+
SELECT
|
|
5
|
+
datname AS database,
|
|
6
|
+
ROUND(pg_database_size(datname) / 1024.0 / 1024.0, 2) AS size_mb,
|
|
7
|
+
pg_size_pretty(pg_database_size(datname)) AS size_pretty
|
|
8
|
+
FROM pg_database
|
|
9
|
+
WHERE datistemplate = false
|
|
10
|
+
ORDER BY pg_database_size(datname) DESC;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
-- Installed extensions
|
|
2
|
+
-- Shows all installed PostgreSQL extensions
|
|
3
|
+
|
|
4
|
+
SELECT
|
|
5
|
+
extname AS name,
|
|
6
|
+
extversion AS version,
|
|
7
|
+
n.nspname AS schema,
|
|
8
|
+
c.description
|
|
9
|
+
FROM pg_extension e
|
|
10
|
+
JOIN pg_namespace n ON e.extnamespace = n.oid
|
|
11
|
+
LEFT JOIN pg_description c ON c.objoid = e.oid AND c.classoid = 'pg_extension'::regclass
|
|
12
|
+
ORDER BY extname;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
-- Important PostgreSQL settings
|
|
2
|
+
-- Shows key configuration parameters
|
|
3
|
+
|
|
4
|
+
SELECT
|
|
5
|
+
name,
|
|
6
|
+
setting,
|
|
7
|
+
unit,
|
|
8
|
+
category,
|
|
9
|
+
short_desc AS description
|
|
10
|
+
FROM pg_settings
|
|
11
|
+
WHERE name IN (
|
|
12
|
+
'shared_buffers',
|
|
13
|
+
'effective_cache_size',
|
|
14
|
+
'work_mem',
|
|
15
|
+
'maintenance_work_mem',
|
|
16
|
+
'max_connections',
|
|
17
|
+
'max_parallel_workers',
|
|
18
|
+
'max_parallel_workers_per_gather',
|
|
19
|
+
'random_page_cost',
|
|
20
|
+
'seq_page_cost',
|
|
21
|
+
'effective_io_concurrency',
|
|
22
|
+
'wal_buffers',
|
|
23
|
+
'checkpoint_completion_target',
|
|
24
|
+
'default_statistics_target',
|
|
25
|
+
'statement_timeout',
|
|
26
|
+
'lock_timeout',
|
|
27
|
+
'idle_in_transaction_session_timeout',
|
|
28
|
+
'log_min_duration_statement',
|
|
29
|
+
'autovacuum',
|
|
30
|
+
'autovacuum_vacuum_scale_factor',
|
|
31
|
+
'autovacuum_analyze_scale_factor'
|
|
32
|
+
)
|
|
33
|
+
ORDER BY category, name;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
-- Bloated tables: tables with high dead tuple ratio
|
|
2
|
+
-- High bloat indicates need for VACUUM
|
|
3
|
+
|
|
4
|
+
SELECT
|
|
5
|
+
schemaname AS schema,
|
|
6
|
+
relname AS table_name,
|
|
7
|
+
n_live_tup AS live_rows,
|
|
8
|
+
n_dead_tup AS dead_rows,
|
|
9
|
+
CASE
|
|
10
|
+
WHEN n_live_tup + n_dead_tup > 0 THEN
|
|
11
|
+
ROUND((n_dead_tup * 100.0 / (n_live_tup + n_dead_tup))::numeric, 2)
|
|
12
|
+
ELSE 0
|
|
13
|
+
END AS bloat_percent,
|
|
14
|
+
pg_size_pretty(pg_table_size(relid)) AS table_size,
|
|
15
|
+
ROUND(pg_table_size(relid) / 1024.0 / 1024.0, 2) AS table_size_mb,
|
|
16
|
+
last_vacuum,
|
|
17
|
+
last_autovacuum,
|
|
18
|
+
last_analyze,
|
|
19
|
+
last_autoanalyze
|
|
20
|
+
FROM pg_stat_user_tables
|
|
21
|
+
WHERE schemaname NOT IN ('pg_catalog', 'information_schema')
|
|
22
|
+
AND n_live_tup + n_dead_tup > 1000
|
|
23
|
+
ORDER BY n_dead_tup DESC;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
-- Table cache hit ratios
|
|
2
|
+
-- Low cache hit ratio may indicate need for more memory or index optimization
|
|
3
|
+
|
|
4
|
+
SELECT
|
|
5
|
+
schemaname AS schema,
|
|
6
|
+
relname AS table_name,
|
|
7
|
+
heap_blks_read,
|
|
8
|
+
heap_blks_hit,
|
|
9
|
+
CASE
|
|
10
|
+
WHEN heap_blks_hit + heap_blks_read > 0 THEN
|
|
11
|
+
ROUND((heap_blks_hit * 100.0 / (heap_blks_hit + heap_blks_read))::numeric, 2)
|
|
12
|
+
ELSE 100
|
|
13
|
+
END AS cache_hit_ratio,
|
|
14
|
+
idx_blks_read,
|
|
15
|
+
idx_blks_hit,
|
|
16
|
+
CASE
|
|
17
|
+
WHEN idx_blks_hit + idx_blks_read > 0 THEN
|
|
18
|
+
ROUND((idx_blks_hit * 100.0 / (idx_blks_hit + idx_blks_read))::numeric, 2)
|
|
19
|
+
ELSE 100
|
|
20
|
+
END AS idx_cache_hit_ratio
|
|
21
|
+
FROM pg_statio_user_tables
|
|
22
|
+
WHERE schemaname NOT IN ('pg_catalog', 'information_schema')
|
|
23
|
+
AND (heap_blks_hit + heap_blks_read) > 0
|
|
24
|
+
ORDER BY cache_hit_ratio ASC;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
-- Recently modified tables
|
|
2
|
+
-- Tables with recent insert/update/delete activity
|
|
3
|
+
|
|
4
|
+
SELECT
|
|
5
|
+
schemaname AS schema,
|
|
6
|
+
relname AS table_name,
|
|
7
|
+
n_tup_ins AS inserts,
|
|
8
|
+
n_tup_upd AS updates,
|
|
9
|
+
n_tup_del AS deletes,
|
|
10
|
+
n_tup_hot_upd AS hot_updates,
|
|
11
|
+
n_live_tup AS live_rows,
|
|
12
|
+
n_dead_tup AS dead_rows,
|
|
13
|
+
last_vacuum,
|
|
14
|
+
last_autovacuum,
|
|
15
|
+
last_analyze,
|
|
16
|
+
last_autoanalyze
|
|
17
|
+
FROM pg_stat_user_tables
|
|
18
|
+
WHERE schemaname NOT IN ('pg_catalog', 'information_schema')
|
|
19
|
+
AND (n_tup_ins + n_tup_upd + n_tup_del) > 0
|
|
20
|
+
ORDER BY (n_tup_ins + n_tup_upd + n_tup_del) DESC;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
-- Table row counts
|
|
2
|
+
-- Estimated row counts from statistics
|
|
3
|
+
|
|
4
|
+
SELECT
|
|
5
|
+
schemaname AS schema,
|
|
6
|
+
relname AS table_name,
|
|
7
|
+
n_live_tup AS row_count,
|
|
8
|
+
n_dead_tup AS dead_rows,
|
|
9
|
+
pg_size_pretty(pg_table_size(relid)) AS table_size,
|
|
10
|
+
ROUND(pg_table_size(relid) / 1024.0 / 1024.0, 2) AS table_size_mb,
|
|
11
|
+
CASE
|
|
12
|
+
WHEN n_live_tup > 0 THEN
|
|
13
|
+
ROUND((pg_table_size(relid) / n_live_tup)::numeric, 0)
|
|
14
|
+
ELSE 0
|
|
15
|
+
END AS bytes_per_row
|
|
16
|
+
FROM pg_stat_user_tables
|
|
17
|
+
WHERE schemaname NOT IN ('pg_catalog', 'information_schema')
|
|
18
|
+
ORDER BY n_live_tup DESC;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
-- Sequential scan statistics
|
|
2
|
+
-- High sequential scans may indicate missing indexes
|
|
3
|
+
|
|
4
|
+
SELECT
|
|
5
|
+
schemaname AS schema,
|
|
6
|
+
relname AS table_name,
|
|
7
|
+
seq_scan,
|
|
8
|
+
seq_tup_read,
|
|
9
|
+
CASE
|
|
10
|
+
WHEN seq_scan > 0 THEN
|
|
11
|
+
ROUND((seq_tup_read / seq_scan)::numeric, 0)
|
|
12
|
+
ELSE 0
|
|
13
|
+
END AS rows_per_seq_scan,
|
|
14
|
+
idx_scan,
|
|
15
|
+
idx_tup_fetch,
|
|
16
|
+
CASE
|
|
17
|
+
WHEN seq_scan + COALESCE(idx_scan, 0) > 0 THEN
|
|
18
|
+
ROUND((seq_scan * 100.0 / (seq_scan + COALESCE(idx_scan, 0)))::numeric, 2)
|
|
19
|
+
ELSE 0
|
|
20
|
+
END AS seq_scan_ratio,
|
|
21
|
+
pg_size_pretty(pg_table_size(relid)) AS table_size,
|
|
22
|
+
n_live_tup AS row_count
|
|
23
|
+
FROM pg_stat_user_tables
|
|
24
|
+
WHERE schemaname NOT IN ('pg_catalog', 'information_schema')
|
|
25
|
+
AND seq_scan > 0
|
|
26
|
+
ORDER BY seq_tup_read DESC;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
-- Table sizes including indexes
|
|
2
|
+
-- Shows disk usage for each table
|
|
3
|
+
|
|
4
|
+
SELECT
|
|
5
|
+
schemaname AS schema,
|
|
6
|
+
relname AS table_name,
|
|
7
|
+
pg_size_pretty(pg_table_size(relid)) AS table_size,
|
|
8
|
+
ROUND(pg_table_size(relid) / 1024.0 / 1024.0, 2) AS table_size_mb,
|
|
9
|
+
pg_size_pretty(pg_indexes_size(relid)) AS index_size,
|
|
10
|
+
ROUND(pg_indexes_size(relid) / 1024.0 / 1024.0, 2) AS index_size_mb,
|
|
11
|
+
pg_size_pretty(pg_total_relation_size(relid)) AS total_size,
|
|
12
|
+
ROUND(pg_total_relation_size(relid) / 1024.0 / 1024.0, 2) AS total_size_mb,
|
|
13
|
+
n_live_tup AS row_count
|
|
14
|
+
FROM pg_stat_user_tables
|
|
15
|
+
WHERE schemaname NOT IN ('pg_catalog', 'information_schema')
|
|
16
|
+
ORDER BY pg_total_relation_size(relid) DESC;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
-- Tables needing vacuum
|
|
2
|
+
-- High dead row count or long time since last vacuum
|
|
3
|
+
|
|
4
|
+
SELECT
|
|
5
|
+
schemaname AS schema,
|
|
6
|
+
relname AS table_name,
|
|
7
|
+
n_live_tup,
|
|
8
|
+
n_dead_tup,
|
|
9
|
+
CASE
|
|
10
|
+
WHEN n_live_tup > 0 THEN
|
|
11
|
+
ROUND((n_dead_tup * 100.0 / n_live_tup)::numeric, 2)
|
|
12
|
+
ELSE 0
|
|
13
|
+
END AS dead_ratio_percent,
|
|
14
|
+
last_vacuum,
|
|
15
|
+
last_autovacuum,
|
|
16
|
+
last_analyze,
|
|
17
|
+
vacuum_count,
|
|
18
|
+
autovacuum_count,
|
|
19
|
+
pg_size_pretty(pg_table_size(relid)) AS table_size
|
|
20
|
+
FROM pg_stat_user_tables
|
|
21
|
+
WHERE schemaname NOT IN ('pg_catalog', 'information_schema')
|
|
22
|
+
ORDER BY n_dead_tup DESC;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PgReports
|
|
4
|
+
# Loads and caches SQL queries from files
|
|
5
|
+
module SqlLoader
|
|
6
|
+
SQL_DIR = File.expand_path("sql", __dir__)
|
|
7
|
+
|
|
8
|
+
class << self
|
|
9
|
+
def load(category, name)
|
|
10
|
+
cache_key = "#{category}/#{name}"
|
|
11
|
+
sql_cache[cache_key] ||= read_sql_file(category, name)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def clear_cache!
|
|
15
|
+
@sql_cache = {}
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def sql_cache
|
|
21
|
+
@sql_cache ||= {}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def read_sql_file(category, name)
|
|
25
|
+
path = File.join(SQL_DIR, category.to_s, "#{name}.sql")
|
|
26
|
+
|
|
27
|
+
unless File.exist?(path)
|
|
28
|
+
raise SqlFileNotFoundError, "SQL file not found: #{path}"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
File.read(path)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "tempfile"
|
|
4
|
+
|
|
5
|
+
module PgReports
|
|
6
|
+
# Sends reports to Telegram using telegram-bot-ruby gem
|
|
7
|
+
module TelegramSender
|
|
8
|
+
class << self
|
|
9
|
+
def send_message(text, parse_mode: "Markdown")
|
|
10
|
+
ensure_configured!
|
|
11
|
+
ensure_telegram_gem!
|
|
12
|
+
|
|
13
|
+
bot.api.send_message(
|
|
14
|
+
chat_id: chat_id,
|
|
15
|
+
text: truncate_message(text),
|
|
16
|
+
parse_mode: parse_mode
|
|
17
|
+
)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def send_file(content, filename:, caption: nil)
|
|
21
|
+
ensure_configured!
|
|
22
|
+
ensure_telegram_gem!
|
|
23
|
+
|
|
24
|
+
# Create a temporary file
|
|
25
|
+
temp_file = Tempfile.new([File.basename(filename, ".*"), File.extname(filename)])
|
|
26
|
+
begin
|
|
27
|
+
temp_file.write(content)
|
|
28
|
+
temp_file.rewind
|
|
29
|
+
|
|
30
|
+
bot.api.send_document(
|
|
31
|
+
chat_id: chat_id,
|
|
32
|
+
document: Faraday::UploadIO.new(temp_file.path, "text/plain", filename),
|
|
33
|
+
caption: caption&.truncate(1024)
|
|
34
|
+
)
|
|
35
|
+
ensure
|
|
36
|
+
temp_file.close
|
|
37
|
+
temp_file.unlink
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def send_report(report)
|
|
42
|
+
if report.to_text.length > 4000
|
|
43
|
+
send_file(report.to_text, filename: report_filename(report), caption: report.title)
|
|
44
|
+
else
|
|
45
|
+
send_message(report.to_markdown)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def bot
|
|
52
|
+
@bot ||= Telegram::Bot::Client.new(PgReports.config.telegram_bot_token)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def chat_id
|
|
56
|
+
PgReports.config.telegram_chat_id
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def ensure_configured!
|
|
60
|
+
unless PgReports.config.telegram_configured?
|
|
61
|
+
raise TelegramNotConfiguredError, "Telegram is not configured. Set telegram_bot_token and telegram_chat_id."
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def ensure_telegram_gem!
|
|
66
|
+
unless defined?(Telegram::Bot)
|
|
67
|
+
raise Error, "telegram-bot-ruby gem is not installed. Add it to your Gemfile: gem 'telegram-bot-ruby'"
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def truncate_message(text)
|
|
72
|
+
# Telegram message limit is 4096 characters
|
|
73
|
+
return text if text.length <= 4096
|
|
74
|
+
|
|
75
|
+
"#{text[0, 4000]}...\n\n_Message truncated. Use send_to_telegram_as_file for full report._"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def report_filename(report)
|
|
79
|
+
"#{report.title.parameterize}-#{report.generated_at.strftime("%Y%m%d-%H%M%S")}.txt"
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
data/lib/pg_reports.rb
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support"
|
|
4
|
+
require "active_support/core_ext"
|
|
5
|
+
require "active_record"
|
|
6
|
+
|
|
7
|
+
require_relative "pg_reports/version"
|
|
8
|
+
require_relative "pg_reports/error"
|
|
9
|
+
require_relative "pg_reports/configuration"
|
|
10
|
+
require_relative "pg_reports/sql_loader"
|
|
11
|
+
require_relative "pg_reports/executor"
|
|
12
|
+
require_relative "pg_reports/report"
|
|
13
|
+
require_relative "pg_reports/telegram_sender"
|
|
14
|
+
require_relative "pg_reports/annotation_parser"
|
|
15
|
+
|
|
16
|
+
# Modules
|
|
17
|
+
require_relative "pg_reports/modules/queries"
|
|
18
|
+
require_relative "pg_reports/modules/indexes"
|
|
19
|
+
require_relative "pg_reports/modules/tables"
|
|
20
|
+
require_relative "pg_reports/modules/connections"
|
|
21
|
+
require_relative "pg_reports/modules/system"
|
|
22
|
+
|
|
23
|
+
# Dashboard
|
|
24
|
+
require_relative "pg_reports/dashboard/reports_registry"
|
|
25
|
+
|
|
26
|
+
# Rails Engine
|
|
27
|
+
require_relative "pg_reports/engine" if defined?(Rails::Engine)
|
|
28
|
+
|
|
29
|
+
module PgReports
|
|
30
|
+
class << self
|
|
31
|
+
# Query analysis methods
|
|
32
|
+
delegate :slow_queries, :heavy_queries, :expensive_queries,
|
|
33
|
+
:missing_index_queries, :low_cache_hit_queries, :all_queries,
|
|
34
|
+
:reset_statistics!, to: Modules::Queries
|
|
35
|
+
|
|
36
|
+
# Index analysis methods
|
|
37
|
+
delegate :unused_indexes, :duplicate_indexes, :invalid_indexes,
|
|
38
|
+
:missing_indexes, :index_usage, :bloated_indexes, :index_sizes,
|
|
39
|
+
to: Modules::Indexes
|
|
40
|
+
|
|
41
|
+
# Table analysis methods
|
|
42
|
+
delegate :table_sizes, :bloated_tables, :vacuum_needed,
|
|
43
|
+
:row_counts, :cache_hit_ratios, :seq_scans, :recently_modified,
|
|
44
|
+
to: Modules::Tables
|
|
45
|
+
|
|
46
|
+
# Connection analysis methods
|
|
47
|
+
delegate :active_connections, :connection_stats, :long_running_queries,
|
|
48
|
+
:blocking_queries, :locks, :idle_connections,
|
|
49
|
+
:kill_connection, :cancel_query,
|
|
50
|
+
to: Modules::Connections
|
|
51
|
+
|
|
52
|
+
# System analysis methods
|
|
53
|
+
delegate :database_sizes, :settings, :extensions,
|
|
54
|
+
:activity_overview, :cache_stats, :pg_stat_statements_available?,
|
|
55
|
+
:pg_stat_statements_preloaded?, :pg_stat_statements_status,
|
|
56
|
+
:enable_pg_stat_statements!,
|
|
57
|
+
to: Modules::System
|
|
58
|
+
|
|
59
|
+
# Generate a comprehensive database health report
|
|
60
|
+
# @return [Report] Combined health report
|
|
61
|
+
def health_report
|
|
62
|
+
# Collect all reports
|
|
63
|
+
reports = {
|
|
64
|
+
"Slow Queries" => slow_queries(limit: 10),
|
|
65
|
+
"Expensive Queries" => expensive_queries(limit: 10),
|
|
66
|
+
"Unused Indexes" => unused_indexes(limit: 10),
|
|
67
|
+
"Tables Needing Vacuum" => vacuum_needed(limit: 10),
|
|
68
|
+
"Long Running Queries" => long_running_queries,
|
|
69
|
+
"Blocking Queries" => blocking_queries
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# Build combined data
|
|
73
|
+
combined_data = reports.map do |name, report|
|
|
74
|
+
{
|
|
75
|
+
"section" => name,
|
|
76
|
+
"items_count" => report.size,
|
|
77
|
+
"has_issues" => report.size.positive?
|
|
78
|
+
}
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
Report.new(
|
|
82
|
+
title: "Database Health Report",
|
|
83
|
+
data: combined_data,
|
|
84
|
+
columns: %w[section items_count has_issues]
|
|
85
|
+
)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Run all reports and send summary to Telegram
|
|
89
|
+
def send_health_report_to_telegram
|
|
90
|
+
health_report.send_to_telegram
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Shorthand for accessing modules directly
|
|
94
|
+
def queries
|
|
95
|
+
Modules::Queries
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def indexes
|
|
99
|
+
Modules::Indexes
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def tables
|
|
103
|
+
Modules::Tables
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def connections
|
|
107
|
+
Modules::Connections
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def system
|
|
111
|
+
Modules::System
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|