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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 47c3b581297903d9228470aeaaf0d292037a607a972f7555c40aca2384db8c70
4
- data.tar.gz: a6f3cba27dd2034de0a24e855c6047966548d7253ead84d6a19395c0d23609dc
3
+ metadata.gz: eb0dc9f78c0e8095569b48f1d6d48235238ea92efa8a94326f97639d2fa30d38
4
+ data.tar.gz: 452e6f781c32bcb4d6b68a6b4bda62425023587c7779a88f34c0905b09ac93c9
5
5
  SHA512:
6
- metadata.gz: '08dd24651f52597743f937956dffca4784589ed9075e2c03cb3319e3d9cc73a71d2eb3409a37e3823b53e23cd1940a50739e95e5357b441ec4b942a6a7a2346c'
7
- data.tar.gz: ca28bc463f7095cebd7e09907aa1e5c1eccd0ab32b288b0963da05344bfcaa292353d8e284636c6d8a642e36514c43419f916bedcfeebe1caca62cdfad3dd3d2
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
@@ -1,4 +1,4 @@
1
- Copyright (c) 2014-2022 Andrew Kane, 2008-2014 Heroku (initial queries)
1
+ Copyright (c) 2014-2023 Andrew Kane, 2008-2014 Heroku (initial queries)
2
2
 
3
3
  MIT License
4
4
 
@@ -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
- prefix =
279
+ explain_options =
273
280
  case params[:commit]
274
281
  when "Analyze"
275
- "ANALYZE "
282
+ {analyze: true}
276
283
  when "Visualize"
277
- "(ANALYZE, COSTS, VERBOSE, BUFFERS, FORMAT JSON) "
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
- @explanation = @database.explain("#{prefix}#{@query}")
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
- @error = e.message
286
-
287
- if @error.include?("bind message supplies 0 parameters")
288
- @error = "Can't explain queries with bind parameters"
289
- end
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
- if @database.server_version_num >= 120000
373
- @database.reset_query_stats
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
- @database.reset_instance_query_stats
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
- <li class="<%= controller.action_name == "explain" ? "active" : "" %>"><%= link_to "Explain", explain_path %></li>
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
- <%= submit_tag "Analyze", class: "btn btn-danger", style: "margin-right: 10px;" %>
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 %>
@@ -4,7 +4,7 @@
4
4
  highlightQueries()
5
5
  </script>
6
6
 
7
- <% if @explainable_query %>
7
+ <% if @explain_enabled && @explainable_query %>
8
8
  <p>
9
9
  <%= button_to "Explain", explain_path, params: {query: @explainable_query}, form: {target: "_blank"}, class: "btn btn-info" %>
10
10
  </p>
@@ -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
@@ -1,3 +1,3 @@
1
1
  module PgHero
2
- VERSION = "3.0.0"
2
+ VERSION = "3.1.0"
3
3
  end
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.0.0
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: 2022-09-13 00:00:00.000000000 Z
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.3.7
126
+ rubygems_version: 3.4.1
127
127
  signing_key:
128
128
  specification_version: 4
129
129
  summary: A performance dashboard for Postgres