pghero 2.8.3 → 3.3.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +47 -0
  3. data/LICENSE.txt +1 -1
  4. data/app/assets/javascripts/pghero/Chart.bundle.js +23379 -19766
  5. data/app/assets/javascripts/pghero/application.js +13 -12
  6. data/app/assets/javascripts/pghero/chartkick.js +834 -764
  7. data/app/assets/javascripts/pghero/highlight.min.js +440 -0
  8. data/app/assets/javascripts/pghero/jquery.js +318 -197
  9. data/app/assets/javascripts/pghero/nouislider.js +676 -1066
  10. data/app/assets/stylesheets/pghero/application.css +8 -2
  11. data/app/assets/stylesheets/pghero/nouislider.css +4 -10
  12. data/app/controllers/pg_hero/home_controller.rb +103 -37
  13. data/app/helpers/pg_hero/home_helper.rb +2 -2
  14. data/app/views/layouts/pg_hero/application.html.erb +4 -2
  15. data/app/views/pg_hero/home/_query_stats_slider.html.erb +6 -6
  16. data/app/views/pg_hero/home/connections.html.erb +6 -6
  17. data/app/views/pg_hero/home/explain.html.erb +4 -2
  18. data/app/views/pg_hero/home/index.html.erb +3 -1
  19. data/app/views/pg_hero/home/queries.html.erb +4 -2
  20. data/app/views/pg_hero/home/relation_space.html.erb +1 -1
  21. data/app/views/pg_hero/home/show_query.html.erb +17 -13
  22. data/app/views/pg_hero/home/space.html.erb +44 -40
  23. data/app/views/pg_hero/home/system.html.erb +6 -6
  24. data/lib/generators/pghero/query_stats_generator.rb +1 -0
  25. data/lib/generators/pghero/space_stats_generator.rb +1 -0
  26. data/lib/generators/pghero/templates/config.yml.tt +6 -0
  27. data/lib/pghero/database.rb +0 -7
  28. data/lib/pghero/engine.rb +1 -1
  29. data/lib/pghero/methods/basic.rb +6 -9
  30. data/lib/pghero/methods/connections.rb +5 -5
  31. data/lib/pghero/methods/constraints.rb +1 -1
  32. data/lib/pghero/methods/explain.rb +34 -0
  33. data/lib/pghero/methods/indexes.rb +8 -8
  34. data/lib/pghero/methods/kill.rb +1 -1
  35. data/lib/pghero/methods/maintenance.rb +4 -4
  36. data/lib/pghero/methods/queries.rb +2 -2
  37. data/lib/pghero/methods/query_stats.rb +28 -29
  38. data/lib/pghero/methods/replication.rb +2 -2
  39. data/lib/pghero/methods/sequences.rb +3 -3
  40. data/lib/pghero/methods/settings.rb +1 -1
  41. data/lib/pghero/methods/space.rb +20 -14
  42. data/lib/pghero/methods/suggested_indexes.rb +40 -110
  43. data/lib/pghero/methods/system.rb +16 -16
  44. data/lib/pghero/methods/tables.rb +4 -5
  45. data/lib/pghero/methods/users.rb +12 -4
  46. data/lib/pghero/version.rb +1 -1
  47. data/lib/pghero.rb +90 -65
  48. data/lib/tasks/pghero.rake +11 -1
  49. data/licenses/LICENSE-chart.js.txt +1 -1
  50. data/licenses/LICENSE-date-fns.txt +21 -20
  51. data/licenses/LICENSE-kurkle-color.txt +9 -0
  52. metadata +9 -8
  53. data/app/assets/javascripts/pghero/highlight.pack.js +0 -2
@@ -2,7 +2,7 @@ module PgHero
2
2
  module Methods
3
3
  module SuggestedIndexes
4
4
  def suggested_indexes_enabled?
5
- defined?(PgQuery) && Gem::Version.new(PgQuery::VERSION) >= Gem::Version.new("0.9.0") && query_stats_enabled?
5
+ defined?(PgQuery) && Gem::Version.new(PgQuery::VERSION) >= Gem::Version.new("2") && query_stats_enabled?
6
6
  end
