pghero 2.8.3 → 3.0.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +15 -0
- data/LICENSE.txt +1 -1
- data/app/controllers/pg_hero/home_controller.rb +23 -14
- data/app/views/pg_hero/home/explain.html.erb +1 -1
- data/lib/generators/pghero/templates/config.yml.tt +3 -0
- data/lib/pghero/database.rb +0 -7
- data/lib/pghero/methods/basic.rb +1 -1
- data/lib/pghero/methods/connections.rb +1 -1
- data/lib/pghero/methods/maintenance.rb +1 -1
- data/lib/pghero/methods/query_stats.rb +7 -9
- data/lib/pghero/methods/sequences.rb +1 -1
- data/lib/pghero/methods/settings.rb +1 -1
- data/lib/pghero/methods/space.rb +2 -2
- data/lib/pghero/methods/suggested_indexes.rb +31 -106
- data/lib/pghero/methods/system.rb +6 -12
- data/lib/pghero/methods/users.rb +12 -4
- data/lib/pghero/version.rb +1 -1
- data/lib/pghero.rb +52 -39
- metadata +6 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 955b2f0edab155c06ade561b2af26b0d706dfab70380771f838ae81f1bd74fb5
|
4
|
+
data.tar.gz: fb5465a01d42913c74198299db7b42f3a8f35562ac8ba358a30c7e81bb1a3c1c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2bc793666bab662f974b20a4c05e5e99513af633cbd50eeed584bef4dd21ad77b01a2d36410f8456f78b3cf7e1296ffca3165e590e6d90113224acde543b4546
|
7
|
+
data.tar.gz: 5a1df4a76599ecc98fdc86f62553eef161a407d4ef84ccf4a2c7edfd50918284d336857a8fd5e0a3d9ae0bf6e4c826416c65572b29403c0e63e89f41f10b413f
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,18 @@
|
|
1
|
+
## 3.0.1 (2022-10-09)
|
2
|
+
|
3
|
+
- Fixed message when database user does not have permission to reset query stats
|
4
|
+
|
5
|
+
## 3.0.0 (2022-09-13)
|
6
|
+
|
7
|
+
- Changed `capture_query_stats` to only reset stats for current database in Postgres 12+
|
8
|
+
- Changed `reset_query_stats` to only reset stats for current database (use `reset_instance_query_stats` to reset stats for entire instance)
|
9
|
+
- Added `visualize_url` option to config
|
10
|
+
- Removed `access_key_id`, `secret_access_key`, `region`, and `db_instance_identifier` methods (use `aws_` prefixed methods instead)
|
11
|
+
- Dropped support for Linux packages for EOL versions
|
12
|
+
- Dropped support for Ruby < 2.7 and Rails < 6
|
13
|
+
- Dropped support for pg_query < 2
|
14
|
+
- Dropped support for aws-sdk < 2
|
15
|
+
|
1
16
|
## 2.8.3 (2022-05-01)
|
2
17
|
|
3
18
|
- Added support for `google-apis-monitoring_v3`
|
data/LICENSE.txt
CHANGED
@@ -84,7 +84,7 @@ module PgHero
|
|
84
84
|
@space_stats_enabled = @database.space_stats_enabled? && !@only_tables
|
85
85
|
if @space_stats_enabled
|
86
86
|
space_growth = @database.space_growth(days: @days, relation_sizes: @relation_sizes)
|
87
|
-
@growth_bytes_by_relation =
|
87
|
+
@growth_bytes_by_relation = space_growth.to_h { |r| [[r[:schema], r[:relation]], r[:growth_bytes]] }
|
88
88
|
if params[:sort] == "growth"
|
89
89
|
@relation_sizes.sort_by! { |r| s = @growth_bytes_by_relation[[r[:schema], r[:relation]]]; [s ? 0 : 1, -s.to_i, r[:schema], r[:relation]] }
|
90
90
|
end
|
@@ -184,7 +184,7 @@ module PgHero
|
|
184
184
|
@chart2_data = [{name: "Value", data: query_hash_stats.map { |r| [r[:captured_at].change(sec: 0), r[:average_time].round(1)] }, library: chart_library_options}]
|
185
185
|
@chart3_data = [{name: "Value", data: query_hash_stats.map { |r| [r[:captured_at].change(sec: 0), r[:calls]] }, library: chart_library_options}]
|
186
186
|
|
187
|
-
@origins =
|
187
|
+
@origins = query_hash_stats.group_by { |r| r[:origin].to_s }.to_h { |k, v| [k, v.size] }
|
188
188
|
@total_count = query_hash_stats.size
|
189
189
|
end
|
190
190
|
|
@@ -192,11 +192,11 @@ module PgHero
|
|
192
192
|
@tables.sort!
|
193
193
|
|
194
194
|
if @tables.any?
|
195
|
-
@row_counts =
|
195
|
+
@row_counts = @database.table_stats(table: @tables).to_h { |i| [i[:table], i[:estimated_rows]] }
|
196
196
|
@indexes_by_table = @database.indexes.group_by { |i| i[:table] }
|
197
197
|
end
|
198
198
|
else
|
199
|
-
render_text "Unknown query"
|
199
|
+
render_text "Unknown query", status: :not_found
|
200
200
|
end
|
201
201
|
end
|
202
202
|
|
@@ -217,9 +217,9 @@ module PgHero
|
|
217
217
|
@period = (params[:period] || 60.seconds).to_i
|
218
218
|
|
219
219
|
if @duration / @period > 1440
|
220
|
-
render_text "Too many data points"
|
220
|
+
render_text "Too many data points", status: :bad_request
|
221
221
|
elsif @period % 60 != 0
|
222
|
-
render_text "Period must be a multiple of 60"
|
222
|
+
render_text "Period must be a multiple of 60", status: :bad_request
|
223
223
|
end
|
224
224
|
end
|
225
225
|
|
@@ -369,10 +369,18 @@ module PgHero
|
|
369
369
|
end
|
370
370
|
|
371
371
|
def reset_query_stats
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
372
|
+
success =
|
373
|
+
if @database.server_version_num >= 120000
|
374
|
+
@database.reset_query_stats
|
375
|
+
else
|
376
|
+
@database.reset_instance_query_stats
|
377
|
+
end
|
378
|
+
|
379
|
+
if success
|
380
|
+
redirect_backward notice: "Query stats reset"
|
381
|
+
else
|
382
|
+
redirect_backward alert: "The database user does not have permission to reset query stats"
|
383
|
+
end
|
376
384
|
end
|
377
385
|
|
378
386
|
protected
|
@@ -445,12 +453,13 @@ module PgHero
|
|
445
453
|
end
|
446
454
|
|
447
455
|
def check_api
|
448
|
-
|
456
|
+
if Rails.application.config.try(:api_only)
|
457
|
+
render_text "No support for Rails API. See https://github.com/pghero/pghero for a standalone app.", status: :internal_server_error
|
458
|
+
end
|
449
459
|
end
|
450
460
|
|
451
|
-
|
452
|
-
|
453
|
-
render plain: message
|
461
|
+
def render_text(message, status:)
|
462
|
+
render plain: message, status: status
|
454
463
|
end
|
455
464
|
|
456
465
|
def ensure_query_stats
|
@@ -12,7 +12,7 @@
|
|
12
12
|
|
13
13
|
<% if @explanation %>
|
14
14
|
<% if @visualize %>
|
15
|
-
<p>Paste the output below into the <%= link_to "
|
15
|
+
<p>Paste the output below into the <%= link_to "explain visualizer", PgHero.visualize_url, target: "_blank" %></p>
|
16
16
|
<% end %>
|
17
17
|
<pre><code><%= @explanation %></code></pre>
|
18
18
|
<% unless @visualize %>
|
data/lib/pghero/database.rb
CHANGED
@@ -118,12 +118,6 @@ module PgHero
|
|
118
118
|
@filter_data
|
119
119
|
end
|
120
120
|
|
121
|
-
# TODO remove in next major version
|
122
|
-
alias_method :access_key_id, :aws_access_key_id
|
123
|
-
alias_method :secret_access_key, :aws_secret_access_key
|
124
|
-
alias_method :region, :aws_region
|
125
|
-
alias_method :db_instance_identifier, :aws_db_instance_identifier
|
126
|
-
|
127
121
|
private
|
128
122
|
|
129
123
|
# check adapter lazily
|
@@ -148,7 +142,6 @@ module PgHero
|
|
148
142
|
|
149
143
|
# resolve spec
|
150
144
|
if !url && config["spec"]
|
151
|
-
raise Error, "Spec requires Rails 6+" unless PgHero.spec_supported?
|
152
145
|
config_options = {env_name: PgHero.env, PgHero.spec_name_key => config["spec"], PgHero.include_replicas_key => true}
|
153
146
|
resolved = ActiveRecord::Base.configurations.configs_for(**config_options)
|
154
147
|
raise Error, "Spec not found: #{config["spec"]}" unless resolved
|
data/lib/pghero/methods/basic.rb
CHANGED
@@ -45,7 +45,7 @@ module PgHero
|
|
45
45
|
if ActiveRecord::VERSION::STRING.to_f >= 6.1
|
46
46
|
result = result.map(&:symbolize_keys)
|
47
47
|
else
|
48
|
-
result = result.map { |row|
|
48
|
+
result = result.map { |row| row.to_h { |col, val| [col.to_sym, result.column_types[col].send(:cast_value, val)] } }
|
49
49
|
end
|
50
50
|
if filter_data
|
51
51
|
query_columns.each do |column|
|
@@ -1,7 +1,7 @@
|
|
1
1
|
module PgHero
|
2
2
|
module Methods
|
3
3
|
module Maintenance
|
4
|
-
# https://www.postgresql.org/docs/
|
4
|
+
# https://www.postgresql.org/docs/current/routine-vacuuming.html#VACUUM-FOR-WRAPAROUND
|
5
5
|
# "the system will shut down and refuse to start any new transactions
|
6
6
|
# once there are fewer than 1 million transactions left until wraparound"
|
7
7
|
# warn when 10,000,000 transactions left
|
@@ -56,10 +56,9 @@ module PgHero
|
|
56
56
|
true
|
57
57
|
end
|
58
58
|
|
59
|
-
# TODO scope by database in PgHero 3.0
|
60
|
-
# (add database: database_name to options)
|
61
59
|
def reset_query_stats(**options)
|
62
|
-
reset_instance_query_stats(
|
60
|
+
raise PgHero::Error, "Use reset_instance_query_stats to pass database" if options.delete(:database)
|
61
|
+
reset_instance_query_stats(**options, database: database_name)
|
63
62
|
end
|
64
63
|
|
65
64
|
# resets query stats for the entire instance
|
@@ -121,7 +120,7 @@ module PgHero
|
|
121
120
|
server_version_num >= 90400
|
122
121
|
end
|
123
122
|
|
124
|
-
# resetting query stats will reset across the entire Postgres instance
|
123
|
+
# resetting query stats will reset across the entire Postgres instance in Postgres < 12
|
125
124
|
# this is problematic if multiple PgHero databases use the same Postgres instance
|
126
125
|
#
|
127
126
|
# to get around this, we capture queries for every Postgres database before we
|
@@ -147,16 +146,15 @@ module PgHero
|
|
147
146
|
# nothing to do
|
148
147
|
return if query_stats.empty?
|
149
148
|
|
150
|
-
#
|
151
|
-
|
152
|
-
if false # mapping.size == 1 && server_version_num >= 120000
|
149
|
+
# reset individual databases for Postgres 12+ instance
|
150
|
+
if server_version_num >= 120000
|
153
151
|
query_stats.each do |db_id, db_query_stats|
|
154
|
-
if
|
152
|
+
if reset_instance_query_stats(database: mapping[db_id], raise_errors: raise_errors)
|
155
153
|
insert_query_stats(db_id, db_query_stats, now)
|
156
154
|
end
|
157
155
|
end
|
158
156
|
else
|
159
|
-
if
|
157
|
+
if reset_instance_query_stats(raise_errors: raise_errors)
|
160
158
|
query_stats.each do |db_id, db_query_stats|
|
161
159
|
insert_query_stats(db_id, db_query_stats, now)
|
162
160
|
end
|
@@ -115,7 +115,7 @@ module PgHero
|
|
115
115
|
end
|
116
116
|
|
117
117
|
# then populate attributes
|
118
|
-
readable =
|
118
|
+
readable = sequence_attributes.to_h { |s| [[s[:schema], s[:sequence]], s[:readable]] }
|
119
119
|
sequences.each do |sequence|
|
120
120
|
sequence[:readable] = readable[[sequence[:schema], sequence[:sequence]]] || false
|
121
121
|
end
|
data/lib/pghero/methods/space.rb
CHANGED
@@ -49,7 +49,7 @@ module PgHero
|
|
49
49
|
def space_growth(days: 7, relation_sizes: nil)
|
50
50
|
if space_stats_enabled?
|
51
51
|
relation_sizes ||= self.relation_sizes
|
52
|
-
sizes =
|
52
|
+
sizes = relation_sizes.to_h { |r| [[r[:schema], r[:relation]], r[:size_bytes]] }
|
53
53
|
start_at = days.days.ago
|
54
54
|
|
55
55
|
stats = select_all_stats <<-SQL
|
@@ -92,7 +92,7 @@ module PgHero
|
|
92
92
|
def relation_space_stats(relation, schema: "public")
|
93
93
|
if space_stats_enabled?
|
94
94
|
relation_sizes ||= self.relation_sizes
|
95
|
-
sizes =
|
95
|
+
sizes = relation_sizes.map { |r| [[r[:schema], r[:relation]], r[:size_bytes]] }.to_h
|
96
96
|
start_at = 30.days.ago
|
97
97
|
|
98
98
|
stats = select_all_stats <<-SQL
|
@@ -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
|
@@ -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
|
@@ -202,68 +202,33 @@ module PgHero
|
|
202
202
|
return {error: "Parse error"}
|
203
203
|
end
|
204
204
|
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
"JOIN not supported yet"
|
221
|
-
end
|
222
|
-
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
|
205
|
+
return {error: "Unknown structure"} unless tree.stmts.size == 1
|
206
|
+
|
207
|
+
tree = tree.stmts.first.stmt
|
208
|
+
|
209
|
+
table = parse_table(tree) rescue nil
|
210
|
+
unless table
|
211
|
+
error =
|
212
|
+
case tree.node
|
213
|
+
when :insert_stmt
|
214
|
+
"INSERT statement"
|
215
|
+
when :variable_set_stmt
|
216
|
+
"SET statement"
|
217
|
+
when :select_stmt
|
218
|
+
if (tree.select_stmt.from_clause.first.join_expr rescue false)
|
219
|
+
"JOIN not supported yet"
|
255
220
|
end
|
256
|
-
|
257
|
-
|
221
|
+
end
|
222
|
+
return {error: error || "Unknown structure"}
|
223
|
+
end
|
258
224
|
|
259
|
-
|
260
|
-
|
261
|
-
|
225
|
+
select = tree[tree.node.to_s]
|
226
|
+
where = (select.where_clause ? parse_where(select.where_clause) : []) rescue nil
|
227
|
+
return {error: "Unknown structure"} unless where
|
262
228
|
|
263
|
-
|
229
|
+
sort = (select.sort_clause ? parse_sort(select.sort_clause) : []) rescue []
|
264
230
|
|
265
|
-
|
266
|
-
end
|
231
|
+
{table: table, where: where, sort: sort}
|
267
232
|
end
|
268
233
|
|
269
234
|
# TODO better row estimation
|
@@ -300,7 +265,7 @@ module PgHero
|
|
300
265
|
end
|
301
266
|
end
|
302
267
|
|
303
|
-
def
|
268
|
+
def parse_table(tree)
|
304
269
|
case tree.node
|
305
270
|
when :select_stmt
|
306
271
|
tree.select_stmt.from_clause.first.range_var.relname
|
@@ -311,24 +276,13 @@ module PgHero
|
|
311
276
|
end
|
312
277
|
end
|
313
278
|
|
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
279
|
# TODO capture values
|
326
|
-
def
|
280
|
+
def parse_where(tree)
|
327
281
|
aexpr = tree.a_expr
|
328
282
|
|
329
283
|
if tree.bool_expr
|
330
284
|
if tree.bool_expr.boolop == :AND_EXPR
|
331
|
-
tree.bool_expr.args.flat_map { |v|
|
285
|
+
tree.bool_expr.args.flat_map { |v| parse_where(v) }
|
332
286
|
else
|
333
287
|
raise "Not Implemented"
|
334
288
|
end
|
@@ -342,27 +296,7 @@ module PgHero
|
|
342
296
|
end
|
343
297
|
end
|
344
298
|
|
345
|
-
|
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)
|
299
|
+
def parse_sort(sort_clause)
|
366
300
|
sort_clause.map do |v|
|
367
301
|
{
|
368
302
|
column: v.sort_by.node.column_ref.fields.last.string.str,
|
@@ -371,15 +305,6 @@ module PgHero
|
|
371
305
|
end
|
372
306
|
end
|
373
307
|
|
374
|
-
def parse_sort(sort_clause)
|
375
|
-
sort_clause.map do |v|
|
376
|
-
{
|
377
|
-
column: v["SortBy"]["node"]["ColumnRef"]["fields"].last["String"]["str"],
|
378
|
-
direction: v["SortBy"]["sortby_dir"] == 2 ? "desc" : "asc"
|
379
|
-
}
|
380
|
-
end
|
381
|
-
end
|
382
|
-
|
383
308
|
def column_stats(schema: nil, table: nil)
|
384
309
|
select_all <<-SQL
|
385
310
|
SELECT
|
@@ -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
|
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
@@ -53,15 +53,15 @@ module PgHero
|
|
53
53
|
|
54
54
|
class << self
|
55
55
|
extend Forwardable
|
56
|
-
def_delegators :primary_database, :
|
56
|
+
def_delegators :primary_database, :aws_access_key_id, :analyze, :analyze_tables, :autoindex, :autovacuum_danger,
|
57
57
|
:best_index, :blocked_queries, :connections, :connection_sources, :connection_states, :connection_stats,
|
58
|
-
:cpu_usage, :create_user, :database_size, :
|
58
|
+
:cpu_usage, :create_user, :database_size, :aws_db_instance_identifier, :disable_query_stats, :drop_user,
|
59
59
|
:duplicate_indexes, :enable_query_stats, :explain, :historical_query_stats_enabled?, :index_caching,
|
60
60
|
:index_hit_rate, :index_usage, :indexes, :invalid_constraints, :invalid_indexes, :kill, :kill_all, :kill_long_running_queries,
|
61
61
|
:last_stats_reset_time, :long_running_queries, :maintenance_info, :missing_indexes, :query_stats,
|
62
62
|
: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, :
|
63
|
+
:rds_stats, :read_iops_stats, :aws_region, :relation_sizes, :replica?, :replication_lag, :replication_lag_stats,
|
64
|
+
:reset_query_stats, :reset_stats, :running_queries, :aws_secret_access_key, :sequence_danger, :sequences, :settings,
|
65
65
|
:slow_queries, :space_growth, :ssl_used?, :stats_connection, :suggested_indexes, :suggested_indexes_by_query,
|
66
66
|
:suggested_indexes_enabled?, :system_stats_enabled?, :table_caching, :table_hit_rate, :table_stats,
|
67
67
|
:total_connections, :transaction_id_danger, :unused_indexes, :unused_tables, :write_iops_stats
|
@@ -86,12 +86,22 @@ module PgHero
|
|
86
86
|
@password ||= config["password"] || ENV["PGHERO_PASSWORD"]
|
87
87
|
end
|
88
88
|
|
89
|
+
# config pattern for https://github.com/ankane/pghero/issues/424
|
89
90
|
def stats_database_url
|
90
|
-
@stats_database_url ||=
|
91
|
+
@stats_database_url ||= (file_config || {})["stats_database_url"] || ENV["PGHERO_STATS_DATABASE_URL"]
|
92
|
+
end
|
93
|
+
|
94
|
+
def visualize_url
|
95
|
+
@visualize_url ||= config["visualize_url"] || ENV["PGHERO_VISUALIZE_URL"] || "https://tatiyants.com/pev/#/plans/new"
|
91
96
|
end
|
92
97
|
|
93
98
|
def config
|
94
|
-
@config ||=
|
99
|
+
@config ||= file_config || default_config
|
100
|
+
end
|
101
|
+
|
102
|
+
# private
|
103
|
+
def file_config
|
104
|
+
unless defined?(@file_config)
|
95
105
|
require "erb"
|
96
106
|
require "yaml"
|
97
107
|
|
@@ -102,40 +112,48 @@ module PgHero
|
|
102
112
|
config = YAML.load(ERB.new(File.read(path)).result) if config_file_exists
|
103
113
|
config ||= {}
|
104
114
|
|
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
|
115
|
+
@file_config =
|
116
|
+
if config[env]
|
117
|
+
config[env]
|
118
|
+
elsif config["databases"] # preferred format
|
119
|
+
config
|
120
|
+
elsif config_file_exists
|
121
|
+
raise "Invalid config file"
|
122
|
+
else
|
123
|
+
nil
|
118
124
|
end
|
125
|
+
end
|
119
126
|
|
120
|
-
|
121
|
-
|
122
|
-
"url" => ENV["PGHERO_DATABASE_URL"] || connection_config(ActiveRecord::Base)
|
123
|
-
}
|
124
|
-
end
|
127
|
+
@file_config
|
128
|
+
end
|
125
129
|
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
"gcp_database_id" => ENV["PGHERO_GCP_DATABASE_ID"],
|
130
|
-
"azure_resource_id" => ENV["PGHERO_AZURE_RESOURCE_ID"]
|
131
|
-
)
|
132
|
-
end
|
130
|
+
# private
|
131
|
+
def default_config
|
132
|
+
databases = {}
|
133
133
|
|
134
|
-
|
135
|
-
|
136
|
-
}
|
134
|
+
unless ENV["PGHERO_DATABASE_URL"]
|
135
|
+
ActiveRecord::Base.configurations.configs_for(env_name: env, include_replicas_key => true).each do |db|
|
136
|
+
databases[db.send(spec_name_key)] = {"spec" => db.send(spec_name_key)}
|
137
137
|
end
|
138
138
|
end
|
139
|
+
|
140
|
+
if databases.empty?
|
141
|
+
databases["primary"] = {
|
142
|
+
"url" => ENV["PGHERO_DATABASE_URL"] || connection_config(ActiveRecord::Base)
|
143
|
+
}
|
144
|
+
end
|
145
|
+
|
146
|
+
if databases.size == 1
|
147
|
+
databases.values.first.merge!(
|
148
|
+
"aws_db_instance_identifier" => ENV["PGHERO_DB_INSTANCE_IDENTIFIER"],
|
149
|
+
"gcp_database_id" => ENV["PGHERO_GCP_DATABASE_ID"],
|
150
|
+
"azure_resource_id" => ENV["PGHERO_AZURE_RESOURCE_ID"]
|
151
|
+
)
|
152
|
+
end
|
153
|
+
|
154
|
+
{
|
155
|
+
"databases" => databases
|
156
|
+
}
|
139
157
|
end
|
140
158
|
|
141
159
|
# ensure we only have one copy of databases
|
@@ -206,11 +224,6 @@ module PgHero
|
|
206
224
|
end
|
207
225
|
end
|
208
226
|
|
209
|
-
# private
|
210
|
-
def spec_supported?
|
211
|
-
ActiveRecord::VERSION::MAJOR >= 6
|
212
|
-
end
|
213
|
-
|
214
227
|
# private
|
215
228
|
def connection_config(model)
|
216
229
|
ActiveRecord::VERSION::STRING.to_f >= 6.1 ? model.connection_db_config.configuration_hash : model.connection_config
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pghero
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 3.0.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew Kane
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-
|
11
|
+
date: 2022-10-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -16,16 +16,16 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
19
|
+
version: '6'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '
|
26
|
+
version: '6'
|
27
27
|
description:
|
28
|
-
email: andrew@
|
28
|
+
email: andrew@ankane.org
|
29
29
|
executables: []
|
30
30
|
extensions: []
|
31
31
|
extra_rdoc_files: []
|
@@ -116,7 +116,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
116
116
|
requirements:
|
117
117
|
- - ">="
|
118
118
|
- !ruby/object:Gem::Version
|
119
|
-
version: '2.
|
119
|
+
version: '2.7'
|
120
120
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
121
121
|
requirements:
|
122
122
|
- - ">="
|