pg_insights 0.1.0 → 0.2.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 +4 -4
- data/README.md +144 -45
- data/app/controllers/pg_insights/timeline_controller.rb +263 -0
- data/app/jobs/pg_insights/database_snapshot_job.rb +101 -0
- data/app/models/pg_insights/health_check_result.rb +151 -0
- data/app/services/pg_insights/health_check_service.rb +159 -3
- data/app/views/layouts/pg_insights/application.html.erb +1 -0
- data/app/views/pg_insights/timeline/compare.html.erb +997 -0
- data/app/views/pg_insights/timeline/index.html.erb +797 -0
- data/app/views/pg_insights/timeline/show.html.erb +1004 -0
- data/config/routes.rb +9 -5
- data/lib/generators/pg_insights/install_generator.rb +69 -18
- data/lib/pg_insights/version.rb +1 -1
- data/lib/pg_insights.rb +24 -10
- data/lib/tasks/pg_insights.rake +419 -33
- metadata +7 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a5345562a5a6b419e8008d02a94cd9136252249ed4d1931f06583233f13ab2e7
|
4
|
+
data.tar.gz: 3381256f037dbbc770164de14552b792e9bfe2437e273f4768bd5de40aac6560
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bd20373bfafbdc70ce5660013389b4030a419135292f7f3347b57d22105962e012e032499d732b31f1eed319f138832347ca999572c14041a99eec1fda8b90c4
|
7
|
+
data.tar.gz: 8121527a9f0de02562c7f22fd6fd0ce935da86eca38c618493bddc23e9e0ded74c3d400ab3f85cba883febe3dcd563b1ef3c8b624c8924b818454201be8fde51
|
data/README.md
CHANGED
@@ -1,39 +1,113 @@
|
|
1
|
-
# PgInsights
|
1
|
+
# 📊 PgInsights
|
2
2
|
|
3
|
-
|
3
|
+
> PostgreSQL performance monitoring for Rails apps
|
4
|
+
|
5
|
+
<div align="center">
|
4
6
|
|
5
7
|
[](https://badge.fury.io/rb/pg_insights)
|
6
8
|
[](https://github.com/mezbahalam/pg_insights/actions/workflows/ci.yml)
|
7
9
|
[](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.
|
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
|
-
|
21
|
+
PgInsights gives you that - a simple dashboard you can access at `/pg_insights` in your Rails app.
|
16
22
|
|
17
|
-
|
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
|
-
|
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
|
-
|
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
|
-
##
|
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
|
+

|
77
|
+
|
78
|
+
### Query Runner
|
79
|
+
Run custom queries and visualize results with built-in charting capabilities.
|
80
|
+
|
81
|
+

|
82
|
+
|
83
|
+
### Timeline Dashboard
|
84
|
+
Track database configuration changes and performance trends over time.
|
85
|
+
|
86
|
+

|
87
|
+
|
88
|
+
### Snapshot Details
|
89
|
+
View detailed information about individual database snapshots including parameters and metadata.
|
90
|
+
|
91
|
+

|
92
|
+
|
93
|
+
### Performance Metrics
|
94
|
+
Monitor key performance indicators and database health metrics across time periods.
|
95
|
+
|
96
|
+

|
97
|
+
|
98
|
+
### Snapshot Comparison
|
99
|
+
Compare database configurations and performance metrics between different time periods.
|
100
|
+
|
101
|
+

|
102
|
+
|
103
|
+
### Side-by-Side Comparison
|
104
|
+
Detailed comparison view showing parameter changes and performance differences.
|
105
|
+
|
106
|
+

|
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
|
-
#
|
135
|
+
# === Background Jobs ===
|
62
136
|
config.enable_background_jobs = true
|
137
|
+
config.background_job_queue = :pg_insights_health
|
63
138
|
|
64
|
-
#
|
65
|
-
config.health_cache_expiry =
|
139
|
+
# === Health Check Settings ===
|
140
|
+
config.health_cache_expiry = 5.minutes
|
141
|
+
config.health_check_timeout = 10.seconds
|
66
142
|
|
67
|
-
#
|
68
|
-
config.
|
69
|
-
|
70
|
-
|
71
|
-
config.
|
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
|
-
|
80
|
-
- ✅
|
81
|
-
- ✅
|
82
|
-
- ✅
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
129
|
-
|
130
|
-
|
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
|
-
|
138
|
-
rails pg_insights:
|
139
|
-
rails pg_insights:
|
140
|
-
|
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
|
-
|
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
|