7
7
 
8
8
  # TODO clean this mess
@@ -79,7 +79,7 @@ module PgHero
79
79
  suggested_indexes.each do |index|
80
80
  p index
81
81
  if create
82
- connection.execute("CREATE INDEX CONCURRENTLY ON #{quote_table_name(index[:table])} (#{index[:columns].map { |c| quote_table_name(c) }.join(",")})")
82
+ connection.execute("CREATE INDEX CONCURRENTLY ON #{quote_table_name(index[:table])} (#{index[:columns].map { |c| quote_column_name(c) }.join(",")})")
83
83
  end
84
84
  end
85
85
  end
@@ -104,7 +104,7 @@ module PgHero
104
104
  # TODO get schema from query structure, then try search path
105
105
  schema = PgHero.connection_config(connection_model)[:schema] || "public"
106
106
  if tables.any?
107
- row_stats = Hash[table_stats(table: tables, schema: schema).map { |i| [i[:table], i[:estimated_rows]] }]
107
+ row_stats = table_stats(table: tables, schema: schema).to_h { |i| [i[:table], i[:estimated_rows]] }
108
108
  col_stats = column_stats(table: tables, schema: schema).group_by { |i| i[:table] }
109
109
  end
110
110
 
@@ -126,7 +126,7 @@ module PgHero
126
126
  total_rows = row_stats[table].to_i
127
127
  index[:rows] = total_rows
128
128
 
129
- ranks = Hash[col_stats[table].to_a.map { |r| [r[:column], r] }]
129
+ ranks = col_stats[table].to_a.to_h { |r| [r[:column], r] }
130
130
  columns = (where + sort).map { |c| c[:column] }.uniq
131
131
 
132
132
  if columns.any?
@@ -135,7 +135,7 @@ module PgHero
135
135
  sort = sort.first(first_desc + 1) if first_desc
136
136
  where = where.sort_by { |c| [row_estimates(ranks[c[:column]], total_rows, total_rows, c[:op]), c[:column]] } + sort
137
137
 
138
- index[:row_estimates] = Hash[where.map { |c| ["#{c[:column]} (#{c[:op] || "sort"})", row_estimates(ranks[c[:column]], total_rows, total_rows, c[:op]).round] }]
138
+ index[:row_estimates] = where.to_h { |c| ["#{c[:column]} (#{c[:op] || "sort"})", row_estimates(ranks[c[:column]], total_rows, total_rows, c[:op]).round] }
139
139
 
140
140
  # no index needed if less than 500 rows
141
141
  if total_rows >= 500
@@ -194,6 +194,7 @@ module PgHero
194
194
  end
195
195
 
196
196
  def best_index_structure(statement)
197
+ return {error: "Empty statement"} if statement.to_s.empty?
197
198
  return {error: "Too large"} if statement.to_s.length > 10000
198
199
 
199
200
  begin
@@ -202,68 +203,33 @@ module PgHero
202
203
  return {error: "Parse error"}
203
204
  end
204
205
 
205
- if PgQuery::VERSION.to_i >= 2
206
- return {error: "Unknown structure"} unless tree.stmts.size == 1
207
-
208
- tree = tree.stmts.first.stmt
209
-
210
- table = parse_table_v2(tree) rescue nil
211
- unless table
212
- error =
213
- case tree.node
214
- when :insert_stmt
215
- "INSERT statement"
216
- when :variable_set_stmt
217
- "SET statement"
218
- when :select_stmt
219
- if (tree.select_stmt.from_clause.first.join_expr rescue false)
220
- "JOIN not supported yet"
221
- end
206
+ return {error: "Unknown structure"} unless tree.stmts.size == 1
207
+
208
+ tree = tree.stmts.first.stmt
209
+
210
+ table = parse_table(tree) rescue nil
211
+ unless table
212
+ error =
213
+ case tree.node
214
+ when :insert_stmt
215
+ "INSERT statement"
216
+ when :variable_set_stmt
217
+ "SET statement"
218
+ when :select_stmt
219
+ if (tree.select_stmt.from_clause.first.join_expr rescue false)
220
+ "JOIN not supported yet"
222
221
  end
