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.
@@ -79,43 +79,15 @@ namespace :pg_insights do
79
79
  end
80
80
  end
81
81
 
82
- desc "Clear all PgInsights stored data"
83
- task clear_data: :environment do
84
- puts "Clearing PgInsights stored data..."
85
-
86
- # Remove data from database
87
- begin
88
- if defined?(PgInsights::Query)
89
- query_count = PgInsights::Query.count
90
- PgInsights::Query.destroy_all
91
- puts "Removed #{query_count} stored queries"
92
- end
93
- rescue => e
94
- puts "Could not remove queries: #{e.message}"
95
- end
96
-
97
- begin
98
- if defined?(PgInsights::HealthCheckResult)
99
- result_count = PgInsights::HealthCheckResult.count
100
- PgInsights::HealthCheckResult.destroy_all
101
- puts "Removed #{result_count} cached health check results"
102
- end
103
- rescue => e
104
- puts "Could not remove health check results: #{e.message}"
105
- end
106
-
107
- puts "PgInsights data cleared successfully!"
108
- puts "To completely uninstall PgInsights (remove migrations and routes), run: rails generate pg_insights:clean"
109
- end
110
-
111
- desc "Reset PgInsights data (clears all stored queries and health check results)"
82
+ desc "Reset all PgInsights data (clears stored queries and health check results)"
112
83
  task reset: :environment do
113
84
  puts "Resetting PgInsights data..."
114
85
 
115
86
  begin
116
87
  if defined?(PgInsights::Query)
88
+ query_count = PgInsights::Query.count
117
89
  PgInsights::Query.destroy_all
118
- puts "Cleared all PgInsights queries"
90
+ puts "Cleared #{query_count} stored queries"
119
91
  end
120
92
  rescue => e
121
93
  puts "Could not clear queries: #{e.message}"
@@ -123,16 +95,21 @@ namespace :pg_insights do
123
95
 
124
96
  begin
125
97
  if defined?(PgInsights::HealthCheckResult)
98
+ result_count = PgInsights::HealthCheckResult.count
126
99
  PgInsights::HealthCheckResult.destroy_all
127
- puts "Cleared all PgInsights health check results"
100
+ puts "Cleared #{result_count} health check results"
128
101
  end
129
102
  rescue => e
130
103
  puts "Could not clear health check results: #{e.message}"
131
104
  end
132
105
 
133
- puts "PgInsights data reset completed!"
106
+ puts "PgInsights data reset completed!"
107
+ puts "To completely uninstall PgInsights (remove migrations and routes), run: rails generate pg_insights:clean"
134
108
  end
135
109
 
110
+ # Alias for backward compatibility
111
+ task clear_data: :reset
112
+
136
113
  desc "Show PgInsights statistics"
137
114
  task stats: :environment do
138
115
  puts "PgInsights Statistics:"
@@ -169,4 +146,413 @@ namespace :pg_insights do
169
146
  puts "Health check results table not available: #{e.message}"
170
147
  end
171
148
  end
