pghero 3.0.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|