223
- return {error: error || "Unknown structure"}
224
- end
225
-
226
- select = tree[tree.node.to_s]
227
- where = (select.where_clause ? parse_where_v2(select.where_clause) : []) rescue nil
228
- return {error: "Unknown structure"} unless where
229
-
230
- sort = (select.sort_clause ? parse_sort_v2(select.sort_clause) : []) rescue []
231
-
232
- {table: table, where: where, sort: sort}
233
- else
234
- # TODO remove support for pg_query < 2 in PgHero 3.0
235
-
236
- return {error: "Unknown structure"} unless tree.size == 1
237
-
238
- tree = tree.first
239
-
240
- # pg_query 1.0.0
241
- tree = tree["RawStmt"]["stmt"] if tree["RawStmt"]
242
-
243
- table = parse_table(tree) rescue nil
244
- unless table
245
- error =
246
- case tree.keys.first
247
- when "InsertStmt"
248
- "INSERT statement"
249
- when "VariableSetStmt"
250
- "SET statement"
251
- when "SelectStmt"
252
- if (tree["SelectStmt"]["fromClause"].first["JoinExpr"] rescue false)
253
- "JOIN not supported yet"
254
- end
255
- end
256
- return {error: error || "Unknown structure"}
257
- end
222
+ end
223
+ return {error: error || "Unknown structure"}
224
+ end
258
225
 
259
- select = tree.values.first
260
- where = (select["whereClause"] ? parse_where(select["whereClause"]) : []) rescue nil
261
- return {error: "Unknown structure"} unless where
226
+ select = tree[tree.node.to_s]
227
+ where = (select.where_clause ? parse_where(select.where_clause) : []) rescue nil
228
+ return {error: "Unknown structure"} unless where
262
229
 
263
- sort = (select["sortClause"] ? parse_sort(select["sortClause"]) : []) rescue []
230
+ sort = (select.sort_clause ? parse_sort(select.sort_clause) : []) rescue []
264
231
 
265
- {table: table, where: where, sort: sort}
266
- end
232
+ {table: table, where: where, sort: sort}
267
233
  end
268
234
 
269
235
  # TODO better row estimation
@@ -300,7 +266,7 @@ module PgHero
300
266
  end
301
267
  end
302
268
 
303
- def parse_table_v2(tree)
269
+ def parse_table(tree)
304
270
  case tree.node
305
271
  when :select_stmt
306
272
  tree.select_stmt.from_clause.first.range_var.relname
@@ -311,77 +277,41 @@ module PgHero
311
277
  end
312
278
  end
313
279
 
314
- def parse_table(tree)
315
- case tree.keys.first
316
- when "SelectStmt"
317
- tree["SelectStmt"]["fromClause"].first["RangeVar"]["relname"]
318
- when "DeleteStmt"
319
- tree["DeleteStmt"]["relation"]["RangeVar"]["relname"]
320
- when "UpdateStmt"
321
- tree["UpdateStmt"]["relation"]["RangeVar"]["relname"]
322
- end
323
- end
324
-
325
280
  # TODO capture values
326
- def parse_where_v2(tree)
281
+ def parse_where(tree)
327
282
  aexpr = tree.a_expr
328
283
 
329
284
  if tree.bool_expr
330
285
  if tree.bool_expr.boolop == :AND_EXPR
331
- tree.bool_expr.args.flat_map { |v| parse_where_v2(v) }
286
+ tree.bool_expr.args.flat_map { |v| parse_where(v) }
332
287
  else
333
288
  raise "Not Implemented"
334
289
  end
