pghero 2.3.0 → 2.5.1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of pghero might be problematic. Click here for more details.

Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +85 -54
  3. data/README.md +20 -8
  4. data/app/assets/javascripts/pghero/Chart.bundle.js +16260 -15580
  5. data/app/assets/javascripts/pghero/application.js +8 -7
  6. data/app/assets/javascripts/pghero/chartkick.js +1973 -1325
  7. data/app/assets/javascripts/pghero/highlight.pack.js +2 -2
  8. data/app/assets/javascripts/pghero/jquery.js +3605 -4015
  9. data/app/assets/javascripts/pghero/nouislider.js +2479 -0
  10. data/app/assets/stylesheets/pghero/application.css +1 -1
  11. data/app/assets/stylesheets/pghero/nouislider.css +299 -0
  12. data/app/controllers/pg_hero/home_controller.rb +94 -35
  13. data/app/helpers/pg_hero/home_helper.rb +11 -0
  14. data/app/views/pg_hero/home/_live_queries_table.html.erb +14 -2
  15. data/app/views/pg_hero/home/connections.html.erb +9 -0
  16. data/app/views/pg_hero/home/index.html.erb +49 -10
  17. data/app/views/pg_hero/home/live_queries.html.erb +1 -1
  18. data/app/views/pg_hero/home/maintenance.html.erb +16 -2
  19. data/app/views/pg_hero/home/relation_space.html.erb +2 -2
  20. data/app/views/pg_hero/home/show_query.html.erb +3 -3
  21. data/app/views/pg_hero/home/space.html.erb +3 -3
  22. data/app/views/pg_hero/home/system.html.erb +4 -4
  23. data/app/views/pg_hero/home/tune.html.erb +2 -1
  24. data/lib/generators/pghero/templates/config.yml.tt +21 -1
  25. data/lib/pghero.rb +63 -15
  26. data/lib/pghero/database.rb +101 -17
  27. data/lib/pghero/methods/basic.rb +28 -7
  28. data/lib/pghero/methods/connections.rb +35 -0
  29. data/lib/pghero/methods/constraints.rb +30 -0
  30. data/lib/pghero/methods/indexes.rb +1 -1
  31. data/lib/pghero/methods/maintenance.rb +3 -1
  32. data/lib/pghero/methods/queries.rb +6 -2
  33. data/lib/pghero/methods/query_stats.rb +12 -3
  34. data/lib/pghero/methods/suggested_indexes.rb +1 -1
  35. data/lib/pghero/methods/system.rb +219 -23
  36. data/lib/pghero/stats.rb +1 -1
  37. data/lib/pghero/version.rb +1 -1
  38. metadata +6 -5
  39. data/app/assets/javascripts/pghero/jquery.nouislider.min.js +0 -31
  40. data/app/assets/stylesheets/pghero/jquery.nouislider.css +0 -165
@@ -15,5 +15,16 @@ module PgHero
15
15
  def pghero_js_var(name, value)
16
16
  "var #{name} = #{json_escape(value.to_json(root: false))};".html_safe
17
17
  end
18
+
19
+ def pghero_remove_index(query)
20
+ if query[:columns]
21
+ columns = query[:columns].map(&:to_sym)
22
+ columns = columns.first if columns.size == 1
23
+ end
24
+ ret = String.new("remove_index #{query[:table].to_sym.inspect}")
25
+ ret << ", name: #{(query[:name] || query[:index]).to_s.inspect}"
26
+ ret << ", column: #{columns.inspect}" if columns
27
+ ret
28
+ end
18
29
  end
19
30
  end
@@ -11,7 +11,17 @@
11
11
  <% queries.reverse.each do |query| %>
12
12
  <tr>
13
13
  <td><%= query[:pid] %></td>
14
- <td><%= number_with_delimiter(query[:duration_ms].round) %> ms</td>
14
+ <td>
15
+ <% sec = query[:duration_ms] / 1000.0 %>
16
+ <% if sec < 1.minute %>
17
+ <%= sec.round(1) %> s
18
+ <% elsif sec < 1.day %>
19
+ <%= Time.at(sec).utc.strftime("%H:%M:%S") %>
20
+ <% else %>
21
+ <% days = (sec / 1.day).floor %>
22
+ <%= days %>d <%= Time.at(sec - days.days).utc.strftime("%H:%M:%S") %>
23
+ <% end %>
24
+ </td>
15
25
  <td>
