pghero 3.7.0 → 4.0.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.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +22 -2
  3. data/LICENSE.txt +1 -1
  4. data/README.md +4 -3
  5. data/app/assets/javascripts/pghero/Chart.bundle.js +2228 -5320
  6. data/app/assets/javascripts/pghero/application.js +104 -68
  7. data/app/assets/javascripts/pghero/highlight.min.js +330 -341
  8. data/app/assets/javascripts/pghero/nouislider.js +64 -5
  9. data/app/assets/stylesheets/pghero/application.css +7 -1
  10. data/app/controllers/pg_hero/home_controller.rb +75 -41
  11. data/app/views/layouts/pg_hero/application.html.erb +1 -1
  12. data/app/views/pg_hero/home/_live_queries_table.html.erb +4 -2
  13. data/app/views/pg_hero/home/_queries_table.html.erb +8 -7
  14. data/app/views/pg_hero/home/_query_stats_slider.html.erb +1 -0
  15. data/app/views/pg_hero/home/_suggested_index.html.erb +3 -3
  16. data/app/views/pg_hero/home/explain.html.erb +2 -1
  17. data/app/views/pg_hero/home/index.html.erb +17 -18
  18. data/app/views/pg_hero/home/index_bloat.html.erb +2 -2
  19. data/app/views/pg_hero/home/live_queries.html.erb +7 -3
  20. data/app/views/pg_hero/home/queries.html.erb +31 -6
  21. data/app/views/pg_hero/home/show_query.html.erb +5 -5
  22. data/app/views/pg_hero/home/space.html.erb +5 -4
  23. data/app/views/pg_hero/home/tune.html.erb +1 -1
  24. data/lib/generators/pghero/templates/config.yml.tt +6 -1
  25. data/lib/generators/pghero/templates/query_stats.rb.tt +7 -1
  26. data/lib/generators/pghero/templates/upgrade_query_stats.rb.tt +23 -0
  27. data/lib/generators/pghero/upgrade_query_stats_generator.rb +19 -0
  28. data/lib/pghero/database.rb +6 -9
  29. data/lib/pghero/engine.rb +3 -10
  30. data/lib/pghero/methods/basic.rb +28 -27
  31. data/lib/pghero/methods/connections.rb +16 -32
  32. data/lib/pghero/methods/explain.rb +13 -20
  33. data/lib/pghero/methods/indexes.rb +8 -5
  34. data/lib/pghero/methods/kill.rb +4 -1
  35. data/lib/pghero/methods/maintenance.rb +14 -20
  36. data/lib/pghero/methods/queries.rb +7 -5
  37. data/lib/pghero/methods/query_stats.rb +264 -238
  38. data/lib/pghero/methods/replication.rb +9 -20
  39. data/lib/pghero/methods/sequences.rb +1 -1
  40. data/lib/pghero/methods/settings.rb +6 -11
  41. data/lib/pghero/methods/space.rb +11 -8
  42. data/lib/pghero/methods/suggested_indexes.rb +15 -16
  43. data/lib/pghero/methods/system.rb +6 -106
  44. data/lib/pghero/methods/tables.rb +7 -3
  45. data/lib/pghero/query.rb +8 -0
  46. data/lib/pghero/query_stats.rb +3 -0
  47. data/lib/pghero/version.rb +1 -1
  48. data/lib/pghero.rb +10 -42
  49. data/licenses/LICENSE-chart.js.txt +1 -1
  50. metadata +8 -8
  51. data/app/assets/javascripts/pghero/jquery.js +0 -10993
  52. data/lib/pghero/methods/users.rb +0 -95
  53. data/licenses/LICENSE-jquery.txt +0 -20
@@ -713,6 +713,7 @@
713
713
  var snap = entry.indexOf("snap") >= 0;
714
714
  var hover = entry.indexOf("hover") >= 0;
715
715
  var unconstrained = entry.indexOf("unconstrained") >= 0;
716
+ var invertConnects = entry.indexOf("invert-connects") >= 0;
716
717
  var dragAll = entry.indexOf("drag-all") >= 0;
717
718
  var smoothSteps = entry.indexOf("smooth-steps") >= 0;
718
719
  if (fixed) {
@@ -722,6 +723,9 @@
722
723
  // Use margin to enforce fixed state
723
724
  testMargin(parsed, parsed.start[1] - parsed.start[0]);
724
725
  }
726
+ if (invertConnects && parsed.handles !== 2) {
727
+ throw new Error("noUiSlider: 'invert-connects' behaviour must be used with 2 handles");
728
+ }
725
729
  if (unconstrained && (parsed.margin || parsed.limit)) {
726
730
  throw new Error("noUiSlider: 'unconstrained' behaviour cannot be used with margin or limit");
727
731
  }
@@ -734,6 +738,7 @@
734
738
  snap: snap,
735
739
  hover: hover,
736
740
  unconstrained: unconstrained,
741
+ invertConnects: invertConnects,
737
742
  };