335
- elsif aexpr && ["=", "<>", ">", ">=", "<", "<=", "~~", "~~*", "BETWEEN"].include?(aexpr.name.first.string.str)
336
- [{column: aexpr.lexpr.column_ref.fields.last.string.str, op: aexpr.name.first.string.str}]
290
+ elsif aexpr && ["=", "<>", ">", ">=", "<", "<=", "~~", "~~*", "BETWEEN"].include?(aexpr.name.first.string.send(str_method))
291
+ [{column: aexpr.lexpr.column_ref.fields.last.string.send(str_method), op: aexpr.name.first.string.send(str_method)}]
337
292
  elsif tree.null_test
338
293
  op = tree.null_test.nulltesttype == :IS_NOT_NULL ? "not_null" : "null"
339
- [{column: tree.null_test.arg.column_ref.fields.last.string.str, op: op}]
294
+ [{column: tree.null_test.arg.column_ref.fields.last.string.send(str_method), op: op}]
340
295
  else
341
296
  raise "Not Implemented"
342
297
  end
343
298
  end
344
299
 
345
- # TODO capture values
346
- def parse_where(tree)
347
- aexpr = tree["A_Expr"]
348
-
349
- if tree["BoolExpr"]
350
- if tree["BoolExpr"]["boolop"] == 0
351
- tree["BoolExpr"]["args"].flat_map { |v| parse_where(v) }
352
- else
353
- raise "Not Implemented"
354
- end
355
- elsif aexpr && ["=", "<>", ">", ">=", "<", "<=", "~~", "~~*", "BETWEEN"].include?(aexpr["name"].first["String"]["str"])
356
- [{column: aexpr["lexpr"]["ColumnRef"]["fields"].last["String"]["str"], op: aexpr["name"].first["String"]["str"]}]
357
- elsif tree["NullTest"]
358
- op = tree["NullTest"]["nulltesttype"] == 1 ? "not_null" : "null"
359
- [{column: tree["NullTest"]["arg"]["ColumnRef"]["fields"].last["String"]["str"], op: op}]
360
- else
361
- raise "Not Implemented"
362
- end
363
- end
364
-
365
- def parse_sort_v2(sort_clause)
366
- sort_clause.map do |v|
367
- {
368
- column: v.sort_by.node.column_ref.fields.last.string.str,
369
- direction: v.sort_by.sortby_dir == :SORTBY_DESC ? "desc" : "asc"
370
- }
371
- end
300
+ def str_method
301
+ @str_method ||= Gem::Version.new(PgQuery::VERSION) >= Gem::Version.new("4") ? :sval : :str
372
302
  end
373
303
 
374
304
  def parse_sort(sort_clause)
375
305
  sort_clause.map do |v|
376
306
  {
377
- column: v["SortBy"]["node"]["ColumnRef"]["fields"].last["String"]["str"],
378
- direction: v["SortBy"]["sortby_dir"] == 2 ? "desc" : "asc"
307
+ column: v.sort_by.node.column_ref.fields.last.string.send(str_method),
308
+ direction: v.sort_by.sortby_dir == :SORTBY_DESC ? "desc" : "asc"
379
309
  }
380
310
  end
381
311
  end
382
312
 
383
313
  def column_stats(schema: nil, table: nil)
384
- select_all <<-SQL
314
+ select_all <<~SQL
385
315
  SELECT
386
316
  schemaname AS schema,
387
317
  tablename AS table,
@@ -5,9 +5,8 @@ module PgHero
5
5
  !system_stats_provider.nil?
6
6
  end
7
7
 
8
- # TODO remove defined checks in 3.0
9
8
  def system_stats_provider
10
- if aws_db_instance_identifier && (defined?(Aws) || defined?(AWS))
9
+ if aws_db_instance_identifier
11
10
  :aws
12
11
  elsif gcp_database_id
13
12
  :gcp
@@ -42,18 +41,13 @@ module PgHero
42
41
 
43
42
  def rds_stats(metric_name, duration: nil, period: nil, offset: nil, series: false)
44
43
  if system_stats_enabled?
45
- aws_options = {region: region}
46
- if access_key_id
47
- aws_options[:access_key_id] = access_key_id
48
- aws_options[:secret_access_key] = secret_access_key
44
+ aws_options = {region: aws_region}
45
+ if aws_access_key_id
46
+ aws_options[:access_key_id] = aws_access_key_id
47
+ aws_options[:secret_access_key] = aws_secret_access_key
49
48
  end
