pghero 2.4.1 → 2.7.0

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.

checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e1eb28c7ae995ccdc8fcc30dec3c42f3abb1463981b89a85ed9a8b8b53dba4ce
4
- data.tar.gz: 0c0b1e34f2f12c46055a15c937b282048b99036ce6b535ac2445bd83b4d7ac67
3
+ metadata.gz: 38e408389854af6f5a2a71ee70bc3d66c257a299b1a1fec255c279819db00482
4
+ data.tar.gz: 2f492cf50fd99032ec9ac9cfd03df70ef5c4d45ea55a0c14a5c608102b5aa106
5
5
  SHA512:
6
- metadata.gz: fc924cde46b89d3d64649f8df7abbe81538892ba0839c4983ad0e67522c36ee4b0ee52673c3d79977cfac05413c846d821fb25d885ba33693e2d01c6de042f8b
7
- data.tar.gz: 790d0cc392a123ac30ae59868a80eb1239adbcf091a53c0edd9cbabb6426f0e86d11d25ac8fbc0c85619c90f766c46a67a49e439467b8c47f960482e8d04c10d
6
+ metadata.gz: edc72b96fb3c766f41682c22aa2287a99c610435687794d0a09395821a129709b0e464f7e9031d43cac7a6d1bc334cb419885dc32b87d9d4e58a75d810c3ba30
7
+ data.tar.gz: a4ad92136153cb3f607bddb0b289d3c180a6a653f7a6eac9b47ca6fe7fa04626923f7b84e80951577c9a94314c2487ac7dccb4cdd5f2483a3076473893bac13e
@@ -1,3 +1,33 @@
1
+ ## 2.7.0 (2020-08-04)
2
+
3
+ - Fixed CSRF vulnerability with non-session based authentication
4
+ - Added `database`, `user`, and `query_hash` options to `reset_query_stats` method
5
+
6
+ ## 2.6.0 (2020-07-09)
7
+
8
+ - Added support for Postgres 13 beta 2
9
+ - Added support for non-integer explain timeout
10
+
11
+ ## 2.5.1 (2020-06-23)
12
+
13
+ - Added support for `google-cloud-monitoring` >= 1
14
+ - Added support for `google-cloud-monitoring-v3`
15
+ - Fixed system stats not showing up in Rails 6 with environment variables
16
+
17
+ ## 2.5.0 (2020-05-24)
18
+
19
+ - Added system stats for Google Cloud SQL and Azure Database
20
+ - Added experimental `filter_data` option
21
+ - Localized times on maintenance page
22
+ - Improved connection pooling
23
+ - Improved error message for non-Postgres connections
24
+ - Fixed more deprecation warnings in Ruby 2.7
25
+
26
+ ## 2.4.2 (2020-04-16)
27
+
28
+ - Added `connections` method
29
+ - Fixed deprecation warnings in Ruby 2.7
30
+
1
31
  ## 2.4.1 (2019-11-21)
2
32
 
3
33
  - Fixed file permissions on `highlight.pack.js`
data/README.md CHANGED
@@ -10,17 +10,21 @@ A performance dashboard for Postgres
10
10
 
11
11
  ---
12
12
 