16
26
  <%= query[:state] %>
17
27
  <% if vacuum_progress[query[:pid]] %>
@@ -20,7 +30,9 @@
20
30
  <% end %>
21
31
  </td>
22
32
  <td class="text-right">
23
- <%= button_to "Explain", explain_path, params: {query: query[:query]}, form: {target: "_blank"}, class: "btn btn-info" %>
33
+ <% unless @database.filter_data %>
34
+ <%= button_to "Explain", explain_path, params: {query: query[:query]}, form: {target: "_blank"}, class: "btn btn-info" %>
35
+ <% end %>
24
36
  <%= button_to "Kill", kill_path(pid: query[:pid]), class: "btn btn-danger" %>
25
37
  </td>
26
38
  </tr>
@@ -18,6 +18,15 @@
18
18
  new Chartkick.PieChart("chart-2", <%= json_escape(@connections_by_user.to_json).html_safe %>);
19
19
  </script>
20
20
 
21
+ <% if @connections_by_ssl_status %>
22
+ <h3>By Security</h3>
23
+
24
+ <div id="chart-3" class="chart" style="height: 260px; line-height: 260px; margin-bottom: 20px;">Loading...</div>
25
+ <script>
26
+ new Chartkick.PieChart("chart-3", <%= json_escape(@connections_by_ssl_status.to_json).html_safe %>);
27
+ </script>
28
+ <% end %>
29
+
21
30
  <%= render partial: "connections_table", locals: {connection_sources: @connection_sources} %>
22
31
  <% end %>
23
32
  </div>
@@ -62,11 +62,17 @@
62
62
  (<%= link_to pluralize(@unreadable_sequences.size, "unreadable sequence", "unreadable sequences"), {unreadable: "t"} %>)
63
63
  <% end %>
64
64
  </div>
65
- <div class="alert alert-<%= @invalid_indexes.empty? ? "success" : "warning" %>">
66
- <% if @invalid_indexes.any? %>
67
- <%= pluralize(@invalid_indexes.size, "invalid index", "invalid indexes") %>
65
+ <div class="alert alert-<%= @invalid_indexes.empty? && @invalid_constraints.empty? ? "success" : "warning" %>">
66
+ <% if @invalid_indexes.empty? && @invalid_constraints.empty? %>
67
+ No invalid indexes or constraints
68
68
  <% else %>
69
- No invalid indexes
69
+ <% if @invalid_indexes.any? %>
70
+ <%= pluralize(@invalid_indexes.size, "invalid index", "invalid indexes") %>
71
+ <% end %>
72
+ <% if @invalid_constraints.any? %>
73
+ <% if @invalid_indexes.any? %>and<% end %>
74
+ <%= pluralize(@invalid_constraints.size, "invalid constraint", "invalid constraints") %>
75
+ <% end %>
70
76
  <% end %>
71
77
  </div>
72
78
  <% if @duplicate_indexes %>
@@ -332,6 +338,39 @@
332
338
  </div>
333
339
  <% end %>
334
340
 
341
+ <% if @invalid_constraints.any? %>
342
+ <div class="content">
343
+ <h1>Invalid Constraints</h1>
344
+
345
+ <p>These constraints are marked as <code>NOT VALID</code>. You should validate them.</p>
346
+
347
+ <table class="table">
348
+ <thead>
349
+ <tr>
350
+ <th>Name</th>
351
+ </tr>
352
+ </thead>
353
+ <tbody>
354
+ <% @invalid_constraints.each do |constraint| %>
355
+ <tr>
356
+ <td>
357
+ <%= constraint[:name] %>
358
+ <% if constraint[:schema] != "public" %>
359
+ <span class="text-muted"><%= constraint[:schema] %></span>
360
+ <% end %>
361
+ </td>
362
+ </tr>
363
+ <tr>
364
+ <td style="border-top: none; padding: 0;">
365
+ <pre><code>ALTER TABLE <%= pghero_pretty_ident(constraint[:table], schema: constraint[:schema]) %> VALIDATE CONSTRAINT <%= pghero_pretty_ident(constraint[:name]) %>;</code></pre>
366
+ </td>
367
+ </tr>
368
+ <% end %>
369
+ </tbody>
370
+ </table>
371
+ </div>
372
+ <% end %>
373
+
335
374
  <% if @duplicate_indexes && @duplicate_indexes.any? %>