50
49
 
51
- client =
52
- if defined?(Aws)
53
- Aws::CloudWatch::Client.new(aws_options)
54
- else
55
- AWS::CloudWatch.new(aws_options).client
56
- end
50
+ client = Aws::CloudWatch::Client.new(aws_options)
57
51
 
58
52
  duration = (duration || 1.hour).to_i
59
53
  period = (period || 1.minute).to_i
@@ -108,7 +102,8 @@ module PgHero
108
102
  end
109
103
 
110
104
  client = Azure::Monitor::Profiles::Latest::Mgmt::Client.new
111
- timespan = "#{start_time.iso8601}/#{end_time.iso8601}"
105
+ # call utc to convert +00:00 to Z
106
+ timespan = "#{start_time.utc.iso8601}/#{end_time.utc.iso8601}"
112
107
  results = client.metrics.list(
113
108
  azure_resource_id,
114
109
  metricnames: metric_name,
@@ -275,12 +270,13 @@ module PgHero
275
270
  used = azure_stats("storage_used", **options)
276
271
  free_space(quota, used)
277
272
  else
278
- # no read_iops, write_iops
279
- # could add io_consumption_percent
273
+ replication_lag_stat = azure_flexible_server? ? "physical_replication_delay_in_seconds" : "pg_replica_log_delay_in_seconds"
280
274
  metrics = {
281
275
  cpu: "cpu_percent",
282
276
  connections: "active_connections",
283
- replication_lag: "pg_replica_log_delay_in_seconds"
277
+ replication_lag: replication_lag_stat,
278
+ read_iops: "read_iops", # flexible server only
279
+ write_iops: "write_iops" # flexible server only
284
280
  }
285
281
  raise Error, "Metric not supported" unless metrics[metric_key]
286
282
  azure_stats(metrics[metric_key], **options)
@@ -290,6 +286,10 @@ module PgHero
290
286
  end
291
287
  end
292
288
 
289
+ def azure_flexible_server?
290
+ azure_resource_id.include?("/Microsoft.DBforPostgreSQL/flexibleServers/")
291
+ end
292
+
293
293
  # only use data points included in both series
294
294
  # this also eliminates need to align Time.now
295
295
  def free_space(quota, used)
@@ -2,17 +2,16 @@ module PgHero
2
2
  module Methods
3
3
  module Tables
4
4
  def table_hit_rate
5
- select_one(<<-SQL
5
+ select_one <<~SQL
6
6
  SELECT
7
7
  sum(heap_blks_hit) / nullif(sum(heap_blks_hit) + sum(heap_blks_read), 0) AS rate
8
8
  FROM
9
9
  pg_statio_user_tables
10
10
  SQL
11
- )
12
11
  end
13
12
 
14
13
  def table_caching
15
- select_all <<-SQL
14
+ select_all <<~SQL
16
15
  SELECT
17
16
  schemaname AS schema,
18
17
  relname AS table,
@@ -29,7 +28,7 @@ module PgHero
29
28
  end
30
29
 
31
30
  def unused_tables
32
- select_all <<-SQL
31
+ select_all <<~SQL
33
32
  SELECT
34
33
  schemaname AS schema,
35
34
  relname AS table,
@@ -45,7 +44,7 @@ module PgHero
45
44
  end
46
45
 
47
46
  def table_stats(schema: nil, table: nil)
48
- select_all <<-SQL
47
+ select_all <<~SQL
49
48
  SELECT
50
49
  nspname AS schema,
51
50
  relname AS table,
@@ -2,11 +2,15 @@ module PgHero
2
2
  module Methods
3
3
  module Users
4
4
  # documented as unsafe to pass user input
5
- # TODO quote in 3.0, but still not officially supported
5
+ # identifiers are now quoted, but still not officially supported
6
6
  def create_user(user, password: nil, schema: "public", database: nil, readonly: false, tables: nil)
7
7
  password ||= random_password
8
8
  database ||= PgHero.connection_config(connection_model)[:database]
9
9
 
10
+ user = quote_ident(user)
11
+ schema = quote_ident(schema)
12
+ database = quote_ident(database)
13
+
10
14
  commands =
11
15
  [
12
16
  "CREATE ROLE #{user} LOGIN PASSWORD #{quote(password)}",
@@ -42,10 +46,14 @@ module PgHero
42
46
  end
43
47
 
44
48
  # documented as unsafe to pass user input
45
- # TODO quote in 3.0, but still not officially supported
49
+ # identifiers are now quoted, but still not officially supported
46
50
  def drop_user(user, schema: "public", database: nil)
47
51
  database ||= PgHero.connection_config(connection_model)[:database]
48
52
 
53
+ user = quote_ident(user)
54
+ schema = quote_ident(schema)
55
+ database = quote_ident(database)
56
+
49
57
  # thanks shiftb
50
58
  commands =
51
59
  [
@@ -77,9 +85,9 @@ module PgHero
77
85
  SecureRandom.base64(40).delete("+/=")[0...24]
78
86
  end
79
87
 
80
- def table_grant_commands(privilege, tables, user)
88
+ def table_grant_commands(privilege, tables, quoted_user)
81
89
  tables.map do |table|
82
- "GRANT #{privilege} ON TABLE #{table} TO #{user}"
90
+ "GRANT #{privilege} ON TABLE #{quote_ident(table)} TO #{quoted_user}"
83
91
  end
84
92
  end
85
93
  end
@@ -1,3 +1,3 @@
1
1
  module PgHero
2
- VERSION = "2.8.3"
2
+ VERSION = "3.3.3"
3
3
  end
data/lib/pghero.rb CHANGED
@@ -1,29 +1,31 @@
1
1
  # dependencies
2
2
  require "active_support"
3
+
4
+ # stdlib
3
5
  require "forwardable"
4
6
 
5
7
  # methods
6
- require "pghero/methods/basic"
7
- require "pghero/methods/connections"
8
- require "pghero/methods/constraints"
9
- require "pghero/methods/explain"
10
- require "pghero/methods/indexes"
11
- require "pghero/methods/kill"
12
- require "pghero/methods/maintenance"
13
- require "pghero/methods/queries"
14
- require "pghero/methods/query_stats"
15
- require "pghero/methods/replication"
16
- require "pghero/methods/sequences"
17
- require "pghero/methods/settings"
18
- require "pghero/methods/space"
19
- require "pghero/methods/suggested_indexes"
20
- require "pghero/methods/system"
21
- require "pghero/methods/tables"
22
- require "pghero/methods/users"
23
-
24
- require "pghero/database"
25
- require "pghero/engine" if defined?(Rails)
26
- require "pghero/version"
8
+ require_relative "pghero/methods/basic"
9
+ require_relative "pghero/methods/connections"
10
+ require_relative "pghero/methods/constraints"
11
+ require_relative "pghero/methods/explain"
12
+ require_relative "pghero/methods/indexes"
13
+ require_relative "pghero/methods/kill"
14
+ require_relative "pghero/methods/maintenance"
15
+ require_relative "pghero/methods/queries"
16
+ require_relative "pghero/methods/query_stats"
17
+ require_relative "pghero/methods/replication"
18
+ require_relative "pghero/methods/sequences"
19
+ require_relative "pghero/methods/settings"
20
+ require_relative "pghero/methods/space"
21
+ require_relative "pghero/methods/suggested_indexes"
22
+ require_relative "pghero/methods/system"
23
+ require_relative "pghero/methods/tables"
24
+ require_relative "pghero/methods/users"
25
+
26
+ require_relative "pghero/database"
27
+ require_relative "pghero/engine" if defined?(Rails)
28
+ require_relative "pghero/version"
27
29
 
28
30
  module PgHero
29
31
  autoload :Connection, "pghero/connection"
@@ -53,15 +55,15 @@ module PgHero
53
55
 
54
56
  class << self
55
57
  extend Forwardable
56
- def_delegators :primary_database, :access_key_id, :analyze, :analyze_tables, :autoindex, :autovacuum_danger,
58
+ def_delegators :primary_database, :aws_access_key_id, :analyze, :analyze_tables, :autoindex, :autovacuum_danger,
57
59
  :best_index, :blocked_queries, :connections, :connection_sources, :connection_states, :connection_stats,
58
- :cpu_usage, :create_user, :database_size, :db_instance_identifier, :disable_query_stats, :drop_user,
60
+ :cpu_usage, :create_user, :database_size, :aws_db_instance_identifier, :disable_query_stats, :drop_user,
59
61
  :duplicate_indexes, :enable_query_stats, :explain, :historical_query_stats_enabled?, :index_caching,
60
62
  :index_hit_rate, :index_usage, :indexes, :invalid_constraints, :invalid_indexes, :kill, :kill_all, :kill_long_running_queries,
61
63
  :last_stats_reset_time, :long_running_queries, :maintenance_info, :missing_indexes, :query_stats,
62
64
  :query_stats_available?, :query_stats_enabled?, :query_stats_extension_enabled?, :query_stats_readable?,
63
- :rds_stats, :read_iops_stats, :region, :relation_sizes, :replica?, :replication_lag, :replication_lag_stats,
64
- :reset_query_stats, :reset_stats, :running_queries, :secret_access_key, :sequence_danger, :sequences, :settings,
65
+ :rds_stats, :read_iops_stats, :aws_region, :relation_sizes, :replica?, :replication_lag, :replication_lag_stats,
66
+ :reset_query_stats, :reset_stats, :running_queries, :aws_secret_access_key, :sequence_danger, :sequences, :settings,
65
67
  :slow_queries, :space_growth, :ssl_used?, :stats_connection, :suggested_indexes, :suggested_indexes_by_query,
66
68
  :suggested_indexes_enabled?, :system_stats_enabled?, :table_caching, :table_hit_rate, :table_stats,
67
69
  :total_connections, :transaction_id_danger, :unused_indexes, :unused_tables, :write_iops_stats
@@ -86,12 +88,32 @@ module PgHero
86
88
  @password ||= config["password"] || ENV["PGHERO_PASSWORD"]
87
89
  end
88
90
 
91
+ # config pattern for https://github.com/ankane/pghero/issues/424
89
92
  def stats_database_url
90
- @stats_database_url ||= config["stats_database_url"] || ENV["PGHERO_STATS_DATABASE_URL"]
93
+ @stats_database_url ||= (file_config || {})["stats_database_url"] || ENV["PGHERO_STATS_DATABASE_URL"]
94
+ end
95
+
96
+ # private
97
+ def explain_enabled?
98
+ explain_mode.nil? || explain_mode == true || explain_mode == "analyze"
99
+ end
100
+
101
+ # private
102
+ def explain_mode
103
+ @config["explain"]
104
+ end
105
+
106
+ def visualize_url
107
+ @visualize_url ||= config["visualize_url"] || ENV["PGHERO_VISUALIZE_URL"] || "https://tatiyants.com/pev/#/plans/new"
91
108
  end
92
109
 
93
110
  def config
94
- @config ||= begin
111
+ @config ||= file_config || default_config
112
+ end
113
+
114
+ # private
115
+ def file_config
116
+ unless defined?(@file_config)
95
117
  require "erb"
96
118
  require "yaml"
97
119
 
@@ -99,43 +121,51 @@ module PgHero
99
121
 
100
122
  config_file_exists = File.exist?(path)
101
123
 
102
- config = YAML.load(ERB.new(File.read(path)).result) if config_file_exists
124
+ config = YAML.safe_load(ERB.new(File.read(path)).result) if config_file_exists
103
125
  config ||= {}
104
126
 
105
- if config[env]
106
- config[env]
107
- elsif config["databases"] # preferred format
108
- config
109
- elsif config_file_exists
110
- raise "Invalid config file"
111
- else
112
- databases = {}
113
-
114
- if !ENV["PGHERO_DATABASE_URL"] && spec_supported?
115
- ActiveRecord::Base.configurations.configs_for(env_name: env, include_replicas_key => true).each do |db|
116
- databases[db.send(spec_name_key)] = {"spec" => db.send(spec_name_key)}
117
- end
127
+ @file_config =
128
+ if config[env]
129
+ config[env]
130
+ elsif config["databases"] # preferred format
131
+ config
132
+ elsif config_file_exists
133
+ raise "Invalid config file"
134
+ else
135
+ nil
118
136
  end
137
+ end
119
138
 
120
- if databases.empty?
121
- databases["primary"] = {
122
- "url" => ENV["PGHERO_DATABASE_URL"] || connection_config(ActiveRecord::Base)
123
- }
124
- end
139
+ @file_config
140
+ end
125
141
 
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
142
+ # private
143
+ def default_config
144
+ databases = {}
133
145
 
134
- {
135
- "databases" => databases
136
- }
146
+ unless ENV["PGHERO_DATABASE_URL"]
147
+ ActiveRecord::Base.configurations.configs_for(env_name: env, include_replicas_key => true).each do |db|
148
+ databases[db.send(spec_name_key)] = {"spec" => db.send(spec_name_key)}
137
149
  end
138
150
  end
151
+
152
+ if databases.empty?
153
+ databases["primary"] = {
154
+ "url" => ENV["PGHERO_DATABASE_URL"] || connection_config(ActiveRecord::Base)
155
+ }
156
+ end
157
+
158
+ if databases.size == 1
159
+ databases.values.first.merge!(
160
+ "aws_db_instance_identifier" => ENV["PGHERO_DB_INSTANCE_IDENTIFIER"],
161
+ "gcp_database_id" => ENV["PGHERO_GCP_DATABASE_ID"],
162
+ "azure_resource_id" => ENV["PGHERO_AZURE_RESOURCE_ID"]
163
+ )
164
+ end
165
+
166
+ {
167
+ "databases" => databases
168
+ }
139
169
  end
140
170
 
141
171
  # ensure we only have one copy of databases
@@ -194,23 +224,18 @@ module PgHero
194
224
  # delete previous stats
195
225
  # go database by database to use an index
196
226
  # stats for old databases are not cleaned up since we can't use an index
197
- def clean_query_stats
227
+ def clean_query_stats(before: nil)
198
228
  each_database do |database|
199
- database.clean_query_stats
229
+ database.clean_query_stats(before: before)
200
230
  end
201
231
  end
202
232
 
203
- def clean_space_stats
233
+ def clean_space_stats(before: nil)
204
234
  each_database do |database|
205
- database.clean_space_stats
235
+ database.clean_space_stats(before: before)
206
236
  end
207
237
  end
208
238
 
209
- # private
210
- def spec_supported?
211
- ActiveRecord::VERSION::MAJOR >= 6
212
- end
213
-
214
239
  # private
215
240
  def connection_config(model)
216
241
  ActiveRecord::VERSION::STRING.to_f >= 6.1 ? model.connection_db_config.configuration_hash : model.connection_config
@@ -22,6 +22,16 @@ namespace :pghero do
22
22
  desc "Remove old query stats"
23
23
  task clean_query_stats: :environment do
24
24
  puts "Deleting old query stats..."
25
- PgHero.clean_query_stats
25
+ options = {}
26
+ options[:before] = Float(ENV["KEEP_DAYS"]).days.ago if ENV["KEEP_DAYS"].present?
27
+ PgHero.clean_query_stats(**options)
28
+ end
29
+
30
+ desc "Remove old space stats"
31
+ task clean_space_stats: :environment do
32
+ puts "Deleting old space stats..."
33
+ options = {}
34
+ options[:before] = Float(ENV["KEEP_DAYS"]).days.ago if ENV["KEEP_DAYS"].present?
35
+ PgHero.clean_space_stats(**options)
26
36
  end
27
37
  end
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2014-2021 Chart.js Contributors
3
+ Copyright (c) 2014-2022 Chart.js Contributors
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
6