pghero 3.0.0 → 3.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +12 -0
- data/LICENSE.txt +1 -1
- data/app/controllers/pg_hero/home_controller.rb +49 -17
- data/app/views/layouts/pg_hero/application.html.erb +3 -1
- data/app/views/pg_hero/home/explain.html.erb +3 -1
- data/app/views/pg_hero/home/show_query.html.erb +1 -1
- data/lib/generators/pghero/templates/config.yml.tt +6 -0
- data/lib/pghero/methods/explain.rb +34 -0
- data/lib/pghero/methods/query_stats.rb +2 -1
- data/lib/pghero/version.rb +1 -1
- data/lib/pghero.rb +10 -0
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: eb0dc9f78c0e8095569b48f1d6d48235238ea92efa8a94326f97639d2fa30d38
|
4
|
+
data.tar.gz: 452e6f781c32bcb4d6b68a6b4bda62425023587c7779a88f34c0905b09ac93c9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f22dc29263e1b6d7c008747bc6907dd125d3d8f53141c4ad030d6acd19ef0d501abeb1db148f190115ad99d4c6a6910cbb7d4b1239a14b77c8a838ae204beb17
|
7
|
+
data.tar.gz: 628a7ffcafd87be24626ffdd171fb4c00a77367548b6e47693cd18265177918a774f8ac02af4c27c15dfc712b41fc88c50269777b868eab412ecc954d6442fd5
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,15 @@
|
|
1
|
+
## 3.1.0 (2023-01-04)
|
2
|
+
|
3
|
+
- Fixed explain error message leaking data - [more info](https://github.com/ankane/pghero/issues/439)
|
4
|
+
- Explain analyze is now opt-in - [more info](https://github.com/ankane/pghero/issues/438)
|
5
|
+
- Added support for disabling explain and explain analyze
|
6
|
+
- Added support for visualize without explain analyze
|
7
|
+
- Added `explain_v2` method
|
8
|
+
|
9
|
+
## 3.0.1 (2022-10-09)
|
10
|
+
|
11
|
+
- Fixed message when database user does not have permission to reset query stats
|
12
|
+
|
1
13
|
## 3.0.0 (2022-09-13)
|
2
14
|
|
3
15
|
- Changed `capture_query_stats` to only reset stats for current database in Postgres 12+
|
data/LICENSE.txt
CHANGED
@@ -263,30 +263,57 @@ module PgHero
|
|
263
263
|
end
|
264
264
|
|
265
265
|
def explain
|
266
|
+
unless @explain_enabled
|
267
|
+
render_text "Explain not enabled", status: :bad_request
|
268
|
+
return
|
269
|
+
end
|
270
|
+
|
266
271
|
@title = "Explain"
|
267
272
|
@query = params[:query]
|
273
|
+
@explain_analyze_enabled = PgHero.explain_mode == "analyze"
|
274
|
+
|
268
275
|
# TODO use get + token instead of post so users can share links
|
269
276
|
# need to prevent CSRF and DoS
|
270
|
-
if request.post? && @query
|
277
|
+
if request.post? && @query.present?
|
271
278
|
begin
|
272
|
-
|
279
|
+
explain_options =
|
273
280
|
case params[:commit]
|
274
281
|
when "Analyze"
|
275
|
-
|
282
|
+
{analyze: true}
|
276
283
|
when "Visualize"
|
277
|
-
|
284
|
+
if @explain_analyze_enabled
|
285
|
+
{analyze: true, costs: true, verbose: true, buffers: true, format: "json"}
|
286
|
+
else
|
287
|
+
{costs: true, verbose: true, format: "json"}
|
288
|
+
end
|
278
289
|
else
|
279
|
-
|
290
|
+
{}
|
280
291
|
end
|
281
|
-
|
292
|
+
|
293
|
+
if explain_options[:analyze] && !@explain_analyze_enabled
|
294
|
+
render_text "Explain analyze not enabled", status: :bad_request
|
295
|
+
return
|
296
|
+
end
|
297
|
+
|
298
|
+
@explanation = @database.explain_v2(@query, **explain_options)
|
282
299
|
@suggested_index = @database.suggested_indexes(queries: [@query]).first if @database.suggested_indexes_enabled?
|
283
300
|
@visualize = params[:commit] == "Visualize"
|
284
301
|
rescue ActiveRecord::StatementInvalid => e
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
302
|
+
message = e.message
|
303
|
+
@error =
|
304
|
+
if message == "Unsafe statement"
|
305
|
+
"Unsafe statement"
|
306
|
+
elsif message.start_with?("PG::ProtocolViolation: ERROR: bind message supplies 0 parameters")
|
307
|
+
"Can't explain queries with bind parameters"
|
308
|
+
elsif message.start_with?("PG::SyntaxError")
|
309
|
+
"Syntax error with query"
|
310
|
+
elsif message.start_with?("PG::QueryCanceled")
|
311
|
+
"Query timed out"
|
312
|
+
else
|
313
|
+
# default to a generic message
|
314
|
+
# since data can be extracted through the Postgres error message
|
315
|
+
"Error explaining query"
|
316
|
+
end
|
290
317
|
end
|
291
318
|
end
|
292
319
|
end
|
@@ -369,14 +396,18 @@ module PgHero
|
|
369
396
|
end
|
370
397
|
|
371
398
|
def reset_query_stats
|
372
|
-
|
373
|
-
@database.
|
399
|
+
success =
|
400
|
+
if @database.server_version_num >= 120000
|
401
|
+
@database.reset_query_stats
|
402
|
+
else
|
403
|
+
@database.reset_instance_query_stats
|
404
|
+
end
|
405
|
+
|
406
|
+
if success
|
407
|
+
redirect_backward notice: "Query stats reset"
|
374
408
|
else
|
375
|
-
|
409
|
+
redirect_backward alert: "The database user does not have permission to reset query stats"
|
376
410
|
end
|
377
|
-
redirect_backward notice: "Query stats reset"
|
378
|
-
rescue ActiveRecord::StatementInvalid
|
379
|
-
redirect_backward alert: "The database user does not have permission to reset query stats"
|
380
411
|
end
|
381
412
|
|
382
413
|
protected
|
@@ -405,6 +436,7 @@ module PgHero
|
|
405
436
|
@query_stats_enabled = @database.query_stats_enabled?
|
406
437
|
@system_stats_enabled = @database.system_stats_enabled?
|
407
438
|
@replica = @database.replica?
|
439
|
+
@explain_enabled = PgHero.explain_enabled?
|
408
440
|
end
|
409
441
|
|
410
442
|
def set_suggested_indexes(min_average_time = 0, min_calls = 0)
|
@@ -48,7 +48,9 @@
|
|
48
48
|
<% unless @database.replica? %>
|
49
49
|
<li class="<%= controller.action_name == "maintenance" ? "active" : "" %>"><%= link_to "Maintenance", maintenance_path %></li>
|
50
50
|
<% end %>
|
51
|
-
|
51
|
+
<% if @explain_enabled %>
|
52
|
+
<li class="<%= controller.action_name == "explain" ? "active" : "" %>"><%= link_to "Explain", explain_path %></li>
|
53
|
+
<% end %>
|
52
54
|
<li class="<%= controller.action_name == "tune" ? "active" : "" %>"><%= link_to "Tune", tune_path %></li>
|
53
55
|
</ul>
|
54
56
|
|
@@ -5,7 +5,9 @@
|
|
5
5
|
<div class="field"><%= text_area_tag :query, @query, placeholder: "Enter a SQL query" %></div>
|
6
6
|
<p>
|
7
7
|
<%= submit_tag "Explain", class: "btn btn-info", style: "margin-right: 10px;" %>
|
8
|
-
|
8
|
+
<% if @explain_analyze_enabled %>
|
9
|
+
<%= submit_tag "Analyze", class: "btn btn-danger", style: "margin-right: 10px;" %>
|
10
|
+
<% end %>
|
9
11
|
<%= submit_tag "Visualize", class: "btn btn-danger" %>
|
10
12
|
</p>
|
11
13
|
<% end %>
|
@@ -24,9 +24,15 @@ databases:
|
|
24
24
|
# Minimum connections for high connections warning
|
25
25
|
# total_connections_threshold: 500
|
26
26
|
|
27
|
+
# Explain functionality
|
28
|
+
# explain: true / false / analyze
|
29
|
+
|
27
30
|
# Statement timeout for explain
|
28
31
|
# explain_timeout_sec: 10
|
29
32
|
|
33
|
+
# Visualize URL for explain
|
34
|
+
# visualize_url: https://...
|
35
|
+
|
30
36
|
# Time zone (defaults to app time zone)
|
31
37
|
# time_zone: "Pacific Time (US & Canada)"
|
32
38
|
|
@@ -1,6 +1,8 @@
|
|
1
1
|
module PgHero
|
2
2
|
module Methods
|
3
3
|
module Explain
|
4
|
+
# TODO remove in 4.0
|
5
|
+
# note: this method is not affected by the explain option
|
4
6
|
def explain(sql)
|
5
7
|
sql = squish(sql)
|
6
8
|
explanation = nil
|
@@ -16,6 +18,23 @@ module PgHero
|
|
16
18
|
explanation
|
17
19
|
end
|
18
20
|
|
21
|
+
# TODO rename to explain in 4.0
|
22
|
+
# note: this method is not affected by the explain option
|
23
|
+
def explain_v2(sql, analyze: nil, verbose: nil, costs: nil, settings: nil, buffers: nil, wal: nil, timing: nil, summary: nil, format: "text")
|
24
|
+
options = []
|
25
|
+
add_explain_option(options, "ANALYZE", analyze)
|
26
|
+
add_explain_option(options, "VERBOSE", verbose)
|
27
|
+
add_explain_option(options, "SETTINGS", settings)
|
28
|
+
add_explain_option(options, "COSTS", costs)
|
29
|
+
add_explain_option(options, "BUFFERS", buffers)
|
30
|
+
add_explain_option(options, "WAL", wal)
|
31
|
+
add_explain_option(options, "TIMING", timing)
|
32
|
+
add_explain_option(options, "SUMMARY", summary)
|
33
|
+
options << "FORMAT #{explain_format(format)}"
|
34
|
+
|
35
|
+
explain("(#{options.join(", ")}) #{sql}")
|
36
|
+
end
|
37
|
+
|
19
38
|
private
|
20
39
|
|
21
40
|
def explain_safe?
|
@@ -24,6 +43,21 @@ module PgHero
|
|
24
43
|
rescue ActiveRecord::StatementInvalid
|
25
44
|
true
|
26
45
|
end
|
46
|
+
|
47
|
+
def add_explain_option(options, name, value)
|
48
|
+
unless value.nil?
|
49
|
+
options << "#{name}#{value ? "" : " FALSE"}"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# important! validate format to prevent injection
|
54
|
+
def explain_format(format)
|
55
|
+
if ["text", "xml", "json", "yaml"].include?(format)
|
56
|
+
format.upcase
|
57
|
+
else
|
58
|
+
raise ArgumentError, "Unknown format"
|
59
|
+
end
|
60
|
+
end
|
27
61
|
end
|
28
62
|
end
|
29
63
|
end
|
@@ -226,6 +226,7 @@ module PgHero
|
|
226
226
|
)
|
227
227
|
SELECT
|
228
228
|
query,
|
229
|
+
query AS explainable_query,
|
229
230
|
query_hash,
|
230
231
|
query_stats.user,
|
231
232
|
total_minutes,
|
@@ -243,7 +244,7 @@ module PgHero
|
|
243
244
|
# we may be able to skip query_columns
|
244
245
|
# in more recent versions of Postgres
|
245
246
|
# as pg_stat_statements should be already normalized
|
246
|
-
select_all(query, query_columns: [:query])
|
247
|
+
select_all(query, query_columns: [:query, :explainable_query])
|
247
248
|
else
|
248
249
|
raise NotEnabled, "Query stats not enabled"
|
249
250
|
end
|
data/lib/pghero/version.rb
CHANGED
data/lib/pghero.rb
CHANGED
@@ -91,6 +91,16 @@ module PgHero
|
|
91
91
|
@stats_database_url ||= (file_config || {})["stats_database_url"] || ENV["PGHERO_STATS_DATABASE_URL"]
|
92
92
|
end
|
93
93
|
|
94
|
+
# private
|
95
|
+
def explain_enabled?
|
96
|
+
explain_mode.nil? || explain_mode == true || explain_mode == "analyze"
|
97
|
+
end
|
98
|
+
|
99
|
+
# private
|
100
|
+
def explain_mode
|
101
|
+
@config["explain"]
|
102
|
+
end
|
103
|
+
|
94
104
|
def visualize_url
|
95
105
|
@visualize_url ||= config["visualize_url"] || ENV["PGHERO_VISUALIZE_URL"] || "https://tatiyants.com/pev/#/plans/new"
|
96
106
|
end
|
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: 3.
|
4
|
+
version: 3.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew Kane
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2023-01-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -123,7 +123,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
123
123
|
- !ruby/object:Gem::Version
|
124
124
|
version: '0'
|
125
125
|
requirements: []
|
126
|
-
rubygems_version: 3.
|
126
|
+
rubygems_version: 3.4.1
|
127
127
|
signing_key:
|
128
128
|
specification_version: 4
|
129
129
|
summary: A performance dashboard for Postgres
|