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 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