738
743
  }
739
744
  function testTooltips(parsed, entry) {
@@ -904,6 +909,7 @@
904
909
  // Slider DOM Nodes
905
910
  var scope_Target = target;
906
911
  var scope_Base;
912
+ var scope_ConnectBase;
907
913
  var scope_Handles;
908
914
  var scope_Connects;
909
915
  var scope_Pips;
@@ -915,6 +921,7 @@
915
921
  var scope_HandleNumbers = [];
916
922
  var scope_ActiveHandlesCount = 0;
917
923
  var scope_Events = {};
924
+ var scope_ConnectsInverted = false;
918
925
  // Document Nodes
919
926
  var scope_Document = target.ownerDocument;
920
927
  var scope_DocumentElement = options.documentElement || scope_Document.documentElement;
@@ -971,17 +978,17 @@
971
978
  }
972
979
  // Add handles to the slider base.
973
980
  function addElements(connectOptions, base) {
974
- var connectBase = addNodeTo(base, options.cssClasses.connects);
981
+ scope_ConnectBase = addNodeTo(base, options.cssClasses.connects);
975
982
  scope_Handles = [];
976
983
  scope_Connects = [];
977
- scope_Connects.push(addConnect(connectBase, connectOptions[0]));
984
+ scope_Connects.push(addConnect(scope_ConnectBase, connectOptions[0]));
978
985
  // [::::O====O====O====]
979
986
  // connectOptions = [0, 1, 1, 1]
980
987
  for (var i = 0; i < options.handles; i++) {
981
988
  // Keep a list of all added handles.
982
989
  scope_Handles.push(addOrigin(base, i));
983
990
  scope_HandleNumbers[i] = i;
984
- scope_Connects.push(addConnect(connectBase, connectOptions[i + 1]));
991
+ scope_Connects.push(addConnect(scope_ConnectBase, connectOptions[i + 1]));
985
992
  }
986
993
  }
987
994
  // Initialize a single slider.
@@ -1930,8 +1937,26 @@
1930
1937
  var translation = transformDirection(to, 0) - scope_DirOffset;
1931
1938
  var translateRule = "translate(" + inRuleOrder(translation + "%", "0") + ")";
1932
1939
  scope_Handles[handleNumber].style[options.transformRule] = translateRule;
1940
+ // sanity check for at least 2 handles (e.g. during setup)
1941
+ if (options.events.invertConnects && scope_Locations.length > 1) {
1942
+ // check if handles passed each other, but don't match the ConnectsInverted state
1943
+ var handlesAreInOrder = scope_Locations.every(function (position, index, locations) {
1944
+ return index === 0 || position >= locations[index - 1];
1945
+ });
1946
+ if (scope_ConnectsInverted !== !handlesAreInOrder) {
1947
+ // invert connects when handles pass each other
1948
+ invertConnects();
1949
+ // invertConnects already updates all connect elements
1950
+ return;
1951
+ }
1952
+ }
1933
1953
  updateConnect(handleNumber);
1934
1954
  updateConnect(handleNumber + 1);
1955
+ if (scope_ConnectsInverted) {
1956
+ // When connects are inverted, we also have to update adjacent connects
1957
+ updateConnect(handleNumber - 1);
1958
+ updateConnect(handleNumber + 2);
1959
+ }
1935
1960
  }
1936
1961
  // Handles before the slider middle are stacked later = higher,
1937
1962
  // Handles after the middle later is lower
@@ -1961,13 +1986,20 @@
1961
1986
  if (!scope_Connects[index]) {
1962
1987
  return;
1963
1988
  }
1989
+ // Create a copy of locations, so we can sort them for the local scope logic
1990
+ var locations = scope_Locations.slice();
1991
+ if (scope_ConnectsInverted) {
1992
+ locations.sort(function (a, b) {
1993
+ return a - b;
1994
+ });
1995
+ }
1964
1996
  var l = 0;
1965
1997
  var h = 100;
1966
1998
  if (index !== 0) {
1967
- l = scope_Locations[index - 1];
1999
+ l = locations[index - 1];
1968
2000
  }
1969
2001
  if (index !== scope_Connects.length - 1) {
1970
- h = scope_Locations[index];
2002
+ h = locations[index];
1971
2003
  }
1972
2004
  // We use two rules:
1973
2005
  // 'translate' to change the left/top offset;
@@ -2159,6 +2191,7 @@
2159
2191
  "format",
2160
2192
  "pips",
2161
2193
  "tooltips",
2194
+ "connect",
2162
2195
  ];
2163
2196
  // Only change options that we're actually passed to update.