336
375
  <div class="content">
337
376
  <h1>Duplicate Indexes</h1>
@@ -345,10 +384,10 @@
345
384
  </p>
346
385
 
347
386
  <div id="migration2" style="display: none;">
348
- <pre>rails g migration remove_unneeded_indexes</pre>
387
+ <pre>rails generate migration remove_unneeded_indexes</pre>
349
388
  <p>And paste</p>
350
389
  <pre style="overflow: scroll; white-space: pre; word-break: normal;"><% @duplicate_indexes.each do |query| %>
351
- remove_index <%= query[:unneeded_index][:table].to_sym.inspect %>, name: <%= query[:unneeded_index][:name].to_s.inspect %><% end %></pre>
390
+ <%= pghero_remove_index(query[:unneeded_index]) %><% end %></pre>
352
391
  </div>
353
392
 
354
393
  <table class="table duplicate-indexes">
@@ -387,12 +426,12 @@ remove_index <%= query[:unneeded_index][:table].to_sym.inspect %>, name: <%= que
387
426
  </p>
388
427
 
389
428
  <div id="migration3" style="display: none;">
390
- <pre>rails g migration add_suggested_indexes</pre>
429
+ <pre>rails generate migration add_suggested_indexes</pre>
391
430
  <p>And paste</p>
392
431
  <pre style="overflow: scroll; white-space: pre; word-break: normal;">commit_db_transaction
393
432
  <% @suggested_indexes.each do |index| %>
394
433
  <% if index[:using] && index[:using] != "btree" %>
395
- connection.execute("CREATE INDEX CONCURRENTLY ON <%= index[:table] %><% if index[:using] %> USING <%= index[:using] %><% end %> (<%= index[:columns].join(", ") %>)")
434
+ add_index <%= index[:table].to_sym.inspect %>, <%= index[:columns].first.inspect %>, using: <%= index[:using].inspect %>, algorithm: :concurrently
396
435
  <% else %>
397
436
  add_index <%= index[:table].to_sym.inspect %>, [<%= index[:columns].map(&:to_sym).map(&:inspect).join(", ") %>], algorithm: :concurrently<% end %>
398
437
  <% end %></pre>
@@ -449,10 +488,10 @@ pg_stat_statements.track = all
449
488
  </p>
450
489
 
451
490
  <div id="migration" style="display: none;">
452
- <pre>rails g migration remove_unused_indexes</pre>
491
+ <pre>rails generate migration remove_unused_indexes</pre>
453
492
  <p>And paste</p>
454
493
  <pre style="overflow: scroll; white-space: pre; word-break: normal;"><% @unused_indexes.each do |query| %>
455
- remove_index <%= query[:table].to_sym.inspect %>, name: <%= query[:index].to_s.inspect %><% end %></pre>
494
+ <%= pghero_remove_index(query)%><% end %></pre>
456
495
  </div>
457
496
 
458
497
  <table class="table">
@@ -7,5 +7,5 @@
7
7
 
8
8
  <p><%= button_to "Kill all connections", kill_all_path, class: "btn btn-danger" %></p>
9
9
 
10
- <p class="text-muted">You may need to restart your app server afterwards.</p>
10
+ <p class="text-muted">You may need to restart your app servers afterwards.</p>
11
11
  </div>
@@ -7,6 +7,9 @@
7
7
  <th>Table</th>
8
8
  <th style="width: 20%;">Last Vacuum</th>
9
9
  <th style="width: 20%;">Last Analyze</th>
10
+ <% if @show_dead_rows %>
11
+ <th style="width: 20%;">Dead Rows</th>
12
+ <% end %>
10
13
  </tr>
11
14
  </thead>
12
15
  <tbody>
@@ -21,7 +24,7 @@
21
24
  <td>
22
25
  <% time = [table[:last_autovacuum], table[:last_vacuum]].compact.max %>
23
26
  <% if time %>
