pg_insights 0.1.0 → 0.2.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3575be5a5a333d43c44c555f5f736c83e5458788143d92b5ca5ec765e3cbd083
4
- data.tar.gz: be967ed421de1a002b52ef7b476fd58a02c866e6cf429ddeef053672831679a5
3
+ metadata.gz: 8d38a7931c85980d77ac00274e9fd20d0aeeccea9968fcdce682e094c2fa8859
4
+ data.tar.gz: 46ace0d92f363030da888b4fc3c0b81ba3429d6d46be11ed884e87cf953d982e
5
5
  SHA512:
6
- metadata.gz: b1756de4bb9c8aa2bceab70f936d79a85d678376917f0976bf2b9fa90ef4cc54fe4683387166ba6b93fe2e72f50d879a7185c89f292a87b60f782e92790c38df
7
- data.tar.gz: 17594af5eeeb6ed8812a6a7659635ce5efa032c6f67bb882fd8caf52dc617fe54b2dd2773c6883047be386c736f0826d6224cece06ebe06b0c6e0a5e49a2203c
6
+ metadata.gz: 72dc38b44223c63b08cf4a3021f53d96dca26c91a00447a92c9434f012add4f2693d02217fcbcc3d5a1a7117584ecab9b5248880a1510220ec24879591ec8b46
7
+ data.tar.gz: 54bfd327be06ff7b2032fbd937e5bd9f1b1ee32042349d58a34efe4025ffcc650e4d219586c0afd116cb21113ca28b3cb43d07d9e2fe8bbf85f717935d648770
data/README.md CHANGED
@@ -1,39 +1,113 @@
1
- # PgInsights
1
+ # 📊 PgInsights
2
2
 
3
- **PostgreSQL performance monitoring for Rails apps**
3
+ > PostgreSQL performance monitoring for Rails apps
4
+
5
+ <div align="center">
4
6
 
