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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +22 -2
- data/LICENSE.txt +1 -1
- data/README.md +4 -3
- data/app/assets/javascripts/pghero/Chart.bundle.js +2228 -5320
- data/app/assets/javascripts/pghero/application.js +104 -68
- data/app/assets/javascripts/pghero/highlight.min.js +330 -341
- data/app/assets/javascripts/pghero/nouislider.js +64 -5
- data/app/assets/stylesheets/pghero/application.css +7 -1
- data/app/controllers/pg_hero/home_controller.rb +75 -41
- data/app/views/layouts/pg_hero/application.html.erb +1 -1
- data/app/views/pg_hero/home/_live_queries_table.html.erb +4 -2
- data/app/views/pg_hero/home/_queries_table.html.erb +8 -7
- data/app/views/pg_hero/home/_query_stats_slider.html.erb +1 -0
- data/app/views/pg_hero/home/_suggested_index.html.erb +3 -3
- data/app/views/pg_hero/home/explain.html.erb +2 -1
- data/app/views/pg_hero/home/index.html.erb +17 -18
- data/app/views/pg_hero/home/index_bloat.html.erb +2 -2
- data/app/views/pg_hero/home/live_queries.html.erb +7 -3
- data/app/views/pg_hero/home/queries.html.erb +31 -6
- data/app/views/pg_hero/home/show_query.html.erb +5 -5
- data/app/views/pg_hero/home/space.html.erb +5 -4
- data/app/views/pg_hero/home/tune.html.erb +1 -1
- data/lib/generators/pghero/templates/config.yml.tt +6 -1
- data/lib/generators/pghero/templates/query_stats.rb.tt +7 -1
- data/lib/generators/pghero/templates/upgrade_query_stats.rb.tt +23 -0
- data/lib/generators/pghero/upgrade_query_stats_generator.rb +19 -0
- data/lib/pghero/database.rb +6 -9
- data/lib/pghero/engine.rb +3 -10
- data/lib/pghero/methods/basic.rb +28 -27
- data/lib/pghero/methods/connections.rb +16 -32
- data/lib/pghero/methods/explain.rb +13 -20
- data/lib/pghero/methods/indexes.rb +8 -5
- data/lib/pghero/methods/kill.rb +4 -1
- data/lib/pghero/methods/maintenance.rb +14 -20
- data/lib/pghero/methods/queries.rb +7 -5
- data/lib/pghero/methods/query_stats.rb +264 -238
- data/lib/pghero/methods/replication.rb +9 -20
- data/lib/pghero/methods/sequences.rb +1 -1
- data/lib/pghero/methods/settings.rb +6 -11
- data/lib/pghero/methods/space.rb +11 -8
- data/lib/pghero/methods/suggested_indexes.rb +15 -16
- data/lib/pghero/methods/system.rb +6 -106
- data/lib/pghero/methods/tables.rb +7 -3
- data/lib/pghero/query.rb +8 -0
- data/lib/pghero/query_stats.rb +3 -0
- data/lib/pghero/version.rb +1 -1
- data/lib/pghero.rb +10 -42
- data/licenses/LICENSE-chart.js.txt +1 -1
- metadata +8 -8
- data/app/assets/javascripts/pghero/jquery.js +0 -10993
- data/lib/pghero/methods/users.rb +0 -95
- 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
|
-
|
|
981
|
+
scope_ConnectBase = addNodeTo(base, options.cssClasses.connects);
|
|
975
982
|
scope_Handles = [];
|
|
976
983
|
scope_Connects = [];
|
|
977
|
-
scope_Connects.push(addConnect(
|
|
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(
|
|
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 =
|
|
1999
|
+
l = locations[index - 1];
|
|
1968
2000
|
}
|
|
1969
2001
|
if (index !== scope_Connects.length - 1) {
|
|
1970
|
-
h =
|
|
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
|
-
|
|
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: "
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
@
|
|
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
|
|
227
|
+
query_hash_stats = @database.query_hash_stats(@query_hash, user: @user)
|
|
192
228
|
|
|
193
|
-
@chart_data = [{name: "
|
|
194
|
-
@chart2_data = [{name: "
|
|
195
|
-
@chart3_data = [{name: "
|
|
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.
|
|
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]
|
|
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
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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
|
|
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/
|
|
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
|
-
|
|
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[:
|
|
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
|
·
|
|
51
51
|
<% end %>
|
|
52
52
|
<% end %>
|
|
53
53
|
<% if @show_details && query[:query_hash] %>
|
|
54
|
-
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
|
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
|
-
|
|
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 <user> SET statement_timeout TO '60s';</code></pre>
|
|
200
|
+
<pre><code class="language-pgsql">ALTER ROLE <user> 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
|
|
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
|
|
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
|
|
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">
|