13
- [![Screenshot](https://pghero.dokkuapp.com/assets/screenshot-5a368624ada55b32e7668c96926840f9.png)](https://pghero.dokkuapp.com/)
13
+ [![Screenshot](https://pghero.dokkuapp.com/assets/pghero-f8abe426e6bf54bb7dba87b425bb809740ebd386208bcd280a7e802b053a1023.png)](https://pghero.dokkuapp.com/)
14
+
15
+ :tangerine: Battle-tested at [Instacart](https://www.instacart.com/opensource)
16
+
17
+ [![Build Status](https://travis-ci.org/ankane/pghero.svg?branch=master)](https://travis-ci.org/ankane/pghero) [![Docker Pulls](https://img.shields.io/docker/pulls/ankane/pghero)](https://hub.docker.com/repository/docker/ankane/pghero)
14
18
 
15
19
  ## Installation
16
20
 
17
- PgHero is available as a Rails engine, Linux package, and Docker image.
21
+ PgHero is available as a Docker image, Linux package, and Rails engine.
18
22
 
19
23
  Select your preferred method of installation to get started.
20
24
 
21
- - [Rails](guides/Rails.md)
22
- - [Linux](guides/Linux.md)
23
25
  - [Docker](guides/Docker.md)
26
+ - [Linux](guides/Linux.md)
27
+ - [Rails](guides/Rails.md)
24
28
 
25
29
  ## Related Projects
26
30
 
@@ -31,10 +35,17 @@ Select your preferred method of installation to get started.
31
35
 
32
36
  ## Credits
33
37
 
34
- A big thanks to [Craig Kerstiens](http://www.craigkerstiens.com/2013/01/10/more-on-postgres-performance/) and [Heroku](https://blog.heroku.com/archives/2013/5/10/more_insight_into_your_database_with_pgextras) for the initial queries and [Bootswatch](https://github.com/thomaspark/bootswatch) for the theme :clap:
38
+ A big thanks to [Craig Kerstiens](http://www.craigkerstiens.com/2013/01/10/more-on-postgres-performance/) and [Heroku](https://blog.heroku.com/archives/2013/5/10/more_insight_into_your_database_with_pgextras) for the initial queries and [Bootswatch](https://github.com/thomaspark/bootswatch) for the theme.
35
39
 
36
- Know a bit about PostgreSQL? [Suggestions](https://github.com/ankane/pghero/issues) are greatly appreciated.
40
+ ## History
37
41
 
38
- :tangerine: Battle-tested at [Instacart](https://www.instacart.com/opensource)
42
+ View the [changelog](https://github.com/ankane/pghero/blob/master/CHANGELOG.md)
43
+
44
+ ## Contributing
45
+
46
+ Everyone is encouraged to help improve this project. Here are a few ways you can help:
39
47
 
40
- [![Build Status](https://travis-ci.org/ankane/pghero.svg?branch=master)](https://travis-ci.org/ankane/pghero)
48
+ - [Report bugs](https://github.com/ankane/pghero/issues)
49
+ - Fix bugs and [submit pull requests](https://github.com/ankane/pghero/pulls)
50
+ - Write, clarify, or fix documentation
51
+ - Suggest or add new features
@@ -59,7 +59,7 @@ function initSlider() {
59
59
  html = "Now";
60
60
  }
61
61
  } else {
62
- html = months[time.getMonth()] + " " + time.getDate() + ", " + pad(time.getHours()) + ":" + pad(time.getMinutes());
62
+ html = time.getDate() + " " + months[time.getMonth()] + " " + pad(time.getHours()) + ":" + pad(time.getMinutes());
63
63
  }
64
64
  $(selector).html(html);
65
65
  }
@@ -2,7 +2,7 @@ module PgHero
2
2
  class HomeController < ActionController::Base
3
3
  layout "pg_hero/application"
4
4
 
5
- protect_from_forgery
5
+ protect_from_forgery with: :exception
6
6
 
7
7
  http_basic_authenticate_with name: PgHero.username, password: PgHero.password if PgHero.password
8
8
 
@@ -13,6 +13,11 @@ module PgHero
13
13
  before_action :ensure_query_stats, only: [:queries]
14
14
 
15
15
  if PgHero.config["override_csp"]
16
+ # note: this does not take into account asset hosts
17
+ # which can be a string with %d or a proc
18
+ # https://api.rubyonrails.org/classes/ActionView/Helpers/AssetUrlHelper.html
19
+ # users should set CSP manually if needed
20
+ # see https://github.com/ankane/pghero/issues/297
16
21
  after_action do
17
22
  response.headers["Content-Security-Policy"] = "default-src 'self' 'unsafe-inline'"
18
23
  end
@@ -198,6 +203,11 @@ module PgHero
198
203
  "1 week" => {duration: 1.week, period: 30.minutes},
199
204
  "2 weeks" => {duration: 2.weeks, period: 1.hours}
200
205
  }
206
+ if @database.system_stats_provider == :azure
207
+ # doesn't support 10, just 5 and 15
208
+ @periods["1 day"][:period] = 15.minutes
209
+ end
210
+
201
211
  @duration = (params[:duration] || 1.hour).to_i
202
212
  @period = (params[:period] || 60.seconds).to_i
203
213
 
@@ -209,22 +219,36 @@ module PgHero
209
219
  end
210
220
 
211
221
  def cpu_usage
212
- render json: [{name: "CPU", data: @database.cpu_usage(system_params).map { |k, v| [k, v.round] }, library: chart_library_options}]
222
+ render json: [{name: "CPU", data: @database.cpu_usage(**system_params).map { |k, v| [k, v ? v.round : v] }, library: chart_library_options}]
213
223
  end
214
224
 
215
225
  def connection_stats
216
- render json: [{name: "Connections", data: @database.connection_stats(system_params), library: chart_library_options}]
226
+ render json: [{name: "Connections", data: @database.connection_stats(**system_params), library: chart_library_options}]
217
227
  end
218
228
 
219
229
  def replication_lag_stats
220
- render json: [{name: "Lag", data: @database.replication_lag_stats(system_params), library: chart_library_options}]
230
+ render json: [{name: "Lag", data: @database.replication_lag_stats(**system_params), library: chart_library_options}]
221
231
  end
222
232
 
223
233
  def load_stats
224
- render json: [
225
- {name: "Read IOPS", data: @database.read_iops_stats(system_params).map { |k, v| [k, v.round] }, library: chart_library_options},
226
- {name: "Write IOPS", data: @database.write_iops_stats(system_params).map { |k, v| [k, v.round] }, library: chart_library_options}
227
- ]
234
+ stats =
235
+ case @database.system_stats_provider
236
+ when :azure
237
+ [
238
+ {name: "IO Consumption", data: @database.azure_stats("io_consumption_percent", **system_params), library: chart_library_options}
239
+ ]
240
+ when :gcp
241
+ [
242
+ {name: "Read Ops", data: @database.read_iops_stats(**system_params).map { |k, v| [k, v ? v.round : v] }, library: chart_library_options},
243
+ {name: "Write Ops", data: @database.write_iops_stats(**system_params).map { |k, v| [k, v ? v.round : v] }, library: chart_library_options}
244
+ ]
245
+ else
246
+ [
247
+ {name: "Read IOPS", data: @database.read_iops_stats(**system_params).map { |k, v| [k, v ? v.round : v] }, library: chart_library_options},
248
+ {name: "Write IOPS", data: @database.write_iops_stats(**system_params).map { |k, v| [k, v ? v.round : v] }, library: chart_library_options}
249
+ ]
250
+ end
251
+ render json: stats
228
252
  end
229
253
 
230
254
  def free_space_stats
@@ -270,17 +294,48 @@ module PgHero
270
294
 
271
295
  def connections
272
296
  @title = "Connections"
273
- @connection_sources = @database.connection_sources
274
- @total_connections = @connection_sources.sum { |cs| cs[:total_connections] }
297
+ connections = @database.connections
298
+
299
+ @total_connections = connections.count
300
+ @connection_sources = group_connections(connections, [:database, :user, :source, :ip])
301
+ @connections_by_database = group_connections_by_key(connections, :database)
302
+ @connections_by_user = group_connections_by_key(connections, :user)
303
+
304
+ if params[:security] && @database.server_version_num >= 90500
305
+ connections.each do |connection|
306
+ connection[:ssl_status] =
307
+ if connection[:ssl]
308
+ # no way to tell if client used verify-full
309
+ # so connection may not be actually secure
310
+ "SSL"
311
+ else
312
+ # variety of reasons for no SSL
313
+ if !connection[:database].present?
314
+ "Internal Process"
315
+ elsif !connection[:ip]
316
+ if connection[:state]
317
+ "Socket"
318
+ else
319
+ # tcp or socket, don't have permission to tell
320
+ "No SSL"
321
+ end
322
+ else
323
+ # tcp
324
+ # could separate out localhost since this should be safe
325
+ "No SSL"
326
+ end
327
+ end
328
+ end
275
329
 
276
- @connections_by_database = group_connections(@connection_sources, :database)
277
- @connections_by_user = group_connections(@connection_sources, :user)
330
+ @connections_by_ssl_status = group_connections_by_key(connections, :ssl_status)
331
+ end
278
332
  end
279
333
 
280
334
  def maintenance
281
335
  @title = "Maintenance"
282
336
  @maintenance_info = @database.maintenance_info
283
337
  @time_zone = PgHero.time_zone
338
+ @show_dead_rows = params[:dead_rows]
284
339
  end
285
340
 
286
341
  def kill
@@ -363,7 +418,8 @@ module PgHero
363
418
  def system_params
364
419
  {
365
420
  duration: params[:duration],
366
- period: params[:period]
421
+ period: params[:period],
422
+ series: true
367
423
  }.delete_if { |_, v| v.nil? }
368
424
  end
369
425
 
@@ -376,18 +432,22 @@ module PgHero
376
432
  @show_details = @historical_query_stats_enabled && @database.supports_query_hash?
377
433
  end
378
434
 
379
- def group_connections(connection_sources, key)
380
- top_connections = Hash.new(0)
381
- connection_sources.each do |source|
382
- top_connections[source[key]] += source[:total_connections]
383
- end
384
- top_connections.sort_by { |k, v| [-v, k] }
435
+ def group_connections(connections, keys)
436
+ connections
437
+ .group_by { |conn| conn.slice(*keys) }
438
+ .map { |k, v| k.merge(total_connections: v.count) }
439
+ .sort_by { |v| [-v[:total_connections]] + keys.map { |k| v[k].to_s } }
440
+ end
441
+
442
+ def group_connections_by_key(connections, key)
443
+ group_connections(connections, [key]).map { |v| [v[key], v[:total_connections]] }.to_h
385
444
  end
386
445
 
387
446
  def check_api
388
447
  render_text "No support for Rails API. See https://github.com/pghero/pghero for a standalone app." if Rails.application.config.try(:api_only)
389
448
  end
390
449
 
450
+ # TODO return error status code
391
451
  def render_text(message)
392
452
  render plain: message
393
453
  end
@@ -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
@@ -30,7 +30,9 @@
30
30
  <% end %>
31
31
  </td>
32
32
  <td class="text-right">
33
- <%= 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 %>
34
36
  <%= button_to "Kill", kill_path(pid: query[:pid]), class: "btn btn-danger" %>
35
37
  </td>
36
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>
@@ -387,7 +387,7 @@
387
387
  <pre>rails generate migration remove_unneeded_indexes</pre>
388
388
  <p>And paste</p>
389
389
  <pre style="overflow: scroll; white-space: pre; word-break: normal;"><% @duplicate_indexes.each do |query| %>
390
- 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>
391
391
  </div>
392
392
 
393
393
  <table class="table duplicate-indexes">
@@ -491,7 +491,7 @@ pg_stat_statements.track = all</pre>
491
491
  <pre>rails generate migration remove_unused_indexes</pre>
492
492
  <p>And paste</p>
493
493
  <pre style="overflow: scroll; white-space: pre; word-break: normal;"><% @unused_indexes.each do |query| %>
494
- remove_index <%= query[:table].to_sym.inspect %>, name: <%= query[:index].to_s.inspect %><% end %></pre>
494
+ <%= pghero_remove_index(query)%><% end %></pre>
495
495
  </div>
496
496
 
497
497
  <table class="table">
@@ -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>
@@ -33,7 +33,7 @@
33
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
 
@@ -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 %>
@@ -3,6 +3,11 @@ databases:
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"] %>
@@ -27,13 +32,15 @@ databases:
27
32
 
28
33
  # Basic authentication
29
34
  # username: admin
30
- # password: secret
35
+ # password: <%%= ENV["PGHERO_PASSWORD"] %>
31
36
 
32
37
  # Stats database URL (defaults to app database)
33
38
  # stats_database_url: <%%= ENV["PGHERO_STATS_DATABASE_URL"] %>
34
39
 
35
40
  # AWS configuration (defaults to app AWS config)
36
- # also need aws_db_instance_identifier with each database
37
- # aws_access_key_id: ...
38
- # aws_secret_access_key: ...
41
+ # aws_access_key_id: <%%= ENV["AWS_ACCESS_KEY_ID"] %>
42
+ # aws_secret_access_key: <%%= ENV["AWS_SECRET_ACCESS_KEY"] %>
39
43
  # aws_region: us-east-1
44
+
45
+ # Filter data from queries (experimental)
46
+ # filter_data: true
@@ -34,24 +34,27 @@ module PgHero
34
34
  class Error < StandardError; end
35
35
  class NotEnabled < Error; end
36
36
 
37
+ MUTEX = Mutex.new
38
+
37
39
  # settings
38
40
  class << self
39
- 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
40
42
  end
41
43
  self.long_running_query_sec = (ENV["PGHERO_LONG_RUNNING_QUERY_SEC"] || 60).to_i
42
44
  self.slow_query_ms = (ENV["PGHERO_SLOW_QUERY_MS"] || 20).to_i
43
45
  self.slow_query_calls = (ENV["PGHERO_SLOW_QUERY_CALLS"] || 100).to_i
44
- self.explain_timeout_sec = (ENV["PGHERO_EXPLAIN_TIMEOUT_SEC"] || 10).to_i
46
+ self.explain_timeout_sec = (ENV["PGHERO_EXPLAIN_TIMEOUT_SEC"] || 10).to_f
45
47
  self.total_connections_threshold = (ENV["PGHERO_TOTAL_CONNECTIONS_THRESHOLD"] || 500).to_i
46
48
  self.cache_hit_rate_threshold = 99
47
49
  self.env = ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
48
50
  self.show_migrations = true
49
51
  self.config_path = ENV["PGHERO_CONFIG_PATH"] || "config/pghero.yml"
52
+ self.filter_data = ENV["PGHERO_FILTER_DATA"].to_s.size > 0
50
53
 
51
54
  class << self
52
55
  extend Forwardable
53
56
  def_delegators :primary_database, :access_key_id, :analyze, :analyze_tables, :autoindex, :autovacuum_danger,
54
- :best_index, :blocked_queries, :connection_sources, :connection_states, :connection_stats,
57
+ :best_index, :blocked_queries, :connections, :connection_sources, :connection_states, :connection_stats,
55
58
  :cpu_usage, :create_user, :database_size, :db_instance_identifier, :disable_query_stats, :drop_user,
56
59
  :duplicate_indexes, :enable_query_stats, :explain, :historical_query_stats_enabled?, :index_caching,
57
60
  :index_hit_rate, :index_usage, :indexes, :invalid_constraints, :invalid_indexes, :kill, :kill_all, :kill_long_running_queries,
@@ -116,11 +119,18 @@ module PgHero
116
119
 
117
120
  if databases.empty?
118
121
  databases["primary"] = {
119
- "url" => ENV["PGHERO_DATABASE_URL"] || ActiveRecord::Base.connection_config,
120
- "db_instance_identifier" => ENV["PGHERO_DB_INSTANCE_IDENTIFIER"]
122
+ "url" => ENV["PGHERO_DATABASE_URL"] || ActiveRecord::Base.connection_config
121
123
  }
122
124
  end
123
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
+
124
134
  {
125
135
  "databases" => databases
126
136
  }
@@ -128,14 +138,20 @@ module PgHero
128
138
  end
129
139
  end
130
140
 
141
+ # ensure we only have one copy of databases
142
+ # so there's only one connection pool per database
131
143
  def databases
132
- @databases ||= begin
133
- Hash[
134
- config["databases"].map do |id, c|
135
- [id.to_sym, PgHero::Database.new(id, c)]
136
- end
137
- ]
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
138
152
  end
153
+
154
+ @databases
139
155
  end
140
156
 
141
157
  def primary_database
@@ -180,13 +196,13 @@ module PgHero
180
196
  # stats for old databases are not cleaned up since we can't use an index
181
197
  def clean_query_stats
182
198
  each_database do |database|
183
- PgHero::QueryStats.where(database: database.id).where("captured_at < ?", 14.days.ago).delete_all
199
+ database.clean_query_stats
184
200
  end
185
201
  end
186
202
 
187
203
  def clean_space_stats
188
204
  each_database do |database|
189
- PgHero::SpaceStats.where(database: database.id).where("captured_at < ?", 90.days.ago).delete_all
205
+ database.clean_space_stats
190
206
  end
191
207
  end
192
208