149
+
150
+ desc "Collect a database snapshot immediately"
151
+ task collect_snapshot: :environment do
152
+ puts "Collecting Database Snapshot"
153
+ puts "==========================="
154
+
155
+ unless PgInsights.snapshots_available?
156
+ puts "❌ Snapshots are not enabled"
157
+ puts " Add to config/initializers/pg_insights.rb:"
158
+ puts " PgInsights.configure { |c| c.enable_snapshots = true }"
159
+ exit 1
160
+ end
161
+
162
+ start_time = Time.current
163
+
164
+ begin
165
+ PgInsights::HealthCheckService.execute_and_cache_check("database_snapshot")
166
+ execution_time = Time.current - start_time
167
+
168
+ puts "✅ Snapshot collected successfully in #{execution_time.round(2)} seconds"
169
+
170
+ latest = PgInsights::HealthCheckResult.latest_snapshot
171
+ if latest
172
+ puts " Snapshot ID: #{latest.id}"
173
+ puts " Collected at: #{latest.executed_at}"
174
+ puts " Parameters tracked: #{latest.result_data['parameters']&.keys&.count || 0}"
175
+ puts " Metrics collected: #{latest.result_data['metrics']&.keys&.count || 0}"
176
+ end
177
+
178
+ puts "Visit /pg_insights/timeline to view the snapshot in the timeline"
179
+ rescue => e
180
+ puts "❌ Snapshot collection failed: #{e.message}"
181
+ exit 1
182
+ end
183
+ end
184
+
185
+ desc "Start recurring snapshot collection"
186
+ task start_snapshots: :environment do
187
+ puts "Starting Recurring Database Snapshots"
188
+ puts "====================================="
189
+
190
+ unless PgInsights.snapshots_available?
191
+ puts "❌ Snapshots are not enabled"
192
+ exit 1
193
+ end
194
+
195
+ unless PgInsights.background_jobs_available?
196
+ puts "❌ Background jobs are not available"
197
+ puts " PgInsights needs ActiveJob to schedule recurring snapshots"
198
+ exit 1
199
+ end
200
+
201
+ validation = PgInsights::DatabaseSnapshotJob.validate_configuration
202
+ unless validation[:valid]
203
+ puts "❌ Configuration issues found:"
204
+ validation[:issues].each { |issue| puts " - #{issue}" }
205
+ exit 1
206
+ end
207
+
208
+ begin
209
+ if PgInsights::DatabaseSnapshotJob.start_recurring_snapshots
210
+ puts "✅ Recurring snapshots started successfully"
211
+ puts " Frequency: #{PgInsights.snapshot_frequency}"
212
+ puts " Queue: #{PgInsights.background_job_queue}"
213
+ puts " Retention: #{PgInsights.snapshot_retention_days} days"
214
+ puts ""
215
+ puts "Snapshots will be collected automatically based on the configured frequency."
216
+ puts "Visit /pg_insights/timeline to monitor collection progress."
217
+ else
218
+ puts "❌ Failed to start recurring snapshots"
219
+ end
220
+ rescue => e
221
+ puts "❌ Error starting snapshots: #{e.message}"
222
+ exit 1
223
+ end
224
+ end
225
+
226
+ desc "Show snapshot configuration and status"
227
+ task snapshot_status: :environment do
228
+ puts "Database Snapshot Status"
229
+ puts "======================="
230
+
231
+ # Configuration
232
+ puts "Configuration:"
233
+ puts " Enabled: #{PgInsights.enable_snapshots ? 'Yes' : 'No'}"
234
+ puts " Collection Enabled: #{PgInsights.snapshot_collection_enabled ? 'Yes' : 'No'}"
235
+ puts " Frequency: #{PgInsights.snapshot_frequency}"
236
+ puts " Retention: #{PgInsights.snapshot_retention_days} days"
237
+ puts ""
238
+
239
+ # Availability check
240
+ if PgInsights.snapshots_available?
241
+ puts "✅ Snapshots are available"
242
+ else
243
+ puts "❌ Snapshots are not available"
244
+ return
245
+ end
246
+
247
+ # Database stats
248
+ begin
249
+ if defined?(PgInsights::HealthCheckResult)
250
+ snapshot_count = PgInsights::HealthCheckResult.snapshots.count
251
+ puts "Statistics:"
252
+ puts " Total snapshots: #{snapshot_count}"
253
+
254
+ latest = PgInsights::HealthCheckResult.latest_snapshot
255
+ if latest
256
+ puts " Latest snapshot: #{latest.executed_at}"
257
+ puts " Latest status: #{latest.status}"
258
+ else
259
+ puts " Latest snapshot: None"
260
+ end
261
+
262
+ # Recent snapshots
263
+ recent = PgInsights::HealthCheckResult.snapshots.limit(5)
264
+ if recent.any?
265
+ puts ""
266
+ puts "Recent snapshots:"
267
+ recent.each do |snapshot|
268
+ puts " #{snapshot.executed_at.strftime('%Y-%m-%d %H:%M')} - #{snapshot.status}"
269
+ end
270
+ end
271
+
272
+ # Parameter changes
273
+ changes = PgInsights::HealthCheckResult.detect_parameter_changes_since(7)
274
+ puts ""
275
+ puts "Parameter changes (last 7 days): #{changes.count}"
276
+ if changes.any?
277
+ changes.first(3).each do |change_event|
278
+ puts " #{change_event[:detected_at].strftime('%Y-%m-%d %H:%M')} - #{change_event[:changes].keys.join(', ')}"
279
+ end
280
+ end
281
+ end
282
+ rescue => e
283
+ puts "Could not retrieve snapshot statistics: #{e.message}"
284
+ end
285
+
286
+ # Background job validation
287
+ puts ""
288
+ validation = PgInsights::DatabaseSnapshotJob.validate_configuration
289
+ if validation[:valid]
290
+ puts "✅ #{validation[:message]}"
291
+ else
292
+ puts "❌ Configuration issues:"
293
+ validation[:issues].each { |issue| puts " - #{issue}" }
294
+ end
295
+
296
+ puts ""
297
+ puts "Commands:"
298
+ puts " Collect snapshot now: rails pg_insights:collect_snapshot"
299
+ puts " Start recurring: rails pg_insights:start_snapshots"
300
+ puts " Change frequency: Set PgInsights.snapshot_frequency in initializer"
301
+ end
302
+
303
+ desc "Clean up old snapshots"
304
+ task cleanup_snapshots: :environment do
305
+ puts "Cleaning up old snapshots..."
306
+
307
+ begin
308
+ deleted_count = PgInsights::HealthCheckResult.cleanup_old_snapshots
309
+ puts "✅ Cleaned up #{deleted_count} old snapshots"
310
+ puts " Retention period: #{PgInsights.snapshot_retention_days} days"
311
+ rescue => e
312
+ puts "❌ Cleanup failed: #{e.message}"
313
+ end
314
+ end
315
+
316
+ desc "Clean up old health check results (keeps recent results, removes old ones)"
317
+ task cleanup: :environment do
318
+ puts "🧹 Cleaning up old PgInsights health check results..."
319
+
320
+ # Keep results from last 30 days by default
321
+ retention_days = 30
322
+ cutoff_date = retention_days.days.ago
323
+
324
+ begin
325
+ deleted_count = PgInsights::HealthCheckResult.where("executed_at < ?", cutoff_date).delete_all
326
+ puts "✅ Cleaned up #{deleted_count} old health check results (older than #{retention_days} days)"
327
+ rescue => e
328
+ puts "❌ Cleanup failed: #{e.message}"
329
+ end
330
+ end
331
+
332
+
333
+
334
+ desc "Generate fake timeline data for testing (90 days)"
335
+ task seed_timeline: :environment do
336
+ puts "Generating fake timeline data for testing..."
337
+
338
+ end_date = Date.current
339
+ start_date = end_date - 90.days
340
+
341
+ puts "📅 Generating data from #{start_date.strftime('%B %d, %Y')} to #{end_date.strftime('%B %d, %Y')}"
342
+
343
+ PgInsights::HealthCheckResult.where(check_type: "database_snapshot").delete_all
344
+ puts "🗑️ Cleared existing timeline data"
345
+
346
+ base_config = {
347
+ "shared_buffers" => "256MB",
348
+ "work_mem" => "4MB",
349
+ "max_connections" => "100",
350
+ "effective_cache_size" => "1GB",
351
+ "checkpoint_completion_target" => "0.9",
352
+ "wal_buffers" => "16MB",
353
+ "default_statistics_target" => "100",
354
+ "random_page_cost" => "4.0",
355
+ "effective_io_concurrency" => "1",
356
+ "maintenance_work_mem" => "64MB"
357
+ }
358
+
359
+ config_changes = [
360
+ { date: start_date + 15.days, changes: { "shared_buffers" => "512MB", "effective_cache_size" => "2GB" } },
361
+ { date: start_date + 30.days, changes: { "work_mem" => "8MB", "max_connections" => "150" } },
362
+ { date: start_date + 45.days, changes: { "checkpoint_completion_target" => "0.7" } },
363
+ { date: start_date + 60.days, changes: { "shared_buffers" => "1GB", "maintenance_work_mem" => "128MB" } },
364
+ { date: start_date + 75.days, changes: { "work_mem" => "6MB", "default_statistics_target" => "150" } }
365
+ ]
366
+
367
+ current_config = base_config.dup
368
+
369
+ (start_date..end_date).each_with_index do |date, index|
370
+ config_changes.each do |change|
371
+ if date >= change[:date] && date < change[:date] + 1.day
372
+ current_config.merge!(change[:changes])
373
+ puts "⚙️ Config change on #{date.strftime('%m/%d/%Y')}: #{change[:changes].keys.join(', ')}"
374
+ end
375
+ end
376
+
377
+ snapshots_per_day = rand(1..3)
378
+
379
+ snapshots_per_day.times do |snapshot_index|
380
+ hour = case snapshot_index
381
+ when 0 then rand(6..10)
382
+ when 1 then rand(11..15)
383
+ when 2 then rand(16..22)
384
+ end
385
+ minute = rand(0..59)
386
+
387
+ timestamp = date.beginning_of_day + hour.hours + minute.minutes
388
+
389
+ base_cache_hit_rate = 92.0 + Math.sin(index * 0.1) * 5 + rand(-2.0..2.0)
390
+ base_cache_hit_rate = [ [ base_cache_hit_rate, 75.0 ].max, 99.5 ].min
391
+
392
+ base_query_time = 15.0 + Math.sin(index * 0.05) * 10 + rand(-5.0..5.0)
393
+ base_query_time = [ base_query_time, 1.0 ].max
394
+
395
+ base_connections = 25 + Math.sin(index * 0.08) * 15 + rand(-5..5)
396
+ base_connections = [ [ base_connections, 5 ].max, 200 ].min
397
+
398
+ if rand(100) < 5
399
+ base_cache_hit_rate *= 0.8
400
+ base_query_time *= 2.5
401
+ base_connections = [ base_connections * 1.5, 300 ].min
402
+ end
403
+
404
+ metrics = {
405
+ "cache_hit_rate" => base_cache_hit_rate.round(2),
406
+ "avg_query_time" => base_query_time.round(2),
407
+ "total_connections" => base_connections.to_i,
408
+ "bloated_tables" => rand(0..8),
409
+ "unused_indexes" => rand(0..12),
410
+ "missing_indexes" => rand(0..5),
411
+ "slow_queries_count" => rand(0..25),
412
+ "dead_tuples_percent" => rand(0.0..5.0).round(2),
413
+ "index_usage_ratio" => (85.0 + rand(-10.0..10.0)).round(2),
414
+ "checkpoint_frequency" => rand(30..300),
415
+ "wal_size_mb" => rand(50..500),
416
+ "temp_files_count" => rand(0..50),
417
+ "lock_waits_count" => rand(0..10)
418
+ }
419
+
420
+ extensions = [ "pg_stat_statements", "uuid-ossp", "hstore", "postgis", "pg_trgm" ]
421
+ selected_extensions = extensions.sample(rand(3..5))
422
+
423
+ metadata = {
424
+ "postgresql_version" => "15.4",
425
+ "database_size" => "#{rand(500..2000)}MB",
426
+ "table_count" => rand(50..200),
427
+ "index_count" => rand(100..400),
428
+ "extensions" => selected_extensions,
429
+ "uptime_hours" => rand(100..8760),
430
+ "max_wal_size" => "1GB",
431
+ "archive_mode" => "on",
432
+ "log_statement" => "none",
433
+ "timezone" => "UTC"
434
+ }
435
+
436
+ result_data = {
437
+ "metrics" => metrics,
438
+ "parameters" => current_config,
439
+ "metadata" => metadata,
440
+ "snapshot_id" => SecureRandom.uuid,
441
+ "collection_method" => "automated"
442
+ }
443
+
444
+ PgInsights::HealthCheckResult.create!(
445
+ check_type: "database_snapshot",
446
+ status: "success",
447
+ result_data: result_data,
448
+ execution_time_ms: rand(50..300),
449
+ executed_at: timestamp
450
+ )
451
+ end
452
+
453
+ print "." if index % 10 == 0
454
+ end
455
+
456
+ puts "\n"
457
+
458
+ total_snapshots = PgInsights::HealthCheckResult.where(check_type: "database_snapshot").count
459
+ date_range = PgInsights::HealthCheckResult.where(check_type: "database_snapshot")
460
+ .pluck(:executed_at)
461
+ .minmax
462
+
463
+ puts "✅ Generated #{total_snapshots} timeline snapshots"
464
+ puts "📊 Date range: #{date_range[0].strftime('%B %d, %Y')} to #{date_range[1].strftime('%B %d, %Y')}"
465
+ puts "⚙️ Configuration changes: #{config_changes.count} parameter updates"
466
+ puts ""
467
+ puts "🚀 Timeline feature is ready for testing!"
468
+ puts " Visit: /pg_insights/timeline"
469
+ puts ""
470
+ puts "📈 Sample metrics generated:"
471
+ latest = PgInsights::HealthCheckResult.where(check_type: "database_snapshot").last
472
+ if latest
473
+ puts " • Cache Hit Rate: #{latest.result_data.dig('metrics', 'cache_hit_rate')}%"
474
+ puts " • Avg Query Time: #{latest.result_data.dig('metrics', 'avg_query_time')}ms"
475
+ puts " • Total Connections: #{latest.result_data.dig('metrics', 'total_connections')}"
476
+ puts " • Database Size: #{latest.result_data.dig('metadata', 'database_size')}"
477
+ end
478
+ end
479
+
480
+ desc "Generate sample data for development"
481
+ task sample_data: :environment do
482
+ puts "🎭 Generating sample health check data..."
483
+
484
+ # Generate some basic health check results
485
+ check_types = %w[
486
+ unused_indexes
487
+ missing_indexes
488
+ bloated_tables
489
+ slow_queries
490
+ cache_hit_ratio
491
+ connection_stats
492
+ ]
493
+
494
+ check_types.each do |check_type|
495
+ 3.times do |i|
496
+ PgInsights::HealthCheckResult.create!(
497
+ check_type: check_type,
498
+ status: %w[success error].sample,
499
+ result_data: generate_sample_result_data(check_type),
500
+ execution_time_ms: rand(10..500),
501
+ executed_at: rand(1..72).hours.ago
502
+ )
503
+ end
504
+ end
505
+
506
+ puts "✅ Generated sample health check data"
507
+ end
508
+
509
+ private
510
+
511
+ def self.generate_sample_result_data(check_type)
512
+ case check_type
513
+ when "unused_indexes"
514
+ {
515
+ "indexes" => [
516
+ { "table" => "users", "index" => "idx_users_old_email", "size" => "45MB", "scans" => 0 },
517
+ { "table" => "orders", "index" => "idx_orders_temp", "size" => "23MB", "scans" => 2 }
518
+ ],
519
+ "total_wasted_space" => "68MB"
520
+ }
521
+ when "missing_indexes"
522
+ {
523
+ "suggestions" => [
524
+ { "table" => "posts", "column" => "created_at", "seq_scans" => 1250, "estimated_benefit" => "High" },
525
+ { "table" => "comments", "column" => "user_id", "seq_scans" => 890, "estimated_benefit" => "Medium" }
526
+ ]
527
+ }
528
+ when "bloated_tables"
529
+ {
530
+ "tables" => [
531
+ { "table" => "logs", "bloat_percent" => 45.2, "wasted_space" => "156MB" },
532
+ { "table" => "events", "bloat_percent" => 32.1, "wasted_space" => "89MB" }
533
+ ]
534
+ }
535
+ when "slow_queries"
536
+ {
537
+ "queries" => [
538
+ { "query" => "SELECT * FROM users WHERE email LIKE...", "avg_time" => 1250.5, "calls" => 45 },
539
+ { "query" => "UPDATE posts SET view_count...", "avg_time" => 890.2, "calls" => 123 }
540
+ ]
541
+ }
542
+ when "cache_hit_ratio"
543
+ {
544
+ "buffer_cache_hit_ratio" => rand(85.0..99.9).round(2),
545
+ "index_cache_hit_ratio" => rand(90.0..99.9).round(2)
546
+ }
547
+ when "connection_stats"
548
+ {
549
+ "total_connections" => rand(10..100),
550
+ "active_connections" => rand(5..50),
551
+ "idle_connections" => rand(5..30),
552
+ "max_connections" => 100
553
+ }
554
+ else
555
+ { "data" => "sample data", "timestamp" => Time.current }
556
+ end
557
+ end
172
558
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pg_insights
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mezbah Alam
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-06-30 00:00:00.000000000 Z
11
+ date: 2025-07-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -62,9 +62,11 @@ files:
62
62
  - app/controllers/pg_insights/health_controller.rb