2164
2197
  updateAble.forEach(function (name) {
@@ -2196,6 +2229,32 @@
2196
2229
  // Invalidate the current positioning so valueSet forces an update.
2197
2230
  scope_Locations = [];
2198
2231
  valueSet(isSet(optionsToUpdate.start) ? optionsToUpdate.start : v, fireSetEvent);
2232
+ // Update connects only if it was set
2233
+ if (optionsToUpdate.connect) {
2234
+ updateConnectOption();
2235
+ }
2236
+ }
2237
+ function updateConnectOption() {
2238
+ // IE supported way of removing children including event handlers
2239
+ while (scope_ConnectBase.firstChild) {
2240
+ scope_ConnectBase.removeChild(scope_ConnectBase.firstChild);
2241
+ }
2242
+ // Adding new connects according to the new connect options
2243
+ for (var i = 0; i <= options.handles; i++) {
2244
+ scope_Connects[i] = addConnect(scope_ConnectBase, options.connect[i]);
2245
+ updateConnect(i);
2246
+ }
2247
+ // re-adding drag events for the new connect elements
2248
+ // to ignore the other events we have to negate the 'if (!behaviour.fixed)' check
2249
+ bindSliderEvents({ drag: options.events.drag, fixed: true });
2250
+ }
2251
+ // Invert options for connect handles
2252
+ function invertConnects() {
2253
+ scope_ConnectsInverted = !scope_ConnectsInverted;
2254
+ testConnect(options,
2255
+ // inverse the connect boolean array
2256
+ options.connect.map(function (b) { return !b; }));
2257
+ updateConnectOption();
2199
2258
  }
2200
2259
  // Initialization steps
2201
2260
  function setupSlider() {
@@ -20,7 +20,7 @@ body {
20
20
  }
21
21
 
22
22
  body {
23
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
23
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
24
24
  line-height: 1.4;
25
25
  color: #333;
26
26
  }
@@ -618,3 +618,9 @@ a.relation-link {
618
618
  color: #f0ad4e;
619
619
  background-color: #333;
620
620
  }
621
+
622
+ #copy {
623
+ float: right;
624
+ margin-top: 10px;
625
+ margin-right: 10px;
626
+ }
@@ -1,16 +1,16 @@
1
1
  module PgHero
2
2
  class HomeController < ActionController::Base
3
- layout "pg_hero/application"
3
+ http_basic_authenticate_with name: PgHero.username, password: PgHero.password if PgHero.password
4
4
 
5
5
  protect_from_forgery with: :exception
6
6
 
7
- http_basic_authenticate_with name: PgHero.username, password: PgHero.password if PgHero.password
8
-
9
7
  before_action :check_api
10
8
  before_action :set_database
9
+ before_action :check_server_version
11
10
  before_action :set_query_stats_enabled
12
11
  before_action :set_show_details, only: [:index, :queries, :show_query]
13
- before_action :ensure_query_stats, only: [:queries]
12
+ before_action :ensure_query_stats, only: [:queries, :show_query, :reset_query_stats]
13
+ before_action :ensure_kill_enabled, only: [:kill, :kill_long_running_queries, :kill_all]
14
14
 
15
15
  if PgHero.config["override_csp"]
16
16
  # note: this does not take into account asset hosts
@@ -23,6 +23,8 @@ module PgHero
23
23
  end
24
24
  end
25
25
 
26
+ layout "pg_hero/application"
27
+
26
28
  def index
27
29
  @title = "Overview"
28
30
  @extended = params[:extended]
@@ -76,7 +78,7 @@ module PgHero
76
78
  @index_hit_rate = @database.index_hit_rate || 0
77
79
  @table_hit_rate = @database.table_hit_rate || 0
78
80
  @good_cache_rate = @table_hit_rate >= @database.cache_hit_rate_threshold / 100.0 && @index_hit_rate >= @database.cache_hit_rate_threshold / 100.0
79
- @unused_indexes = @database.unused_indexes(max_scans: 0)
81
+ @unused_indexes = @database.unused_indexes(max_scans: 0, min_size: @database.unused_index_bytes)
80
82
  end
81
83
 
82
84
  @show_migrations = PgHero.show_migrations
@@ -106,6 +108,7 @@ module PgHero
106
108
  across = params[:across].to_s.split(",")
107
109
  @unused_indexes = @database.unused_indexes(max_scans: 0, across: across)
108
110
  @unused_index_names = Set.new(@unused_indexes.map { |r| r[:index] })
111
+ @unused_indexes = @unused_indexes.select { |r| r[:size_bytes] >= @database.unused_index_bytes }
109
112
  @show_migrations = PgHero.show_migrations
110
113
  @system_stats_enabled = @database.system_stats_enabled?
111
114
  @index_bloat = [] # @database.index_bloat
@@ -116,7 +119,7 @@ module PgHero
116
119
  @relation = params[:relation]
117
120
  @title = @relation
118
121
  relation_space_stats = @database.relation_space_stats(@relation, schema: @schema)
119
- @chart_data = [{name: "Value", data: relation_space_stats.map { |r| [r[:captured_at].change(sec: 0), r[:size_bytes].to_i] }, library: chart_library_options}]
122
+ @chart_data = [{name: "Size", data: relation_space_stats.map { |r| [r[:captured_at].change(sec: 0), r[:size_bytes].to_i] }, library: chart_library_options}]
120
123
  end
121
124
 
122
125
  def index_bloat
@@ -140,6 +143,8 @@ module PgHero
140
143
  @sort = %w(average_time calls).include?(params[:sort]) ? params[:sort] : nil
141
144
  @min_average_time = params[:min_average_time] ? params[:min_average_time].to_i : nil
142
145
  @min_calls = params[:min_calls] ? params[:min_calls].to_i : nil
146
+ @user = params[:user] ? params[:user].to_s : nil
147
+ @link_user = !@user
143
148
 
144
149
  if @historical_query_stats_enabled
145
150
  begin
@@ -150,6 +155,13 @@ module PgHero
150
155
  end
151
156
  end
152
157
 
158
+ @link_options = {sort: @sort}
159
+ if @historical_query_stats_enabled
160
+ # use same format as JavaScript
161
+ @link_options[:start_at] = @start_at.utc.iso8601 if params[:start_at]
162
+ @link_options[:end_at] = @end_at.utc.iso8601 if params[:end_at]
163
+ end
164
+
153
165
  @query_stats =
154
166
  if @historical_query_stats_enabled && !request.xhr?
155
167
  []
@@ -164,6 +176,11 @@ module PgHero
164
176
  )