24
- <%= time.in_time_zone(@time_zone).strftime("%-m/%-e %l:%M %P") %>
27
+ <%= l time.in_time_zone(@time_zone), format: :short %>
25
28
  <% else %>
26
29
  <span class="text-muted">Unknown</span>
27
30
  <% end %>
@@ -29,11 +32,22 @@
29
32
  <td>
30
33
  <% time = [table[:last_autoanalyze], table[:last_analyze]].compact.max %>
31
34
  <% if time %>
32
- <%= time.in_time_zone(@time_zone).strftime("%-m/%-e %l:%M %P") %>
35
+ <%= l time.in_time_zone(@time_zone), format: :short %>
33
36
  <% else %>
34
37
  <span class="text-muted">Unknown</span>
35
38
  <% end %>
36
39
  </td>
40
+ <% if @show_dead_rows %>
41
+ <td>
42
+ <% if table[:live_rows] != 0 %>
43
+ <%# use live rows only for denominator to make it easier to compare with autovacuum_vacuum_scale_factor %>
44
+ <%# it's not a true percentage, since it can go above 100% %>
45
+ <%= (100.0 * table[:dead_rows] / table[:live_rows]).round %>%
46
+ <% else %>
47
+ <span class="text-muted">Unknown</span>
48
+ <% end %>
49
+ </td>
50
+ <% end %>
37
51
  </tr>
38
52
  <% end %>
39
53
  </tbody>
@@ -6,9 +6,9 @@
6
6
  <% end %>
7
7
  </h1>
8
8
 
9
- <h1>Size <small>MB</small></h1>
9
+ <h1>Size</h1>
10
10
  <div id="chart-1" class="chart" style="margin-bottom: 20px;">Loading...</div>
11
11
  <script>
12
- new Chartkick.LineChart("chart-1", <%= json_escape(@chart_data.to_json).html_safe %>, {colors: ["#5bc0de"], legend: false, min: null})
12
+ new Chartkick.LineChart("chart-1", <%= json_escape(@chart_data.to_json).html_safe %>, {colors: ["#5bc0de"], legend: false, min: null, bytes: true, library: {tooltips: {intersect: false, mode: "index"}}})
13
13
  </script>
14
14
  </div>
@@ -49,19 +49,19 @@
49
49
  <h1>Total Time <small>ms</small></h1>
50
50
  <div id="chart-1" class="chart" style="margin-bottom: 20px;">Loading...</div>
51
51
  <script>
52
- new Chartkick.LineChart("chart-1", <%= json_escape(@chart_data.to_json).html_safe %>, {colors: ["#5bc0de"], legend: false})
52
+ new Chartkick.LineChart("chart-1", <%= json_escape(@chart_data.to_json).html_safe %>, {colors: ["#5bc0de"], legend: false, library: {tooltips: {intersect: false, mode: "index"}}})
53
53
  </script>
54
54
 
55
55
  <h1>Average Time <small>ms</small></h1>
56
56
  <div id="chart-2" class="chart" style="margin-bottom: 20px;">Loading...</div>
57
57
  <script>
58
- new Chartkick.LineChart("chart-2", <%= json_escape(@chart2_data.to_json).html_safe %>, {colors: ["#5bc0de"], legend: false})
58
+ new Chartkick.LineChart("chart-2", <%= json_escape(@chart2_data.to_json).html_safe %>, {colors: ["#5bc0de"], legend: false, library: {tooltips: {intersect: false, mode: "index"}}})
59
59
  </script>
60
60
 
61
61
  <h1>Calls</h1>
62
62
  <div id="chart-3" class="chart" style="margin-bottom: 20px;">Loading...</div>
63
63
  <script>
64
- new Chartkick.LineChart("chart-3", <%= json_escape(@chart3_data.to_json).html_safe %>, {colors: ["#5bc0de"], legend: false})
64
+ new Chartkick.LineChart("chart-3", <%= json_escape(@chart3_data.to_json).html_safe %>, {colors: ["#5bc0de"], legend: false, library: {tooltips: {intersect: false, mode: "index"}}})
65
65
  </script>
66
66
  <% else %>
67
67
  <p>
@@ -6,7 +6,7 @@
6
6
  <% if @system_stats_enabled %>
7
7
  <div id="chart-1" class="chart" style="margin-bottom: 20px;">Loading...</div>