63
63
  - app/controllers/pg_insights/insights_controller.rb
64
64
  - app/controllers/pg_insights/queries_controller.rb
65
+ - app/controllers/pg_insights/timeline_controller.rb
65
66
  - app/helpers/pg_insights/application_helper.rb
66
67
  - app/helpers/pg_insights/insights_helper.rb
67
68
  - app/jobs/pg_insights/application_job.rb
69
+ - app/jobs/pg_insights/database_snapshot_job.rb
68
70
  - app/jobs/pg_insights/health_check_job.rb
69
71
  - app/jobs/pg_insights/health_check_scheduler_job.rb
70
72
  - app/jobs/pg_insights/recurring_health_checks_job.rb
@@ -87,6 +89,9 @@ files:
87
89
  - app/views/pg_insights/insights/_table_controls.html.erb
88
90
  - app/views/pg_insights/insights/_table_view.html.erb
89
91
  - app/views/pg_insights/insights/index.html.erb
92
+ - app/views/pg_insights/timeline/compare.html.erb
93
+ - app/views/pg_insights/timeline/index.html.erb
94
+ - app/views/pg_insights/timeline/show.html.erb
90
95
  - config/default_queries.yml
91
96
  - config/routes.rb
92
97
  - lib/generators/pg_insights/clean_generator.rb