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.
Files changed (57) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +22 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +335 -0
  5. data/app/controllers/pg_reports/dashboard_controller.rb +133 -0
  6. data/app/views/layouts/pg_reports/application.html.erb +594 -0
  7. data/app/views/pg_reports/dashboard/index.html.erb +435 -0
  8. data/app/views/pg_reports/dashboard/show.html.erb +481 -0
  9. data/config/routes.rb +13 -0
  10. data/lib/pg_reports/annotation_parser.rb +114 -0
  11. data/lib/pg_reports/configuration.rb +83 -0
  12. data/lib/pg_reports/dashboard/reports_registry.rb +89 -0
  13. data/lib/pg_reports/engine.rb +22 -0
  14. data/lib/pg_reports/error.rb +15 -0
  15. data/lib/pg_reports/executor.rb +51 -0
  16. data/lib/pg_reports/modules/connections.rb +106 -0
  17. data/lib/pg_reports/modules/indexes.rb +111 -0
  18. data/lib/pg_reports/modules/queries.rb +140 -0
  19. data/lib/pg_reports/modules/system.rb +148 -0
  20. data/lib/pg_reports/modules/tables.rb +113 -0
  21. data/lib/pg_reports/report.rb +228 -0
  22. data/lib/pg_reports/sql/connections/active_connections.sql +20 -0
  23. data/lib/pg_reports/sql/connections/blocking_queries.sql +35 -0
  24. data/lib/pg_reports/sql/connections/connection_stats.sql +13 -0
  25. data/lib/pg_reports/sql/connections/idle_connections.sql +19 -0
  26. data/lib/pg_reports/sql/connections/locks.sql +20 -0
  27. data/lib/pg_reports/sql/connections/long_running_queries.sql +21 -0
  28. data/lib/pg_reports/sql/indexes/bloated_indexes.sql +36 -0
  29. data/lib/pg_reports/sql/indexes/duplicate_indexes.sql +38 -0
  30. data/lib/pg_reports/sql/indexes/index_sizes.sql +14 -0
  31. data/lib/pg_reports/sql/indexes/index_usage.sql +19 -0
  32. data/lib/pg_reports/sql/indexes/invalid_indexes.sql +15 -0
  33. data/lib/pg_reports/sql/indexes/missing_indexes.sql +27 -0
  34. data/lib/pg_reports/sql/indexes/unused_indexes.sql +18 -0
  35. data/lib/pg_reports/sql/queries/all_queries.sql +20 -0
  36. data/lib/pg_reports/sql/queries/expensive_queries.sql +22 -0
  37. data/lib/pg_reports/sql/queries/heavy_queries.sql +17 -0
  38. data/lib/pg_reports/sql/queries/low_cache_hit_queries.sql +19 -0
  39. data/lib/pg_reports/sql/queries/missing_index_queries.sql +25 -0
  40. data/lib/pg_reports/sql/queries/slow_queries.sql +17 -0
  41. data/lib/pg_reports/sql/system/activity_overview.sql +29 -0
  42. data/lib/pg_reports/sql/system/cache_stats.sql +19 -0
  43. data/lib/pg_reports/sql/system/database_sizes.sql +10 -0
  44. data/lib/pg_reports/sql/system/extensions.sql +12 -0
  45. data/lib/pg_reports/sql/system/settings.sql +33 -0
  46. data/lib/pg_reports/sql/tables/bloated_tables.sql +23 -0
  47. data/lib/pg_reports/sql/tables/cache_hit_ratios.sql +24 -0
  48. data/lib/pg_reports/sql/tables/recently_modified.sql +20 -0
  49. data/lib/pg_reports/sql/tables/row_counts.sql +18 -0
  50. data/lib/pg_reports/sql/tables/seq_scans.sql +26 -0
  51. data/lib/pg_reports/sql/tables/table_sizes.sql +16 -0
  52. data/lib/pg_reports/sql/tables/vacuum_needed.sql +22 -0
  53. data/lib/pg_reports/sql_loader.rb +35 -0
  54. data/lib/pg_reports/telegram_sender.rb +83 -0
  55. data/lib/pg_reports/version.rb +5 -0
  56. data/lib/pg_reports.rb +114 -0
  57. 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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgReports
4
+ VERSION = "0.1.0"
5
+ 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