165
177
  end
166
178
 
179
+ if @user
180
+ # filter in Ruby for accurate percent of total time
181
+ @query_stats = @query_stats.select { |v| v[:user] == @user }
182
+ end
183
+
167
184
  if !@historical_query_stats_enabled || request.xhr?
168
185
  set_suggested_indexes
169
186
  else
@@ -178,21 +195,40 @@ module PgHero
178
195
  end
179
196
 
180
197
  def show_query
181
- @query_hash = params[:query_hash].to_i
182
- @user = params[:user].to_s
183
- @title = @query_hash
198
+ query_hash = params[:query_hash].to_s
199
+
200
+ # v needed for ambiguous values
201
+ hash_format =
202
+ if params[:v] != "2" && query_hash.size <= 20 && /\A-?\d{1,19}\z/.match?(query_hash)
203
+ 1
204
+ elsif query_hash.size == 16 && /\A[0-9a-f]{16}\z/.match?(query_hash)
205
+ 2
206
+ end
207
+
208
+ @query_hash =
209
+ case hash_format
210
+ when 2
211
+ # this format uses big endian to match Postgres to_hex
212
+ [query_hash].pack("H16").unpack1("q>")
213
+ when 1
214
+ query_hash.to_i
215
+ end
216
+
217
+ if @query_hash
218
+ stats = @database.query_stats(historical: true, user: @user, query_hash: @query_hash, start_at: 24.hours.ago).first
219
+ end
184
220
 
185
- stats = @database.query_stats(historical: true, query_hash: @query_hash, start_at: 24.hours.ago).find { |qs| qs[:user] == @user }
186
221
  if stats
187
222
  @query = stats[:query]
188
- @explainable_query = stats[:explainable_query]
223
+ @title = @query.truncate(70)
224
+ @explainable_query = stats[:query] if @database.send(:explainable?, stats[:query])
189
225
 
190
226
  if @show_details
191
- query_hash_stats = @database.query_hash_stats(@query_hash, user: @user, current: true)
227
+ query_hash_stats = @database.query_hash_stats(@query_hash, user: @user)
192
228
 
193
- @chart_data = [{name: "Value", data: query_hash_stats.map { |r| [r[:captured_at].change(sec: 0), (r[:total_minutes] * 60 * 1000).round] }, library: chart_library_options}]
194
- @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}]
195
- @chart3_data = [{name: "Value", data: query_hash_stats.map { |r| [r[:captured_at].change(sec: 0), r[:calls]] }, library: chart_library_options}]
229
+ @chart_data = [{name: "Total Time", data: query_hash_stats.map { |r| [r[:captured_at].change(sec: 0), r[:total_time].round] }, library: chart_library_options}]
230
+ @chart2_data = [{name: "Average Time", data: query_hash_stats.map { |r| [r[:captured_at].change(sec: 0), r[:average_time].round(1)] }, library: chart_library_options}]
231
+ @chart3_data = [{name: "Calls", data: query_hash_stats.map { |r| [r[:captured_at].change(sec: 0), r[:calls]] }, library: chart_library_options}]
196
232
 
197
233
  @origins = query_hash_stats.group_by { |r| r[:origin].to_s }.to_h { |k, v| [k, v.size] }
198
234
  @total_count = query_hash_stats.size
@@ -219,10 +255,6 @@ module PgHero
219
255
  "1 week" => {duration: 1.week, period: 30.minutes},
220
256
  "2 weeks" => {duration: 2.weeks, period: 1.hours}
221
257
  }
222
- if @database.system_stats_provider == :azure
223
- # doesn't support 10, just 5 and 15
224
- @periods["1 day"][:period] = 15.minutes
225
- end
226
258
 
