pghero 2.8.3 → 3.3.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +47 -0
- data/LICENSE.txt +1 -1
- data/app/assets/javascripts/pghero/Chart.bundle.js +23379 -19766
- data/app/assets/javascripts/pghero/application.js +13 -12
- data/app/assets/javascripts/pghero/chartkick.js +834 -764
- data/app/assets/javascripts/pghero/highlight.min.js +440 -0
- data/app/assets/javascripts/pghero/jquery.js +318 -197
- data/app/assets/javascripts/pghero/nouislider.js +676 -1066
- data/app/assets/stylesheets/pghero/application.css +8 -2
- data/app/assets/stylesheets/pghero/nouislider.css +4 -10
- data/app/controllers/pg_hero/home_controller.rb +103 -37
- data/app/helpers/pg_hero/home_helper.rb +2 -2
- data/app/views/layouts/pg_hero/application.html.erb +4 -2
- data/app/views/pg_hero/home/_query_stats_slider.html.erb +6 -6
- data/app/views/pg_hero/home/connections.html.erb +6 -6
- data/app/views/pg_hero/home/explain.html.erb +4 -2
- data/app/views/pg_hero/home/index.html.erb +3 -1
- data/app/views/pg_hero/home/queries.html.erb +4 -2
- data/app/views/pg_hero/home/relation_space.html.erb +1 -1
- data/app/views/pg_hero/home/show_query.html.erb +17 -13
- data/app/views/pg_hero/home/space.html.erb +44 -40
- data/app/views/pg_hero/home/system.html.erb +6 -6
- data/lib/generators/pghero/query_stats_generator.rb +1 -0
- data/lib/generators/pghero/space_stats_generator.rb +1 -0
- data/lib/generators/pghero/templates/config.yml.tt +6 -0
- data/lib/pghero/database.rb +0 -7
- data/lib/pghero/engine.rb +1 -1
- data/lib/pghero/methods/basic.rb +6 -9
- data/lib/pghero/methods/connections.rb +5 -5
- data/lib/pghero/methods/constraints.rb +1 -1
- data/lib/pghero/methods/explain.rb +34 -0
- data/lib/pghero/methods/indexes.rb +8 -8
- data/lib/pghero/methods/kill.rb +1 -1
- data/lib/pghero/methods/maintenance.rb +4 -4
- data/lib/pghero/methods/queries.rb +2 -2
- data/lib/pghero/methods/query_stats.rb +28 -29
- data/lib/pghero/methods/replication.rb +2 -2
- data/lib/pghero/methods/sequences.rb +3 -3
- data/lib/pghero/methods/settings.rb +1 -1
- data/lib/pghero/methods/space.rb +20 -14
- data/lib/pghero/methods/suggested_indexes.rb +40 -110
- data/lib/pghero/methods/system.rb +16 -16
- data/lib/pghero/methods/tables.rb +4 -5
- data/lib/pghero/methods/users.rb +12 -4
- data/lib/pghero/version.rb +1 -1
- data/lib/pghero.rb +90 -65
- data/lib/tasks/pghero.rake +11 -1
- data/licenses/LICENSE-chart.js.txt +1 -1
- data/licenses/LICENSE-date-fns.txt +21 -20
- data/licenses/LICENSE-kurkle-color.txt +9 -0
- metadata +9 -8
- 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("
|
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|
|
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 =
|
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 =
|
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] =
|
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
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
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
|
-
|
224
|
-
|
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
|
-
|
260
|
-
|
261
|
-
|
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
|
-
|
230
|
+
sort = (select.sort_clause ? parse_sort(select.sort_clause) : []) rescue []
|
264
231
|
|
265
|
-
|
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
|
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
|
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|
|
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.
|
336
|
-
[{column: aexpr.lexpr.column_ref.fields.last.string.
|
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.
|
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
|
-
|
346
|
-
|
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
|
378
|
-
direction: v
|
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
|
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
|
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:
|
46
|
-
if
|
47
|
-
aws_options[:access_key_id] =
|
48
|
-
aws_options[: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
|
-
|
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
|
-
|
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:
|
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
|
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
|
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
|
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
|
47
|
+
select_all <<~SQL
|
49
48
|
SELECT
|
50
49
|
nspname AS schema,
|
51
50
|
relname AS table,
|
data/lib/pghero/methods/users.rb
CHANGED
@@ -2,11 +2,15 @@ module PgHero
|
|
2
2
|
module Methods
|
3
3
|
module Users
|
4
4
|
# documented as unsafe to pass user input
|
5
|
-
#
|
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
|
-
#
|
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,
|
88
|
+
def table_grant_commands(privilege, tables, quoted_user)
|
81
89
|
tables.map do |table|
|
82
|
-
"GRANT #{privilege} ON TABLE #{table} TO #{
|
90
|
+
"GRANT #{privilege} ON TABLE #{quote_ident(table)} TO #{quoted_user}"
|
83
91
|
end
|
84
92
|
end
|
85
93
|
end
|
data/lib/pghero/version.rb
CHANGED
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
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
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, :
|
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, :
|
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, :
|
64
|
-
:reset_query_stats, :reset_stats, :running_queries, :
|
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 ||=
|
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 ||=
|
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.
|
124
|
+
config = YAML.safe_load(ERB.new(File.read(path)).result) if config_file_exists
|
103
125
|
config ||= {}
|
104
126
|
|
105
|
-
|
106
|
-
config[env]
|
107
|
-
|
108
|
-
config
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
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
|
-
|
121
|
-
|
122
|
-
"url" => ENV["PGHERO_DATABASE_URL"] || connection_config(ActiveRecord::Base)
|
123
|
-
}
|
124
|
-
end
|
139
|
+
@file_config
|
140
|
+
end
|
125
141
|
|
126
|
-
|
127
|
-
|
128
|
-
|
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
|
-
|
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
|
data/lib/tasks/pghero.rake
CHANGED
@@ -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
|
-
|
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-
|
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
|
|