5
7
  [![Gem Version](https://badge.fury.io/rb/pg_insights.svg)](https://badge.fury.io/rb/pg_insights)
6
8
  [![CI](https://github.com/mezbahalam/pg_insights/actions/workflows/ci.yml/badge.svg)](https://github.com/mezbahalam/pg_insights/actions/workflows/ci.yml)
7
9
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
8
10
 
11
+ </div>
12
+
13
+ ---
14
+
9
15
  PgInsights is a Rails engine that gives you a web dashboard for monitoring your PostgreSQL database performance. Think of it as a lightweight alternative to external monitoring tools that lives right inside your Rails app.
10
16
 
11
- ## Why I built this
17
+ ## 🤔 Why I built this
12
18
 
13
- I got tired of switching between different tools to check database performance. Sometimes you just want to quickly see which indexes aren't being used, or find slow queries without setting up a whole monitoring infrastructure. PgInsights gives you that - a simple dashboard you can access at `/pg_insights` in your Rails app.
19
+ I got tired of switching between different tools to check database performance. Sometimes you just want to quickly see which indexes aren't being used, or find slow queries without setting up a whole monitoring infrastructure.
14
20
 
15
- ## What you get
21
+ PgInsights gives you that - a simple dashboard you can access at `/pg_insights` in your Rails app.
16
22
 
17
- **Health Dashboard**
23
+ ## 🎁 What you get
24
+
25
+ <table>
26
+ <tr>
27
+ <td width="50%">
28
+
29
+ ### 🏥 Health Dashboard
18
30
  - Find unused indexes that are wasting space
19
31
  - Spot tables that might need indexes (high sequential scans)
20
32
  - Identify slow queries (if you have pg_stat_statements enabled)
21
33
  - Check for table bloat that needs cleanup
22
34
  - Review PostgreSQL configuration settings
23
35
 
24
- **Query Runner**
36
+ </td>
37
+ <td width="50%">
38
+
39
+ ### 🔍 Query Runner
25
40
  - Run your own SELECT queries safely
26
41
  - Built-in queries for common performance checks
27
42
  - Save queries you use frequently
28
43
  - Results displayed as tables or charts
29
44
 
30
- **Smart execution**
45
+ </td>
46
+ </tr>
47
+ <tr>
48
+ <td colspan="2">
49
+
50
+ ### 📈 Timeline & Monitoring
51
+ - **Database snapshots**: Automatic collection of performance metrics and configuration parameters
52
+ - **Parameter change tracking**: Detect when PostgreSQL settings are modified over time
53
+ - **Performance trends**: Monitor cache hit rates, query times, and connection counts
54
+ - **Historical comparisons**: Compare database state between any two time periods
55
+ - **Export capabilities**: Download timeline data as CSV or JSON for further analysis
56
+ - **Configurable retention**: Automatic cleanup of old snapshots based on retention policy
57
+
58
+ </td>
59
+ </tr>
60
+ </table>
61
+
62
+ ### ⚡️ Smart execution
31
63
  - Runs health checks in background jobs if you have them set up
32
64
  - Falls back to running directly if you don't
33
65
  - Caches results so repeated visits are fast
34
66
  - Configurable timeouts to prevent slow queries from hanging
35
67
 
36
- ## Installation
68
+ ## 📸 Screenshots
69
+
70
+ <details>
71
+ <summary>👀 Click to see the interface</summary>
72
+
73
+ ### Health Dashboard
74
+ Monitor your PostgreSQL database performance and identify potential issues at a glance.
75
+
76
+ ![Health Dashboard](health_dashboard.png)
77
+
78
+ ### Query Runner
79
+ Run custom queries and visualize results with built-in charting capabilities.
80
+
81
+ ![Query Runner](query_runner.png)
82
+
83
+ ### Timeline Dashboard
84
+ Track database configuration changes and performance trends over time.
85
+
86
+ ![Timeline Dashboard](timeline_dashboard.png)
87
+
88
+ ### Snapshot Details
89
+ View detailed information about individual database snapshots including parameters and metadata.
90
+
91
+ ![Snapshot Details](snapshot_details.png)
92
+
93
+ ### Performance Metrics
94
+ Monitor key performance indicators and database health metrics across time periods.
95
+
96
+ ![Performance Metrics](snapshot_performance.png)
97
+
98
+ ### Snapshot Comparison
99
+ Compare database configurations and performance metrics between different time periods.
100
+
101
+ ![Snapshot Comparison](snapshot_comparison.png)
102
+
103
+ ### Side-by-Side Comparison
104
+ Detailed comparison view showing parameter changes and performance differences.
105
+
106
+ ![Compare Snapshots](compare_snapshots.png)
107
+
108
+ </details>
109
+
110
+ ## ⏩ Quick Start
37
111
 
38
112
  Add to your Gemfile:
39
113
 
@@ -51,45 +125,47 @@ rails db:migrate
51
125
 
52
126
  That's it. Visit `/pg_insights` in your browser.
53
127
 
54
- ## Configuration
128
+ ## ⚙️ Configuration
55
129
 
56
130
  The engine works out of the box, but you can customize it:
57
131
 
58
132
  ```ruby
59
133
  # config/initializers/pg_insights.rb
60
134
  PgInsights.configure do |config|
61
- # Run health checks in background (default: true)
135
+ # === Background Jobs ===
62
136
  config.enable_background_jobs = true
137
+ config.background_job_queue = :pg_insights_health
63
138
 
64
- # How long to cache results (default: 5 minutes)
65
- config.health_cache_expiry = 10.minutes
139
+ # === Health Check Settings ===
140
+ config.health_cache_expiry = 5.minutes
141
+ config.health_check_timeout = 10.seconds
66
142
 
67
- # Timeout for health check queries (default: 10 seconds)
68
- config.health_check_timeout = 15.seconds
69
-
70
- # Queue name for background jobs (default: :pg_insights_health)
71
- config.background_job_queue = :low_priority
143
+ # === Timeline & Snapshot Settings ===
144
+ config.enable_snapshots = true # Enable timeline feature
145
+ config.snapshot_frequency = 1.day # How often to collect snapshots
146
+ config.snapshot_retention_days = 90 # How long to keep snapshots
147
+ config.snapshot_collection_enabled = true # Master switch for snapshot collection
72
148
  end
73
149
  ```
74
150
 
75
- ## How Background Jobs Work
151
+ ## 🔄 How Background Jobs Work
76
152
 
77
- **PgInsights uses on-demand background jobs, not automatic scheduling.**
153
+ > **Note:** PgInsights uses on-demand background jobs, not automatic scheduling.
78
154
 
79
- ### When health checks run:
80
- - ✅ **When you visit the health dashboard `/pg_insights/health`** and cached data is older than `health_cache_expiry` (default: 5 minutes)
81
- - ✅ **When you click the "Refresh" button** in the dashboard
82
- - ✅ **When you run** `rails pg_insights:health_check` manually
155
+ **When health checks run:**
156
+ - ✅ When you visit the health dashboard `/pg_insights/health` and cached data is older than `health_cache_expiry` (default: 5 minutes)
157
+ - ✅ When you click the "Refresh" button in the dashboard
158
+ - ✅ When you run `rails pg_insights:health_check` manually
83
159
  - ❌ **NOT automatically** - PgInsights doesn't run background jobs on its own
84
160
 
85
- ### How caching works:
161
+ **How caching works:**
86
162
  ```
87
163
  Visit at 2:00 PM → Runs health checks, caches results for 5 minutes
88
- Visit at 2:03 PM → Uses cached results (still fresh)
164
+ Visit at 2:03 PM → Uses cached results (still fresh)
89
165
  Visit at 2:06 PM → Data is stale, triggers new background jobs
90
166
  ```
91
167
 
92
- ### Background job setup (optional but recommended):
168
+ **Background job setup (optional but recommended):**
93
169
 
94
170
  If your app has background jobs (Sidekiq, Resque, etc.), PgInsights will use them for better performance:
95
171
 
@@ -101,7 +177,7 @@ rails pg_insights:status
101
177
  **Without background jobs**: Health checks run synchronously when you visit the page (slower but works)
102
178
  **With background jobs**: Health checks run asynchronously (faster, non-blocking)
103
179
 
104
- ### Optional: Automatic recurring checks
180
+ **Optional: Automatic recurring checks**
105
181
 
106
182
  If you want health checks to run automatically (not just on-demand), set up a scheduler:
107
183
 
@@ -121,51 +197,70 @@ Sidekiq::Cron::Job.create(
121
197
 
122
198
  **Note**: Even with automatic scheduling, the jobs are smart - they only run expensive queries if the cached data is actually stale.
123
199
 
124
- ## Usage
200
+ ## 💻 Usage
125
201
 
126
202
  Navigate to `/pg_insights` in your app. The interface is pretty straightforward:
127
203
 
128
- - **Main page**: Run queries and see results as tables or charts
129
- - **Health tab**: Database performance overview
130
- - **Query examples**: Pre-built queries for common checks
204
+ | Page | What it does |
205
+ |------|-------------|
206
+ | **Query Runner** | Run custom queries and see results as tables or charts |
207
+ | **Health Dashboard** | Real-time database performance overview and issue detection |
208
+ | **Timeline** | Historical tracking of database performance metrics and configuration changes |
131
209
 
132
210
  All queries are read-only (SELECT statements only) and have timeouts to prevent issues.
133
211
 
134
- ## Available rake tasks
212
+ ## 🛠️ Available rake tasks
135
213
 
136
214
  ```bash
137
- rails pg_insights:status # Check configuration
138
- rails pg_insights:health_check # Run health checks manually
139
- rails pg_insights:stats # Show usage statistics
140
- rails pg_insights:clear_data # Clear stored data and caches
215
+ # Configuration & Status
216
+ rails pg_insights:status # Check configuration and background job status
217
+ rails pg_insights:test_jobs # Test background job functionality
218
+
219
+ # Health Checks
220
+ rails pg_insights:health_check # Run health checks manually (synchronous)
221
+ rails pg_insights:stats # Show usage statistics
222
+
223
+ # Timeline & Snapshots
224
+ rails pg_insights:collect_snapshot # Collect a database snapshot immediately
225
+ rails pg_insights:start_snapshots # Start recurring snapshot collection
226
+ rails pg_insights:snapshot_status # Check snapshot configuration and status
227
+ rails pg_insights:cleanup_snapshots # Clean up old snapshots
228
+
229
+ # Data Management
230
+ rails pg_insights:reset # Reset all PgInsights data (queries + health checks)
231
+ rails pg_insights:clear_data # Alias for reset (backward compatibility)
232
+ rails pg_insights:cleanup # Clean up old health check results (30+ days)
233
+
234
+ # Development & Testing
235
+ rails pg_insights:seed_timeline # Generate fake timeline data for testing
236
+ rails pg_insights:sample_data # Generate sample health check data
141
237
  ```
142
238
 
143
- ## Safety
239
+ ## 🔒 Safety
144
240
 
145
241
  - Only SELECT queries allowed
146
242
  - Query timeouts prevent long-running queries
147
243
  - Focuses on public schema by default
148
244
  - No modification of your data
149
245
 
150
- ## Uninstalling
246
+ ## 🗑️ Uninstalling
151
247
 
152
248
  ```bash
153
249
  rails generate pg_insights:clean
154
- rails db:rollback STEP=2
155
250
  # Remove gem from Gemfile
156
251
  ```
157
252
 
158
- ## Requirements
253
+ ## 📋 Requirements
159
254
 
160
255
  - Rails 6.1+
161
256
  - PostgreSQL
162
257
  - For slow query detection: pg_stat_statements extension (optional)
163
258
 
164
- ## Contributing
259
+ ## 🤝 Contributing
165
260
 
166
261
  Found a bug or have an idea? Open an issue or send a pull request. The codebase is pretty straightforward.
167
262
 
168
- Development setup:
263
+ **Development setup:**
169
264
 
170
265
  ```bash
171
266
  git clone https://github.com/mezbahalam/pg_insights.git
@@ -174,10 +269,14 @@ bundle install
174
269
  bundle exec rake spec
175
270
  ```
176
271
 
177
- ## License
272
+ ## 📄 License
178
273
 
179
274
  MIT License. See [LICENSE](MIT-LICENSE) file.
180
275
 
181
276
  ---
182
277
 
183
- Built by [Mezbah Alam](https://github.com/mezbahalam). Inspired by pg_hero and other database monitoring tools.
278
+ <div align="center">
279
+
280
+ Built by [Mezbah Alam](https://github.com/mezbahalam) • Inspired by pg_hero and other database monitoring tools
281
+
282
+ </div>
@@ -0,0 +1,263 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgInsights
4
+ class TimelineController < ApplicationController
5
+ layout "pg_insights/application"
6
+
7
+ def index
8
+ unless PgInsights.snapshots_available?
9
+ flash[:notice] = "Database snapshots are not enabled. Configure PgInsights.enable_snapshots = true to use timeline features."
10
+ redirect_to health_path
11
+ return
12
+ end
13
+
14
+ @snapshots = HealthCheckResult.snapshots(90)
15
+ @parameter_changes = HealthCheckResult.detect_parameter_changes_since(30)
16
+ @timeline_data = HealthCheckResult.timeline_data(30)
17
+ @stats = calculate_timeline_stats(@snapshots)
18
+ end
19
+
20
+ def show
21
+ @snapshot = HealthCheckResult.find(params[:id])
22
+
23
+ unless @snapshot.check_type == "database_snapshot"
24
+ flash[:error] = "Invalid snapshot"
25
+ redirect_to timeline_path
26
+ return
27
+ end
28
+
29
+ @previous_snapshot = HealthCheckResult.snapshots
30
+ .where("executed_at < ?", @snapshot.executed_at)
31
+ .first
32
+
33
+ if @previous_snapshot
34
+ @parameter_changes = HealthCheckResult.compare_snapshots(@previous_snapshot, @snapshot)
35
+ @performance_comparison = compare_performance_metrics(@previous_snapshot, @snapshot)
36
+ end
37
+ end
38
+
39
+ def compare
40
+ @date1 = Date.parse(params[:date1]) rescue nil
41
+ @date2 = Date.parse(params[:date2]) rescue nil
42
+
43
+ unless @date1 && @date2
44
+ flash[:error] = "Invalid dates provided"
45
+ redirect_to timeline_path
46
+ return
47
+ end
48
+
49
+ @snapshot1 = HealthCheckResult.find_snapshot_by_date(@date1)
50
+ @snapshot2 = HealthCheckResult.find_snapshot_by_date(@date2)
51
+
52
+ unless @snapshot1 && @snapshot2
53
+ flash[:error] = "Could not find snapshots for the selected dates"
54
+ redirect_to timeline_path
55
+ return
56
+ end
57
+
58
+ @parameter_changes = HealthCheckResult.compare_snapshots(@snapshot1, @snapshot2)
59
+ performance_metrics = compare_performance_metrics(@snapshot1, @snapshot2)
60
+ @performance_comparison = {
61
+ metrics: performance_metrics.transform_values do |data|
62
+ data.merge(difference: data[:change])
63
+ end
64
+ }
65
+ @metadata_comparison = compare_metadata(@snapshot1, @snapshot2)
66
+ @configuration_comparison = @parameter_changes
67
+
68
+ @comparison_data = {
69
+ snapshot1: @snapshot1,
70
+ snapshot2: @snapshot2,
71
+ parameters: @parameter_changes,
72
+ performance: @performance_comparison,
73
+ metadata: @metadata_comparison
74
+ }
75
+ end
76
+
77
+ def export
78
+ format = params[:format]&.downcase || "csv"
79
+ days = params[:days]&.to_i || 30
80
+
81
+ snapshots = HealthCheckResult.snapshots(days + 30)
82
+
83
+ case format
84
+ when "csv"
85
+ send_data generate_csv_export(snapshots),
86
+ filename: "pg_insights_timeline_#{Date.current}.csv",
87
+ type: "text/csv",
88
+ disposition: "attachment"
89
+ when "json"
90
+ send_data generate_json_export(snapshots),
91
+ filename: "pg_insights_timeline_#{Date.current}.json",
92
+ type: "application/json",
93
+ disposition: "attachment"
94
+ else
95
+ flash[:error] = "Unsupported export format. Use 'csv' or 'json'."
96
+ redirect_to timeline_path
97
+ end
98
+ end
99
+
100
+ def refresh
101
+ if PgInsights.snapshots_available? && PgInsights.background_jobs_available?
102
+ if PgInsights::DatabaseSnapshotJob.perform_later
103
+ render json: { message: "Snapshot collection started" }
104
+ else
105
+ render json: { error: "Failed to start snapshot collection" }, status: 422
106
+ end
107
+ else
108
+ begin
109
+ HealthCheckService.execute_and_cache_check("database_snapshot")
110
+ render json: { message: "Snapshot collected successfully" }
111
+ rescue => e
112
+ render json: { error: "Snapshot collection failed: #{e.message}" }, status: 422
113
+ end
114
+ end
115
+ end
116
+
117
+ def status
118
+ snapshot_status = {
119
+ enabled: PgInsights.snapshots_available?,
120
+ frequency: PgInsights.snapshot_frequency,
121
+ retention_days: PgInsights.snapshot_retention_days,
122
+ latest_snapshot: HealthCheckResult.latest_snapshot&.executed_at,
123
+ total_snapshots: HealthCheckResult.snapshots.count,
124
+ configuration_valid: PgInsights::DatabaseSnapshotJob.validate_configuration
125
+ }
126
+
127
+ render json: snapshot_status
128
+ end
129
+
130
+ private
131
+
132
+ def calculate_timeline_stats(snapshots)
133
+ return {} if snapshots.empty?
134
+
135
+ latest = snapshots.first
136
+ oldest = snapshots.last
137
+
138
+ cache_hit_rate = latest.result_data.dig("metrics", "cache_hit_rate")
139
+ numeric_cache_hit_rate = cache_hit_rate ? cache_hit_rate.to_f : nil
140
+
141
+ {
142
+ total_snapshots: snapshots.count,
143
+ date_range: {
144
+ from: oldest.executed_at.to_date,
145
+ to: latest.executed_at.to_date
146
+ },
147
+ latest_cache_hit_rate: numeric_cache_hit_rate,
148
+ parameter_changes_count: @parameter_changes.sum { |change| change[:changes].count }
149
+ }
150
+ end
151
+
152
+ def compare_performance_metrics(snapshot1, snapshot2)
153
+ metrics1 = snapshot1.result_data["metrics"] || {}
154
+ metrics2 = snapshot2.result_data["metrics"] || {}
155
+
156
+ comparison = {}
157
+
158
+ %w[cache_hit_rate avg_query_time p95_query_time bloated_tables
159
+ total_connections active_connections high_seq_scan_tables].each do |metric|
160
+ val1 = metrics1[metric]
161
+ val2 = metrics2[metric]
162
+
163
+ if val1 && val2
164
+ num_val1 = val1.to_f
165
+ num_val2 = val2.to_f
166
+
167
+ change = num_val2 - num_val1
168
+ change_pct = num_val1 != 0 ? (change / num_val1) * 100 : 0
169
+
170
+ comparison[metric] = {
171
+ before: num_val1,
172
+ after: num_val2,
173
+ change: change.round(2),
174
+ change_percent: change_pct.round(2),
175
+ direction: change > 0 ? "increase" : (change < 0 ? "decrease" : "stable")
176
+ }
177
+ end
178
+ end
179
+
180
+ comparison
181
+ end
182
+
183
+ def compare_metadata(snapshot1, snapshot2)
184
+ meta1 = snapshot1.result_data["metadata"] || {}
185
+ meta2 = snapshot2.result_data["metadata"] || {}
186
+
187
+ {
188
+ postgres_version_changed: meta1["postgresql_version"] != meta2["postgresql_version"],
189
+ extensions_added: (meta2["extensions"] || []) - (meta1["extensions"] || []),
190
+ extensions_removed: (meta1["extensions"] || []) - (meta2["extensions"] || []),
191
+ database_size: {
192
+ before: meta1["database_size"],
193
+ after: meta2["database_size"]
194
+ },
195
+ table_count: {
196
+ before: meta1["table_count"],
197
+ after: meta2["table_count"]
198
+ },
199
+ index_count: {
200
+ before: meta1["index_count"],
201
+ after: meta2["index_count"]
202
+ }
203
+ }
204
+ end
205
+
206
+ def generate_csv_export(snapshots)
207
+ require "csv"
208
+
209
+ CSV.generate(headers: true) do |csv|
210
+ csv << [
211
+ "Date", "Time", "Cache Hit Rate %", "Avg Query Time (ms)", "P95 Query Time (ms)",
212
+ "Bloated Tables", "Total Connections", "Active Connections", "High Seq Scan Tables",
213
+ "Database Size", "PostgreSQL Version"
214
+ ]
215
+
216
+ snapshots.each do |snapshot|
217
+ metrics = snapshot.result_data["metrics"] || {}
218
+ metadata = snapshot.result_data["metadata"] || {}
219
+
220
+ csv << [
221
+ snapshot.executed_at.strftime("%Y-%m-%d"),
222
+ snapshot.executed_at.strftime("%H:%M:%S"),
223
+ metrics["cache_hit_rate"]&.to_f,
224
+ metrics["avg_query_time"]&.to_f,
225
+ metrics["p95_query_time"]&.to_f,
226
+ metrics["bloated_tables"]&.to_i,
227
+ metrics["total_connections"]&.to_i,
228
+ metrics["active_connections"]&.to_i,
229
+ metrics["high_seq_scan_tables"]&.to_i,
230
+ metadata["database_size"],
231
+ metadata["postgres_version"]&.split(" ")&.first
232
+ ]
233
+ end
234
+ end
235
+ end
236
+
237
+ def generate_json_export(snapshots)
238
+ {
239
+ exported_at: Time.current.iso8601,
240
+ export_info: {
241
+ snapshot_count: snapshots.size,
242
+ date_range: {
243
+ from: snapshots.last&.executed_at,
244
+ to: snapshots.first&.executed_at
245
+ },
246
+ frequency: PgInsights.snapshot_frequency.to_s,
247
+ retention_days: PgInsights.snapshot_retention_days
248
+ },
249
+ parameter_changes: HealthCheckResult.detect_parameter_changes_since(30),
250
+ snapshots: snapshots.map do |snapshot|
251
+ {
252
+ id: snapshot.id,
253
+ collected_at: snapshot.executed_at.iso8601,
254
+ execution_time_ms: snapshot.execution_time_ms,
255
+ parameters: snapshot.result_data["parameters"],
256
+ metrics: snapshot.result_data["metrics"],
257
+ metadata: snapshot.result_data["metadata"]
258
+ }
259
+ end
260
+ }.to_json(indent: 2)
261
+ end
262
+ end
263
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgInsights
4
+ class DatabaseSnapshotJob < ApplicationJob
5
+ queue_as -> { PgInsights.background_job_queue }
6
+
7
+ rescue_from(StandardError) do |exception|
8
+ Rails.logger.error "PgInsights::DatabaseSnapshotJob failed: #{exception.message}"
9
+ end
10
+
11
+ def perform
12
+ unless PgInsights.snapshots_available?
13
+ Rails.logger.warn "PgInsights: Snapshots not available, skipping snapshot collection"
14
+ return
15
+ end
16
+
17
+ Rails.logger.info "PgInsights: Starting database snapshot collection"
18
+
19
+ begin
20
+ # Use existing infrastructure to collect and store snapshot
21
+ HealthCheckService.execute_and_cache_check("database_snapshot")
22
+
23
+ # Cleanup old snapshots after successful collection
24
+ cleanup_count = HealthCheckResult.cleanup_old_snapshots
25
+
26
+ Rails.logger.info "PgInsights: Database snapshot completed successfully"
27
+ Rails.logger.info "PgInsights: Cleaned up #{cleanup_count} old snapshots" if cleanup_count > 0
28
+ rescue => e
29
+ Rails.logger.error "PgInsights: Database snapshot collection failed: #{e.message}"
30
+ raise e
31
+ end
32
+ end
33
+
34
+ def self.schedule_next_snapshot
35
+ return false unless PgInsights.snapshots_available?
36
+ return false unless PgInsights.background_jobs_available?
37
+
38
+ begin
39
+ # Schedule the next snapshot based on configured frequency
40
+ next_run_time = Time.current + PgInsights.snapshot_frequency
41
+ set(wait_until: next_run_time).perform_later
42
+
43
+ Rails.logger.info "PgInsights: Next snapshot scheduled for #{next_run_time}"
44
+ true
45
+ rescue => e
46
+ Rails.logger.warn "PgInsights: Failed to schedule next snapshot: #{e.message}"
47
+ false
48
+ end
49
+ end
50
+
51
+ def self.start_recurring_snapshots
52
+ return false unless PgInsights.snapshots_available?
53
+ return false unless PgInsights.background_jobs_available?
54
+
55
+ begin
56
+ # Start the recurring snapshot cycle
57
+ perform_later
58
+ Rails.logger.info "PgInsights: Recurring snapshots started with frequency: #{PgInsights.snapshot_frequency}"
59
+ true
60
+ rescue => e
61
+ Rails.logger.warn "PgInsights: Failed to start recurring snapshots: #{e.message}"
62
+ false
63
+ end
64
+ end
65
+
66
+ def self.validate_configuration
67
+ issues = []
68
+
69
+ issues << "Snapshots are disabled" unless PgInsights.enable_snapshots
70
+ issues << "Snapshot collection is disabled" unless PgInsights.snapshot_collection_enabled
71
+ issues << "Background jobs not available" unless PgInsights.background_jobs_available?
72
+ issues << "HealthCheckResult model not available" unless defined?(HealthCheckResult)
73
+
74
+ frequency = PgInsights.snapshot_frequency
75
+ if frequency.respond_to?(:to_i) && frequency.to_i < 60
76
+ issues << "Snapshot frequency too low (minimum 1 minute recommended)"
77
+ end
78
+
79
+ if issues.empty?
80
+ { valid: true, message: "Database snapshots are properly configured" }
81
+ else
82
+ { valid: false, issues: issues }
83
+ end
84
+ end
85
+
86
+ private
87
+
88
+ # Override perform to automatically schedule the next run
89
+ def perform_with_scheduling
90
+ perform
91
+
92
+ # Schedule the next snapshot if this one was successful
93
+ self.class.schedule_next_snapshot if PgInsights.snapshots_available?
94
+ rescue => e
95
+ # Log the error but still try to schedule the next run
96
+ Rails.logger.error "PgInsights: Snapshot failed but will retry: #{e.message}"
97
+ self.class.schedule_next_snapshot if PgInsights.snapshots_available?
98
+ raise e
99
+ end
100
+ end
101
+ end