8
8
  <script>
9
- new Chartkick.LineChart("chart-1", <%= json_escape(free_space_stats_path.to_json).html_safe %>, {colors: ["#5bc0de"]})
9
+ new Chartkick.LineChart("chart-1", <%= json_escape(free_space_stats_path.to_json).html_safe %>, {colors: ["#5bc0de"], bytes: true, library: {tooltips: {intersect: false, mode: "index"}}})
10
10
  </script>
11
11
  <% end %>
12
12
 
@@ -30,10 +30,10 @@
30
30
  </p>
31
31
 
32
32
  <div id="migration" style="display: none;">
33
- <pre>rails g migration remove_unused_indexes</pre>
33
+ <pre>rails generate migration remove_unused_indexes</pre>
34
34
  <p>And paste</p>
35
35
  <pre style="overflow: scroll; white-space: pre; word-break: normal;"><% @unused_indexes.sort_by { |q| [-q[:size_bytes], q[:index]] }.each do |query| %>
36
- remove_index <%= query[:table].to_sym.inspect %>, name: <%= query[:index].to_s.inspect %><% end %></pre>
36
+ <%= pghero_remove_index(query) %><% end %></pre>
37
37
  </div>
38
38
  <% end %>
39
39
 
@@ -9,26 +9,26 @@
9
9
  <h1>CPU</h1>
10
10
  <div id="chart-1" class="chart" style="margin-bottom: 20px;">Loading...</div>
11
11
  <script>
12
- new Chartkick.LineChart("chart-1", <%= json_escape(cpu_usage_path(path_options).to_json).html_safe %>, {max: 100, colors: ["#5bc0de"]})
12
+ new Chartkick.LineChart("chart-1", <%= json_escape(cpu_usage_path(path_options).to_json).html_safe %>, {max: 100, colors: ["#5bc0de"], suffix: "%", library: {tooltips: {intersect: false, mode: "index"}}})
13
13
  </script>
14
14
 
15
15
  <h1>Load</h1>
16
16
  <div id="chart-2" class="chart" style="margin-bottom: 20px;">Loading...</div>
17
17
  <script>
18
- new Chartkick.LineChart("chart-2", <%= json_escape(load_stats_path(path_options).to_json).html_safe %>, {colors: ["#5bc0de", "#d9534f"]})
18
+ new Chartkick.LineChart("chart-2", <%= json_escape(load_stats_path(path_options).to_json).html_safe %>, {colors: ["#5bc0de", "#d9534f"], library: {tooltips: {intersect: false, mode: "nearest"}}})
19
19
  </script>
20
20
 
21
21
  <h1>Connections</h1>
22
22
  <div id="chart-3" class="chart" style="margin-bottom: 20px;">Loading...</div>
23
23
  <script>
24
- new Chartkick.LineChart("chart-3", <%= json_escape(connection_stats_path(path_options).to_json).html_safe %>, {colors: ["#5bc0de"]})
24
+ new Chartkick.LineChart("chart-3", <%= json_escape(connection_stats_path(path_options).to_json).html_safe %>, {colors: ["#5bc0de"], library: {tooltips: {intersect: false, mode: "index"}}})
25
25
  </script>
26
26
 
27
27
  <% if @database.replica? %>
28
28
  <h1>Replication Lag</h1>
29
29
  <div id="chart-4" class="chart" style="margin-bottom: 20px;">Loading...</div>
30
30
  <script>
31
- new Chartkick.LineChart("chart-4", <%= json_escape(replication_lag_stats_path(path_options).to_json).html_safe %>, {colors: ["#5bc0de"]})
31
+ new Chartkick.LineChart("chart-4", <%= json_escape(replication_lag_stats_path(path_options).to_json).html_safe %>, {colors: ["#5bc0de"], library: {tooltips: {intersect: false, mode: "index"}}})
32
32
  </script>
33
33
  <% end %>
34
34
  </div>
@@ -18,7 +18,8 @@
18
18
  </tbody>
19
19
  </table>
20
20
 
