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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f0a634540a400676a49a8db9574fe8abbccd9faeae920e5d22323b1d54c791bd
4
+ data.tar.gz: ec2665323a7adbc2eae7dbcdd2a0b49ca5d7e4a96ca3f9603b1fd4304c23c98b
5
+ SHA512:
6
+ metadata.gz: ff18cbb7ffe10932d42b8a277fb33ef812a7d8d4211874575ea51869d9478c868143b7e912d639c493eb4ee81252b6a0c296aaaeefd132aa381bb6e5b99aef99
7
+ data.tar.gz: ef94bcbc4e686d1c82f6e6664735dbdbdbdbd441285be485c43fa593c69ba47886d16dd25998776ab7ef772382ec7e97d472fa34011b8683ee0c7d88881ec69b
data/CHANGELOG.md ADDED
@@ -0,0 +1,22 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.0] - 2026-01-17
9
+
10
+ ### Added
11
+
12
+ - Initial release
13
+ - Query analysis module (slow, heavy, expensive queries)
14
+ - Index analysis module (unused, duplicate, invalid, missing indexes)
15
+ - Table analysis module (sizes, bloat, vacuum status)
16
+ - Connection analysis module (active connections, locks, blocking queries)
17
+ - System module (database sizes, settings, extensions)
18
+ - Web dashboard with beautiful dark theme
19
+ - Expandable table rows for full query text
20
+ - Download reports in TXT, CSV, JSON formats
21
+ - Telegram integration for sending reports
22
+ - pg_stat_statements management (enable/status check)
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Your Name
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,335 @@
1
+ # PgReports
2
+
3
+ [![Ruby](https://img.shields.io/badge/Ruby-2.7%2B-red.svg)](https://www.ruby-lang.org/)
4
+ [![Rails](https://img.shields.io/badge/Rails-5.0%2B-red.svg)](https://rubyonrails.org/)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+
7
+ A comprehensive PostgreSQL monitoring and analysis library for Rails applications. Get insights into query performance, index usage, table statistics, connection health, and more. Includes a beautiful web dashboard and Telegram integration for notifications.
8
+
9
+ ![Dashboard Screenshot](docs/dashboard.png)
10
+
11
+ ## Features
12
+
13
+ - 📊 **Query Analysis** - Identify slow, heavy, and expensive queries using `pg_stat_statements`
14
+ - 📇 **Index Analysis** - Find unused, duplicate, invalid, and missing indexes
15
+ - 📋 **Table Statistics** - Monitor table sizes, bloat, vacuum needs, and cache hit ratios
16
+ - 🔌 **Connection Monitoring** - Track active connections, locks, and blocking queries
17
+ - 🖥️ **System Overview** - Database sizes, PostgreSQL settings, installed extensions
18
+ - 🌐 **Web Dashboard** - Beautiful dark-themed UI with expandable rows
19
+ - 📨 **Telegram Integration** - Send reports directly to Telegram
20
+ - 📥 **Export** - Download reports in TXT, CSV, or JSON format
21
+
22
+ ## Installation
23
+
24
+ Add to your Gemfile:
25
+
26
+ ```ruby
27
+ gem "pg_reports"
28
+
29
+ # Optional: for Telegram support
30
+ gem "telegram-bot-ruby"
31
+ ```
32
+
33
+ Run:
34
+
35
+ ```bash
36
+ bundle install
37
+ ```
38
+
39
+ ## Quick Start
40
+
41
+ ### Mount the Dashboard
42
+
43
+ Add to your `config/routes.rb`:
44
+
45
+ ```ruby
46
+ Rails.application.routes.draw do
47
+ # Mount in development only (recommended)
48
+ if Rails.env.development?
49
+ mount PgReports::Engine, at: "/pg_reports"
50
+ end
51
+
52
+ # Or with authentication
53
+ authenticate :user, ->(u) { u.admin? } do
54
+ mount PgReports::Engine, at: "/pg_reports"
55
+ end
56
+ end
57
+ ```
58
+
59
+ Visit `http://localhost:3000/pg_reports` to access the dashboard.
60
+
61
+ ### Use in Console or Code
62
+
63
+ ```ruby
64
+ # Get slow queries
65
+ PgReports.slow_queries.display
66
+
67
+ # Get unused indexes
68
+ report = PgReports.unused_indexes
69
+ report.each { |row| puts row["index_name"] }
70
+
71
+ # Export to different formats
72
+ report.to_text # Plain text
73
+ report.to_csv # CSV
74
+ report.to_a # Array of hashes
75
+
76
+ # Send to Telegram
77
+ PgReports.expensive_queries.send_to_telegram
78
+
79
+ # Health report
80
+ PgReports.health_report.display
81
+ ```
82
+
83
+ ## Configuration
84
+
85
+ Create an initializer `config/initializers/pg_reports.rb`:
86
+
87
+ ```ruby
88
+ PgReports.configure do |config|
89
+ # Telegram (optional)
90
+ config.telegram_bot_token = ENV["PG_REPORTS_TELEGRAM_TOKEN"]
91
+ config.telegram_chat_id = ENV["PG_REPORTS_TELEGRAM_CHAT_ID"]
92
+
93
+ # Query thresholds
94
+ config.slow_query_threshold_ms = 100 # Queries slower than this
95
+ config.heavy_query_threshold_calls = 1000 # Queries with more calls
96
+ config.expensive_query_threshold_ms = 10000 # Total time threshold
97
+
98
+ # Index thresholds
99
+ config.unused_index_threshold_scans = 50 # Index with fewer scans
100
+
101
+ # Table thresholds
102
+ config.bloat_threshold_percent = 20 # Tables with more bloat
103
+ config.dead_rows_threshold = 10000 # Dead rows needing vacuum
104
+
105
+ # Output settings
106
+ config.max_query_length = 200 # Truncate queries in text output
107
+
108
+ # Dashboard authentication (optional)
109
+ config.dashboard_auth = -> {
110
+ authenticate_or_request_with_http_basic do |user, pass|
111
+ user == "admin" && pass == "secret"
112
+ end
113
+ }
114
+
115
+ end
116
+ ```
117
+
118
+ ## Query Source Tracking
119
+
120
+ PgReports automatically parses query annotations to show **where queries originated**. Works with:
121
+
122
+ ### Marginalia (recommended)
123
+
124
+ If you use [marginalia](https://github.com/basecamp/marginalia), PgReports will automatically parse and display controller/action info in the **source** column.
125
+
126
+ ```ruby
127
+ # Gemfile
128
+ gem 'marginalia'
129
+ ```
130
+
131
+ ### Rails 7+ Query Logs
132
+
133
+ ```ruby
134
+ # config/application.rb
135
+ config.active_record.query_log_tags_enabled = true
136
+ config.active_record.query_log_tags = [:controller, :action]
137
+ ```
138
+
139
+ ## Available Reports
140
+
141
+ ### Queries (requires pg_stat_statements)
142
+
143
+ | Method | Description |
144
+ |--------|-------------|
145
+ | `slow_queries` | Queries with high mean execution time |
146
+ | `heavy_queries` | Most frequently called queries |
147
+ | `expensive_queries` | Queries consuming most total time |
148
+ | `missing_index_queries` | Queries potentially missing indexes |
149
+ | `low_cache_hit_queries` | Queries with poor cache utilization |
150
+ | `all_queries` | All query statistics |
151
+ | `reset_statistics!` | Reset pg_stat_statements data |
152
+
153
+ ### Indexes
154
+
155
+ | Method | Description |
156
+ |--------|-------------|
157
+ | `unused_indexes` | Indexes rarely or never scanned |
158
+ | `duplicate_indexes` | Redundant indexes |
159
+ | `invalid_indexes` | Indexes that failed to build |
160
+ | `missing_indexes` | Tables potentially missing indexes |
161
+ | `index_usage` | Index scan statistics |
162
+ | `bloated_indexes` | Indexes with high bloat |
163
+ | `index_sizes` | Index disk usage |
164
+
165
+ ### Tables
166
+
167
+ | Method | Description |
168
+ |--------|-------------|
169
+ | `table_sizes` | Table disk usage |
170
+ | `bloated_tables` | Tables with high dead tuple ratio |
171
+ | `vacuum_needed` | Tables needing vacuum |
172
+ | `row_counts` | Table row counts |
173
+ | `cache_hit_ratios` | Table cache statistics |
174
+ | `seq_scans` | Tables with high sequential scans |
175
+ | `recently_modified` | Tables with recent activity |
176
+
177
+ ### Connections
178
+
179
+ | Method | Description |
180
+ |--------|-------------|
181
+ | `active_connections` | Current database connections |
182
+ | `connection_stats` | Connection statistics by state |
183
+ | `long_running_queries` | Queries running for extended period |
184
+ | `blocking_queries` | Queries blocking others |
185
+ | `locks` | Current database locks |
186
+ | `idle_connections` | Idle connections |
187
+ | `kill_connection(pid)` | Terminate a backend process |
188
+ | `cancel_query(pid)` | Cancel a running query |
189
+
190
+ ### System
191
+
192
+ | Method | Description |
193
+ |--------|-------------|
194
+ | `database_sizes` | Size of all databases |
195
+ | `settings` | PostgreSQL configuration |
196
+ | `extensions` | Installed extensions |
197
+ | `activity_overview` | Current activity summary |
198
+ | `cache_stats` | Database cache statistics |
199
+ | `pg_stat_statements_available?` | Check if extension is ready |
200
+ | `enable_pg_stat_statements!` | Create the extension |
201
+
202
+ ## pg_stat_statements Setup
203
+
204
+ For query analysis, you need to enable `pg_stat_statements`:
205
+
206
+ 1. Edit `postgresql.conf`:
207
+ ```
208
+ shared_preload_libraries = 'pg_stat_statements'
209
+ pg_stat_statements.track = all
210
+ ```
211
+
212
+ 2. Restart PostgreSQL:
213
+ ```bash
214
+ sudo systemctl restart postgresql
215
+ ```
216
+
217
+ 3. Create extension (via dashboard or console):
218
+ ```ruby
219
+ PgReports.enable_pg_stat_statements!
220
+ ```
221
+
222
+ ## Report Object
223
+
224
+ Every method returns a `PgReports::Report` object:
225
+
226
+ ```ruby
227
+ report = PgReports.slow_queries
228
+
229
+ report.title # "Slow Queries (mean time >= 100ms)"
230
+ report.data # Array of hashes
231
+ report.columns # Column names
232
+ report.size # Row count
233
+ report.empty? # Boolean
234
+ report.generated_at # Timestamp
235
+
236
+ # Output formats
237
+ report.to_text # Plain text table
238
+ report.to_markdown # Markdown table
239
+ report.to_html # HTML table
240
+ report.to_csv # CSV
241
+ report.to_a # Raw data
242
+
243
+ # Actions
244
+ report.display # Print to STDOUT
245
+ report.send_to_telegram # Send as message
246
+ report.send_to_telegram_as_file # Send as file attachment
247
+
248
+ # Enumerable
249
+ report.each { |row| puts row }
250
+ report.map { |row| row["query"] }
251
+ report.select { |row| row["calls"] > 100 }
252
+ ```
253
+
254
+ ## Web Dashboard
255
+
256
+ The dashboard provides:
257
+
258
+ - 📊 Overview of all report categories
259
+ - ⚡ One-click report execution
260
+ - 🔍 Expandable rows for full query text
261
+ - 📋 Copy query to clipboard
262
+ - 📥 Download in multiple formats
263
+ - 📨 Send to Telegram
264
+ - 🔧 pg_stat_statements management
265
+
266
+ ### Authentication
267
+
268
+ ```ruby
269
+ PgReports.configure do |config|
270
+ # HTTP Basic Auth
271
+ config.dashboard_auth = -> {
272
+ authenticate_or_request_with_http_basic do |user, pass|
273
+ user == ENV["ADMIN_USER"] && pass == ENV["ADMIN_PASS"]
274
+ end
275
+ }
276
+
277
+ # Or use Devise
278
+ config.dashboard_auth = -> {
279
+ redirect_to main_app.root_path unless current_user&.admin?
280
+ }
281
+ end
282
+ ```
283
+
284
+ ## Telegram Integration
285
+
286
+ 1. Create a bot via [@BotFather](https://t.me/BotFather)
287
+ 2. Get your chat ID (add [@userinfobot](https://t.me/userinfobot) to get it)
288
+ 3. Configure:
289
+
290
+ ```ruby
291
+ PgReports.configure do |config|
292
+ config.telegram_bot_token = "123456:ABC-DEF..."
293
+ config.telegram_chat_id = "-1001234567890"
294
+ end
295
+ ```
296
+
297
+ 4. Send reports:
298
+
299
+ ```ruby
300
+ PgReports.slow_queries.send_to_telegram
301
+ PgReports.health_report.send_to_telegram_as_file
302
+ ```
303
+
304
+ ## Development
305
+
306
+ ```bash
307
+ # Clone the repo
308
+ git clone https://github.com/yourusername/pg_reports
309
+ cd pg_reports
310
+
311
+ # Install dependencies
312
+ bundle install
313
+
314
+ # Run tests
315
+ bundle exec rspec
316
+
317
+ # Run linter
318
+ bundle exec rubocop
319
+ ```
320
+
321
+ ## Contributing
322
+
323
+ 1. Fork it
324
+ 2. Create your feature branch (`git checkout -b feature/my-feature`)
325
+ 3. Commit your changes (`git commit -am 'Add my feature'`)
326
+ 4. Push to the branch (`git push origin feature/my-feature`)
327
+ 5. Create a Pull Request
328
+
329
+ ## License
330
+
331
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
332
+
333
+ ## Acknowledgments
334
+
335
+ Inspired by [rails-pg-extras](https://github.com/pawurb/rails-pg-extras) and built with ❤️ for the Rails community.
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgReports
4
+ class DashboardController < ActionController::Base
5
+ layout "pg_reports/application"
6
+
7
+ before_action :authenticate_dashboard!, if: -> { PgReports.config.dashboard_auth.present? }
8
+ before_action :set_categories
9
+
10
+ def index
11
+ @pg_stat_status = PgReports.pg_stat_statements_status
12
+ end
13
+
14
+ def enable_pg_stat_statements
15
+ result = PgReports.enable_pg_stat_statements!
16
+ render json: result
17
+ end
18
+
19
+ def reset_statistics
20
+ PgReports.reset_statistics!
21
+ render json: {success: true, message: "Statistics have been reset successfully"}
22
+ rescue => e
23
+ render json: {success: false, error: e.message}, status: :unprocessable_entity
24
+ end
25
+
26
+ def show
27
+ @category = params[:category].to_sym
28
+ @report_key = params[:report].to_sym
29
+ @report_info = Dashboard::ReportsRegistry.find(@category, @report_key)
30
+
31
+ if @report_info.nil?
32
+ redirect_to root_path, alert: "Report not found"
33
+ return
34
+ end
35
+
36
+ @report = execute_report(@category, @report_key)
37
+ rescue => e
38
+ @error = e.message
39
+ @report = nil
40
+ end
41
+
42
+ def run
43
+ category = params[:category].to_sym
44
+ report_key = params[:report].to_sym
45
+
46
+ report = execute_report(category, report_key)
47
+
48
+ render json: {
49
+ success: true,
50
+ title: report.title,
51
+ columns: report.columns,
52
+ data: report.data.first(100),
53
+ total: report.size,
54
+ generated_at: report.generated_at.strftime("%Y-%m-%d %H:%M:%S")
55
+ }
56
+ rescue => e
57
+ render json: {success: false, error: e.message}, status: :unprocessable_entity
58
+ end
59
+
60
+ def send_to_telegram
61
+ category = params[:category].to_sym
62
+ report_key = params[:report].to_sym
63
+
64
+ report = execute_report(category, report_key)
65
+
66
+ if report.size > 50
67
+ report.send_to_telegram_as_file
68
+ else
69
+ report.send_to_telegram
70
+ end
71
+
72
+ render json: {success: true, message: "Report sent to Telegram"}
73
+ rescue => e
74
+ render json: {success: false, error: e.message}, status: :unprocessable_entity
75
+ end
76
+
77
+ def download
78
+ category = params[:category].to_sym
79
+ report_key = params[:report].to_sym
80
+ format_type = params[:format] || "txt"
81
+
82
+ report = execute_report(category, report_key)
83
+ filename = "#{report.title.parameterize}-#{Time.current.strftime("%Y%m%d-%H%M%S")}"
84
+
85
+ case format_type
86
+ when "csv"
87
+ send_data report.to_csv,
88
+ filename: "#{filename}.csv",
89
+ type: "text/csv; charset=utf-8",
90
+ disposition: "attachment"
91
+ when "json"
92
+ send_data report.to_a.to_json,
93
+ filename: "#{filename}.json",
94
+ type: "application/json; charset=utf-8",
95
+ disposition: "attachment"
96
+ else
97
+ send_data report.to_text,
98
+ filename: "#{filename}.txt",
99
+ type: "text/plain; charset=utf-8",
100
+ disposition: "attachment"
101
+ end
102
+ rescue => e
103
+ render json: {success: false, error: e.message}, status: :unprocessable_entity
104
+ end
105
+
106
+ private
107
+
108
+ def authenticate_dashboard!
109
+ instance_exec(&PgReports.config.dashboard_auth)
110
+ end
111
+
112
+ def set_categories
113
+ @categories = Dashboard::ReportsRegistry.all
114
+ end
115
+
116
+ def execute_report(category, report_key)
117
+ mod = case category
118
+ when :queries then Modules::Queries
119
+ when :indexes then Modules::Indexes
120
+ when :tables then Modules::Tables
121
+ when :connections then Modules::Connections
122
+ when :system then Modules::System
123
+ else raise ArgumentError, "Unknown category: #{category}"
124
+ end
125
+
126
+ unless mod.respond_to?(report_key)
127
+ raise ArgumentError, "Unknown report: #{report_key}"
128
+ end
129
+
130
+ mod.public_send(report_key)
131
+ end
132
+ end
133
+ end