227
259
  @duration = (params[:duration] || 1.hour).to_i
228
260
  @period = (params[:period] || 60.seconds).to_i
@@ -249,17 +281,6 @@ module PgHero
249
281
  def load_stats
250
282
  stats =
251
283
  case @database.system_stats_provider
252
- when :azure
253
- if @database.send(:azure_flexible_server?)
254
- [
255
- {name: "Read IOPS", data: @database.read_iops_stats(**system_params).map { |k, v| [k, v ? v.round : v] }, library: chart_library_options},
256
- {name: "Write IOPS", data: @database.write_iops_stats(**system_params).map { |k, v| [k, v ? v.round : v] }, library: chart_library_options}
257
- ]
258
- else
259
- [
260
- {name: "IO Consumption", data: @database.azure_stats("io_consumption_percent", **system_params), library: chart_library_options}
261
- ]
262
- end
263
284
  when :gcp
264
285
  [
265
286
  {name: "Read Ops", data: @database.read_iops_stats(**system_params).map { |k, v| [k, v ? v.round : v] }, library: chart_library_options},
@@ -317,7 +338,7 @@ module PgHero
317
338
  return
318
339
  end
319
340
 
320
- @explanation = @database.explain_v2(@query, **explain_options)
341
+ @explanation = @database.explain(@query, **explain_options)
321
342
  @suggested_index = @database.suggested_indexes(queries: [@query]).first if @database.suggested_indexes_enabled?
322
343
  @visualize = params[:commit] == "Visualize"
323
344
  rescue ActiveRecord::StatementInvalid => e
@@ -357,7 +378,7 @@ module PgHero
357
378
  @connections_by_database = group_connections_by_key(connections, :database)
358
379
  @connections_by_user = group_connections_by_key(connections, :user)
359
380
 
360
- if params[:security] && @database.server_version_num >= 90500
381
+ if params[:security]
361
382
  connections.each do |connection|
362
383
  connection[:ssl_status] =
363
384
  if connection[:ssl]
@@ -419,14 +440,14 @@ module PgHero
419
440
  redirect_backward alert: "The database user does not have permission to enable query stats"
420
441
  end
421
442
 
422
- # TODO disable if historical query stats enabled?
423
443
  def reset_query_stats
424
- success =
425
- if @database.server_version_num >= 120000
426
- @database.reset_query_stats
427
- else
428
- @database.reset_instance_query_stats
429
- end
444
+ # also disable when upgrading
445
+ if @database.historical_query_stats_enabled? || @database.query_stats_table_exists?
446
+ render_text "Cannot reset when historical query stats are enabled", status: :bad_request
447
+ return
448
+ end
449
+
450
+ success = @database.reset_query_stats
430
451
 
431
452
  if success
432
453
  redirect_backward notice: "Query stats reset"
@@ -437,7 +458,7 @@ module PgHero
437
458
 
438
459
  protected
439
460
 
440
- def redirect_backward(options = {})
461
+ def redirect_backward(**options)
441
462
  redirect_back fallback_location: root_path, **options
442
463
  end
443
464
 
@@ -462,6 +483,7 @@ module PgHero
462
483
  @system_stats_enabled = @database.system_stats_enabled?
463
484
  @replica = @database.replica?
464
485
  @explain_enabled = PgHero.explain_enabled?
486
+ @kill_enabled = PgHero.kill_enabled?
465
487
  end
466
488
 
467
489
  def set_suggested_indexes(min_average_time = 0, min_calls = 0)
@@ -495,7 +517,7 @@ module PgHero
495
517
 
496
518
  def set_show_details
497
519
  @historical_query_stats_enabled = @query_stats_enabled && @database.historical_query_stats_enabled?
498
- @show_details = @historical_query_stats_enabled && @database.supports_query_hash?
520
+ @show_details = @historical_query_stats_enabled
499
521
  end
500
522
 
501
523
  def group_connections(connections, keys)
@@ -515,6 +537,12 @@ module PgHero
515
537
  end
516
538
  end
517
539
 
540
+ def check_server_version
541
+ if @database.server_version_num < 140000
542
+ render_text "Requires PostgreSQL 14+", status: :internal_server_error
543
+ end
544
+ end
545
+
518
546
  def render_text(message, status:)
519
547
  render plain: message, status: status
520
548
  end
@@ -525,6 +553,12 @@ module PgHero
525
553
  end
526
554
  end
527
555
 
556
+ def ensure_kill_enabled
557
+ unless @kill_enabled
558
+ render_text "Kill not enabled", status: :bad_request
559
+ end
560
+ end
561
+
528
562
  # rescue QueryCanceled for case when
529
563
  # statement timeout is less than lock timeout
530
564
  def rescue_timeout(default)
@@ -7,7 +7,7 @@
7
7
  <%= favicon_link_tag "pghero/favicon.png" %>
8
8
  <% if defined?(Propshaft::Railtie) && Rails.application.assets.is_a?(Propshaft::Assembly) %>
9
9
  <%= stylesheet_link_tag "pghero/nouislider", "pghero/arduino-light", "pghero/application" %>
10
- <%= javascript_include_tag "pghero/jquery", "pghero/nouislider", "pghero/Chart.bundle", "pghero/chartkick", "pghero/highlight.min", "pghero/application", nonce: true %>
10
+ <%= javascript_include_tag "pghero/nouislider", "pghero/Chart.bundle", "pghero/chartkick", "pghero/highlight.min", "pghero/application", nonce: true %>
11
11
  <% else %>
12
12
  <%= stylesheet_link_tag "pghero/application" %>
13
13
  <%= javascript_include_tag "pghero/application", nonce: true %>
@@ -35,13 +35,15 @@
35
35
  <% unless @database.filter_data %>
36
36
  <%= button_to "Explain", explain_path, params: {query: query[:query]}, form: {target: "_blank"}, class: "btn btn-info" %>
37
37
  <% end %>
38
- <%= button_to "Kill", kill_path(pid: query[:pid]), class: "btn btn-danger" %>
38
+ <% if @kill_enabled %>
39
+ <%= button_to "Kill", kill_path(pid: query[:pid]), class: "btn btn-danger" %>
40
+ <% end %>
39
41
  </td>
40
42
  </tr>
41
43
  <tr>
42
44
  <td colspan="4" class="query-row">
43
45
  <%= query[:source] %> <span class="text-muted"><%= query[:user] %></span>
44
- <pre class="query-pre"><code><%= query[:query] %></code></pre>
46
+ <pre class="query-pre"><code class="language-pgsql"><%= query[:query] %></code></pre>
45
47
  </td>
46
48
  </tr>
47
49
  <% end %>
@@ -2,9 +2,9 @@
2
2
  <% unless local_assigns[:xhr] %>
3
3
  <thead>
4
4
  <tr>
5
- <th class="width-33"><%= local_assigns[:sort_headers] ? link_to("Total Time", {sort: nil}, data: {sort: nil}) : "Total Time" %></th>
6
- <th class="width-33"><%= local_assigns[:sort_headers] ? link_to("Average Time", {sort: "average_time"}, data: {sort: "average_time"}) : "Average Time" %></th>
7
- <th class="width-33"><%= local_assigns[:sort_headers] ? link_to("Calls", {sort: "calls"}, data: {sort: "calls"}) : "Calls" %></th>
5
+ <th class="width-33"><%= local_assigns[:sort_headers] ? link_to("Total Time", {sort: nil, user: @user}, data: {sort: nil}) : "Total Time" %></th>
6
+ <th class="width-33"><%= local_assigns[:sort_headers] ? link_to("Average Time", {sort: "average_time", user: @user}, data: {sort: "average_time"}) : "Average Time" %></th>
7
+ <th class="width-33"><%= local_assigns[:sort_headers] ? link_to("Calls", {sort: "calls", user: @user}, data: {sort: "calls"}) : "Calls" %></th>
8
8
  </tr>
9
9
  </thead>
10
10
  <% end %>
@@ -25,7 +25,7 @@
25
25
  <% queries.each do |query| %>
26
26
  <tr>
27
27
  <td>
28
- <%= number_with_delimiter(query[:total_minutes].round) %> min
28
+ <%= number_with_delimiter((query[:total_time] / 1000 / 60).round) %> min
29
29
  <span class="percent">
30
30
  <% percent = query[:total_percent] %>
31
31
  <% if percent > 1 %>
@@ -45,20 +45,21 @@
45
45
 
46
46
  <span class="user">
47
47
  <% if query[:user] %>
48
- <%= query[:user] %>
48
+ <%= @link_user ? link_to(query[:user], {user: query[:user]}.merge(@link_options)) : query[:user] %>
49
49
  <% if @show_details %>
50
50
  &middot;
51
51
  <% end %>
52
52
  <% end %>
53
53
  <% if @show_details && query[:query_hash] %>
54
- <%= link_to "details", show_query_path(query[:query_hash], user: query[:user]), target: "_blank" %>
54
+ <% hex_hash = [query[:query_hash]].pack("q>").unpack1("H16") %>
55
+ <%= link_to "details", show_query_path(hex_hash, user: query[:user], v: /\A\d{16}\z/.match?(hex_hash) ? 2 : nil), target: "_blank" %>
55
56
  <% end %>
56
57
  </span>
57
58
  </td>
58
59
  </tr>
59
60
  <tr>
60
61
  <td colspan="3" class="query-row">
61
- <pre><code class="query-code"><%= query[:query] %></code></pre>
62
+ <pre><code class="query-code language-pgsql"><%= query[:query] %></code></pre>
62
63
  <% if query[:query] == "<insufficient privilege>" %>
63
64
  <p class="text-muted">For security reasons, only superusers can see queries executed by other users.</p>
64
65
  <% end %>
@@ -8,6 +8,7 @@
8
8
  var sort = <%= pghero_js_value(@sort) %>;
9
9
  var minAverageTime = <%= pghero_js_value(@min_average_time) %>;
10
10
  var minCalls = <%= pghero_js_value(@min_calls) %>;
11
+ var user = <%= pghero_js_value(@user) %>;
11
12
  var debug = <%= pghero_js_value(@debug) %>;
12
13
  var startAt = <%= pghero_js_value(params[:start_at] ? @start_at.to_i * 1000 : nil) %>;
13
14
  var endAt = <%= pghero_js_value(@end_at.to_i * 1000) %>;
@@ -2,10 +2,10 @@
2
2
  <% unless @debug %>
3
3
  <div class="show-details">Details</div>
4
4
  <% end %>
5
- <code><pre class="create-index">CREATE INDEX CONCURRENTLY ON <%= index[:table] %><% if index[:using] %> USING <%= index[:using] %><% end %> (<%= index[:columns].join(", ") %>)</pre></code>
5
+ <pre class="create-index"><code>CREATE INDEX CONCURRENTLY ON <%= index[:table] %><% if index[:using] %> USING <%= index[:using] %><% end %> (<%= index[:columns].join(", ") %>)</code></pre>
6
6
  <% end %>
7
7
  <div class="details<%= " hide" unless @debug %>">
8
- <code><pre><% if details[:explanation] %><%= details[:explanation] %>
8
+ <pre><% if details[:explanation] %><%= details[:explanation] %>
9
9
  <% end %><% if details[:row_estimates] %>Rows: <%= details[:rows] %>
10
10
  <% if details[:row_progression] %>Row progression: <%= details[:row_progression].to_a.join(", ") %>
11
11
  <% end %>
@@ -15,5 +15,5 @@ Row estimates
15
15
 
16
16
  Existing indexes
17
17
  <% details[:table_indexes].sort_by { |i| [i[:primary] ? 0 : 1, i[:columns]] }.each do |i3| %>- <%= i3[:columns].join(", ") %><% if i3[:using] != "btree" %> <%= i3[:using].to_s.upcase %><% end %><% if i3[:primary] %> PRIMARY<% elsif i3[:unique] %> UNIQUE<% end %>
18
- <% end %><% end %></pre></code>
18
+ <% end %><% end %></pre>
19
19
  </div>
@@ -15,8 +15,9 @@
15
15
  <% if @explanation %>
16
16
  <% if @visualize %>
17
17
  <p>Paste the output below into the <%= link_to "explain visualizer", PgHero.visualize_url, target: "_blank" %></p>
18
+ <button id="copy" class="btn btn-info hide">Copy</button>
18
19
  <% end %>
19
- <pre><code><%= @explanation %></code></pre>
20
+ <pre><code id="explanation"><%= @explanation %></code></pre>
20
21
  <% unless @visualize %>
21
22
  <p><%= link_to "See how to interpret this", "https://www.postgresql.org/docs/current/static/using-explain.html", target: "_blank" %></p>
22
23
  <% end %>
@@ -170,7 +170,7 @@
170
170
  <h1>Inactive Replication Slots</h1>
171
171
  <p>Inactive replication slots can cause a lot of disk space to be consumed.</p>
172
172
  <p>For each, run:</p>
173
- <pre><code>SELECT pg_drop_replication_slot('slot_name');</code></pre>
173
+ <pre><code class="language-pgsql">SELECT pg_drop_replication_slot('slot_name');</code></pre>
174
174
  <table class="table">
175
175
  <thead>
176
176
  <tr>
@@ -190,12 +190,14 @@
190
190
 
191
191
  <% if @long_running_queries.any? %>
192
192
  <div class="content">
193
- <%= button_to "Kill All", kill_long_running_queries_path, class: "btn btn-danger", style: "float: right;" %>
193
+ <% if @kill_enabled %>
194
+ <%= button_to "Kill All", kill_long_running_queries_path, class: "btn btn-danger push-right" %>
195
+ <% end %>
194
196
  <h1>Long Running Queries</h1>
195
197
 
196
198
  <p>We recommend setting a statement timeout on all non-superusers with:</p>
197
199
 
198
- <pre><code>ALTER ROLE &lt;user&gt; SET statement_timeout TO '60s';</code></pre>
200
+ <pre><code class="language-pgsql">ALTER ROLE &lt;user&gt; SET statement_timeout TO '60s';</code></pre>
199
201
 
200
202
  <%= render partial: "live_queries_table", locals: {queries: @long_running_queries, vacuum_progress: {}} %>
201
203
  </div>
@@ -246,7 +248,7 @@
246
248
  <p>Try <%= link_to "tuning autovacuum", "https://blog.2ndquadrant.com/autovacuum-tuning-basics/", target: "_blank" %> - specifically autovacuum_vacuum_cost_limit.</p>
247
249
 
248
250
  <p>If that doesn’t work, for each table, run:</p>
249
- <pre><code>VACUUM FREEZE VERBOSE table;</code></pre>
251
+ <pre><code class="language-pgsql">VACUUM FREEZE VERBOSE table;</code></pre>
250
252
  <table class="table">
251
253
  <thead>
252
254
  <tr>
@@ -333,7 +335,7 @@
333
335
  </tr>
334
336
  <tr>
335
337
  <td class="query-row">
336
- <pre><code>DROP INDEX CONCURRENTLY <%= pghero_pretty_ident(index[:name], schema: index[:schema]) %>;
338
+ <pre><code class="language-pgsql">DROP INDEX CONCURRENTLY <%= pghero_pretty_ident(index[:name], schema: index[:schema]) %>;
337
339
  <%= index[:definition].sub("CREATE INDEX ", "CREATE INDEX CONCURRENTLY ") %>;</code></pre>
338
340
  </td>
339
341
  </tr>
@@ -367,7 +369,7 @@
367
369
  </tr>
368
370
  <tr>
369
371
  <td class="query-row">
370
- <pre><code>ALTER TABLE <%= pghero_pretty_ident(constraint[:table], schema: constraint[:schema]) %> VALIDATE CONSTRAINT <%= pghero_pretty_ident(constraint[:name]) %>;</code></pre>
372
+ <pre><code class="language-pgsql">ALTER TABLE <%= pghero_pretty_ident(constraint[:table], schema: constraint[:schema]) %> VALIDATE CONSTRAINT <%= pghero_pretty_ident(constraint[:name]) %>;</code></pre>
371
373
  </td>
372
374
  </tr>
373
375
  <% end %>
@@ -389,10 +391,9 @@
389
391
  </p>
390
392
 
391
393
  <div class="migration">
392
- <pre>rails generate migration remove_unneeded_indexes</pre>
394
+ <pre><code>rails generate migration remove_unneeded_indexes</code></pre>
393
395
  <p>And paste</p>
394
- <pre><% @duplicate_indexes.each do |query| %>
395
- <%= pghero_remove_index(query[:unneeded_index]) %><% end %></pre>
396
+ <pre><code><%= @duplicate_indexes.map { |q| pghero_remove_index(q[:unneeded_index]) }.join("\n") %></code></pre>
396
397
  </div>
397
398
 
398
399
  <table class="table duplicate-indexes">
@@ -431,15 +432,15 @@
431
432
  </p>
432
433
 
433
434
  <div class="migration">
434
- <pre>rails generate migration add_suggested_indexes</pre>
435
+ <pre><code>rails generate migration add_suggested_indexes</code></pre>
435
436
  <p>And paste</p>
436
- <pre>commit_db_transaction
437
+ <pre><code>commit_db_transaction
437
438
  <% @suggested_indexes.each do |index| %>
438
439
  <% if index[:using] && index[:using] != "btree" %>
439
440
  add_index <%= index[:table].to_sym.inspect %>, <%= index[:columns].first.inspect %>, using: <%= index[:using].inspect %>, algorithm: :concurrently
440
441
  <% else %>
441
442
  add_index <%= index[:table].to_sym.inspect %>, [<%= index[:columns].map(&:to_sym).map(&:inspect).join(", ") %>], algorithm: :concurrently<% end %>
442
- <% end %></pre>
443
+ <% end %></code></pre>
443
444
  </div>
444
445
 
445
446
  <% @suggested_indexes.each_with_index do |index, i| %>
@@ -461,9 +462,8 @@ add_index <%= index[:table].to_sym.inspect %>, [<%= index[:columns].map(&:to_sym
461
462
  <%= button_to "Enable", enable_query_stats_path, class: "btn btn-info" %>
462
463
  </p>
463
464
  <% else %>
464
- <p>Make them available by adding the following lines to <code>postgresql.conf</code>:</p>
465
- <pre>shared_preload_libraries = 'pg_stat_statements'
466
- pg_stat_statements.track = all</pre>
465
+ <p>Make them available by adding the following line to <code>postgresql.conf</code>:</p>
466
+ <pre><code>shared_preload_libraries = 'pg_stat_statements'</code></pre>
467
467
  <p>Restart the server for the changes to take effect.</p>
468
468
  <% end %>
469
469
  </div>
@@ -493,10 +493,9 @@ pg_stat_statements.track = all</pre>
493
493
  </p>
494
494
 
495
495
  <div class="migration">
496
- <pre>rails generate migration remove_unused_indexes</pre>
496
+ <pre><code>rails generate migration remove_unused_indexes</code></pre>
497
497
  <p>And paste</p>
498
- <pre><% @unused_indexes.each do |query| %>
499
- <%= pghero_remove_index(query)%><% end %></pre>
498
+ <pre><code><%= @unused_indexes.map { |q| pghero_remove_index(q) }.join("\n") %></code></pre>
500
499
  </div>
501
500
 
502
501
  <table class="table">