21
- <p>Check out <%= link_to "PgTune", "https://pgtune.leopard.in.ua/", target: "_blank" %> for recommendations. DB version is <%= @database.server_version.split(" ").first.split(".").first(2).join(".") %>.</p>
21
+ <% version_parts = @database.server_version.split(" ").first.split(".") %>
22
+ <p>Check out <%= link_to "PgTune", "https://pgtune.leopard.in.ua/", target: "_blank" %> for recommendations. DB version is <%= version_parts[0].to_i >= 10 ? version_parts[0] : version_parts.first(2).join(".") %>.</p>
22
23
  </div>
23
24
 
24
25
  <% if @autovacuum_settings %>
@@ -1,8 +1,13 @@
1
1
  databases:
2
- main:
2
+ primary:
3
3
  # Database URL (defaults to app database)
4
4
  # url: <%%= ENV["DATABASE_URL"] %>
5
5
 
6
+ # System stats
7
+ # aws_db_instance_identifier: my-instance
8
+ # gcp_database_id: my-project:my-instance
9
+ # azure_resource_id: my-resource-id
10
+
6
11
  # Add more databases
7
12
  # other:
8
13
  # url: <%%= ENV["OTHER_DATABASE_URL"] %>
@@ -24,3 +29,18 @@ databases:
24
29
 
25
30
  # Time zone (defaults to app time zone)
26
31
  # time_zone: "Pacific Time (US & Canada)"
32
+
33
+ # Basic authentication
34
+ # username: admin
35
+ # password: <%%= ENV["PGHERO_PASSWORD"] %>
36
+
37
+ # Stats database URL (defaults to app database)
38
+ # stats_database_url: <%%= ENV["PGHERO_STATS_DATABASE_URL"] %>
39
+
40
+ # AWS configuration (defaults to app AWS config)
41
+ # aws_access_key_id: <%%= ENV["AWS_ACCESS_KEY_ID"] %>
42
+ # aws_secret_access_key: <%%= ENV["AWS_SECRET_ACCESS_KEY"] %>
43
+ # aws_region: us-east-1
44
+
45
+ # Filter data from queries (experimental)
46
+ # filter_data: true
@@ -5,6 +5,7 @@ require "forwardable"
5
5
  # methods
6
6
  require "pghero/methods/basic"
7
7
  require "pghero/methods/connections"
8
+ require "pghero/methods/constraints"
8
9
  require "pghero/methods/explain"
9
10
  require "pghero/methods/indexes"
10
11
  require "pghero/methods/kill"
@@ -33,9 +34,11 @@ module PgHero
33
34
  class Error < StandardError; end
34
35
  class NotEnabled < Error; end
35
36
 
37
+ MUTEX = Mutex.new
38
+
36
39
  # settings
37
40
  class << self
38
- attr_accessor :long_running_query_sec, :slow_query_ms, :slow_query_calls, :explain_timeout_sec, :total_connections_threshold, :cache_hit_rate_threshold, :env, :show_migrations, :config_path
41
+ attr_accessor :long_running_query_sec, :slow_query_ms, :slow_query_calls, :explain_timeout_sec, :total_connections_threshold, :cache_hit_rate_threshold, :env, :show_migrations, :config_path, :filter_data
39
42
  end
40
43
  self.long_running_query_sec = (ENV["PGHERO_LONG_RUNNING_QUERY_SEC"] || 60).to_i
41
44
  self.slow_query_ms = (ENV["PGHERO_SLOW_QUERY_MS"] || 20).to_i
@@ -46,14 +49,15 @@ module PgHero
46
49
  self.env = ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
47
50
  self.show_migrations = true
48
51
  self.config_path = ENV["PGHERO_CONFIG_PATH"] || "config/pghero.yml"
52
+ self.filter_data = ENV["PGHERO_FILTER_DATA"].to_s.size > 0
49
53
 
50
54
  class << self
51
55
  extend Forwardable
52
56
  def_delegators :primary_database, :access_key_id, :analyze, :analyze_tables, :autoindex, :autovacuum_danger,
53
- :best_index, :blocked_queries, :connection_sources, :connection_states, :connection_stats,
57
+ :best_index, :blocked_queries, :connections, :connection_sources, :connection_states, :connection_stats,
54
58
  :cpu_usage, :create_user, :database_size, :db_instance_identifier, :disable_query_stats, :drop_user,
55
59
  :duplicate_indexes, :enable_query_stats, :explain, :historical_query_stats_enabled?, :index_caching,
56
- :index_hit_rate, :index_usage, :indexes, :invalid_indexes, :kill, :kill_all, :kill_long_running_queries,
60
+ :index_hit_rate, :index_usage, :indexes, :invalid_constraints, :invalid_indexes, :kill, :kill_all, :kill_long_running_queries,
57
61
  :last_stats_reset_time, :long_running_queries, :maintenance_info, :missing_indexes, :query_stats,
58
62
  :query_stats_available?, :query_stats_enabled?, :query_stats_extension_enabled?, :query_stats_readable?,
59
63
  :rds_stats, :read_iops_stats, :region, :relation_sizes, :replica?, :replication_lag, :replication_lag_stats,
@@ -70,6 +74,22 @@ module PgHero
70
74
  @time_zone || Time.zone
71
75
  end
72
76
 
77
+ # use method instead of attr_accessor to ensure
78
+ # this works if variable set after PgHero is loaded
79
+ def username
80
+ @username ||= config["username"] || ENV["PGHERO_USERNAME"]
81
+ end
82
+
83
+ # use method instead of attr_accessor to ensure
84
+ # this works if variable set after PgHero is loaded
85
+ def password
86
+ @password ||= config["password"] || ENV["PGHERO_PASSWORD"]
87
+ end
88
+
89
+ def stats_database_url
90
+ @stats_database_url ||= config["stats_database_url"] || ENV["PGHERO_STATS_DATABASE_URL"]
91
+ end
92
+
73
93
  def config
74
94
  @config ||= begin
75
95
  require "erb"
@@ -89,26 +109,49 @@ module PgHero
89
109
  elsif config_file_exists
90
110
  raise "Invalid config file"
91
111
  else
92
- {
93
- "databases" => {
94
- "primary" => {
95
- "url" => ENV["PGHERO_DATABASE_URL"] || ActiveRecord::Base.connection_config,
96
- "db_instance_identifier" => ENV["PGHERO_DB_INSTANCE_IDENTIFIER"]
97
- }
112
+ databases = {}
113
+
114
+ if !ENV["PGHERO_DATABASE_URL"] && spec_supported?
115
+ ActiveRecord::Base.configurations.configs_for(env_name: env, include_replicas: true).each do |db|
116
+ databases[db.spec_name] = {"spec" => db.spec_name}
117
+ end
118
+ end
119
+
120
+ if databases.empty?
121
+ databases["primary"] = {
122
+ "url" => ENV["PGHERO_DATABASE_URL"] || ActiveRecord::Base.connection_config
98
123
  }
124
+ end
125
+
126
+ if databases.size == 1
127
+ databases.values.first.merge!(
128
+ "db_instance_identifier" => ENV["PGHERO_DB_INSTANCE_IDENTIFIER"],
129
+ "gcp_database_id" => ENV["PGHERO_GCP_DATABASE_ID"],
130
+ "azure_resource_id" => ENV["PGHERO_AZURE_RESOURCE_ID"]
131
+ )
132
+ end
133
+
134
+ {
135
+ "databases" => databases
99
136
  }
100
137
  end
101
138
  end
102
139
  end
103
140
 
141
+ # ensure we only have one copy of databases
142
+ # so there's only one connection pool per database
104
143
  def databases
105
- @databases ||= begin
106
- Hash[
107
- config["databases"].map do |id, c|
108
- [id.to_sym, PgHero::Database.new(id, c)]
109
- end
110
- ]
144
+ unless defined?(@databases)
145
+ # only use mutex on initialization
146
+ MUTEX.synchronize do
147
+ # return if another process initialized while we were waiting
148
+ return @databases if defined?(@databases)
149
+
150
+ @databases = config["databases"].map { |id, c| [id.to_sym, Database.new(id, c)] }.to_h
151
+ end
111
152
  end
153
+
154
+ @databases
112
155
  end
113
156
 
114
157
  def primary_database
@@ -163,6 +206,11 @@ module PgHero
163
206
  end
164
207
  end
165
208
 
209
+ # private
210
+ def spec_supported?
211
+ ActiveRecord::VERSION::MAJOR >= 6
212
+ end
213
+
166
214
  private
167
215
 
168
216
  def each_database