pghero 1.4.2 → 1.5.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of pghero might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/README.md +8 -8
- data/app/assets/javascripts/pghero/Chart.bundle.js +183 -55
- data/app/assets/javascripts/pghero/chartkick.js +53 -20
- data/app/assets/stylesheets/pghero/application.css +7 -0
- data/app/controllers/pg_hero/home_controller.rb +61 -57
- data/app/views/layouts/pg_hero/application.html.erb +3 -3
- data/app/views/pg_hero/home/_connections_table.html.erb +1 -1
- data/app/views/pg_hero/home/_queries_table.html.erb +6 -1
- data/app/views/pg_hero/home/connections.html.erb +1 -1
- data/app/views/pg_hero/home/explain.html.erb +11 -2
- data/app/views/pg_hero/home/index.html.erb +4 -4
- data/app/views/pg_hero/home/maintenance.html.erb +2 -2
- data/app/views/pg_hero/home/system.html.erb +2 -2
- data/guides/Rails.md +8 -0
- data/lib/generators/pghero/space_stats_generator.rb +29 -0
- data/lib/generators/pghero/templates/space_stats.rb +13 -0
- data/lib/pghero.rb +109 -23
- data/lib/pghero/database.rb +46 -8
- data/lib/pghero/engine.rb +3 -1
- data/lib/pghero/methods/basic.rb +3 -42
- data/lib/pghero/methods/connections.rb +18 -1
- data/lib/pghero/methods/explain.rb +2 -0
- data/lib/pghero/methods/indexes.rb +2 -2
- data/lib/pghero/methods/kill.rb +1 -1
- data/lib/pghero/methods/queries.rb +2 -2
- data/lib/pghero/methods/query_stats.rb +81 -75
- data/lib/pghero/methods/space.rb +12 -1
- data/lib/pghero/methods/suggested_indexes.rb +71 -31
- data/lib/pghero/version.rb +1 -1
- data/lib/tasks/pghero.rake +5 -0
- metadata +4 -3
- data/lib/pghero/methods/databases.rb +0 -39
@@ -2,7 +2,7 @@
|
|
2
2
|
* Chartkick.js
|
3
3
|
* Create beautiful JavaScript charts with minimal code
|
4
4
|
* https://github.com/ankane/chartkick.js
|
5
|
-
* v2.0
|
5
|
+
* v2.1.0
|
6
6
|
* MIT License
|
7
7
|
*/
|
8
8
|
|
@@ -62,10 +62,10 @@
|
|
62
62
|
function parseISO8601(input) {
|
63
63
|
var day, hour, matches, milliseconds, minutes, month, offset, result, seconds, type, year;
|
64
64
|
type = Object.prototype.toString.call(input);
|
65
|
-
if (type ===
|
65
|
+
if (type === "[object Date]") {
|
66
66
|
return input;
|
67
67
|
}
|
68
|
-
if (type !==
|
68
|
+
if (type !== "[object String]") {
|
69
69
|
return;
|
70
70
|
}
|
71
71
|
matches = input.match(ISO8601_PATTERN);
|
@@ -83,7 +83,7 @@
|
|
83
83
|
if (matches[17]) {
|
84
84
|
offset += parseInt(matches[17], 10);
|
85
85
|
}
|
86
|
-
offset *= matches[14] ===
|
86
|
+
offset *= matches[14] === "-" ? -1 : 1;
|
87
87
|
result -= offset * 60 * 1000;
|
88
88
|
}
|
89
89
|
return new Date(result);
|
@@ -164,18 +164,37 @@
|
|
164
164
|
}
|
165
165
|
|
166
166
|
function getJSON(element, url, success) {
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
url: url,
|
171
|
-
success: success,
|
172
|
-
error: function (jqXHR, textStatus, errorThrown) {
|
173
|
-
var message = (typeof errorThrown === "string") ? errorThrown : errorThrown.message;
|
174
|
-
chartError(element, message);
|
175
|
-
}
|
167
|
+
ajaxCall(url, success, function (jqXHR, textStatus, errorThrown) {
|
168
|
+
var message = (typeof errorThrown === "string") ? errorThrown : errorThrown.message;
|
169
|
+
chartError(element, message);
|
176
170
|
});
|
177
171
|
}
|
178
172
|
|
173
|
+
function ajaxCall(url, success, error) {
|
174
|
+
var $ = window.jQuery || window.Zepto || window.$;
|
175
|
+
|
176
|
+
if ($) {
|
177
|
+
$.ajax({
|
178
|
+
dataType: "json",
|
179
|
+
url: url,
|
180
|
+
success: success,
|
181
|
+
error: error
|
182
|
+
});
|
183
|
+
} else {
|
184
|
+
var xhr = new XMLHttpRequest();
|
185
|
+
xhr.open("GET", url, true);
|
186
|
+
xhr.setRequestHeader("Content-Type", "application/json");
|
187
|
+
xhr.onload = function () {
|
188
|
+
if (xhr.status === 200) {
|
189
|
+
success(JSON.parse(xhr.responseText), xhr.statusText, xhr);
|
190
|
+
} else {
|
191
|
+
error(xhr, "error", xhr.statusText);
|
192
|
+
}
|
193
|
+
};
|
194
|
+
xhr.send();
|
195
|
+
}
|
196
|
+
}
|
197
|
+
|
179
198
|
function errorCatcher(chart, callback) {
|
180
199
|
try {
|
181
200
|
callback(chart);
|
@@ -344,7 +363,9 @@
|
|
344
363
|
}
|
345
364
|
var options = jsOptions(chart.data, chart.options, chartOptions), data, i, j;
|
346
365
|
options.xAxis.type = chart.options.discrete ? "category" : "datetime";
|
347
|
-
options.chart.type
|
366
|
+
if (!options.chart.type) {
|
367
|
+
options.chart.type = chartType;
|
368
|
+
}
|
348
369
|
options.chart.renderTo = chart.element.id;
|
349
370
|
|
350
371
|
var series = chart.data;
|
@@ -364,7 +385,7 @@
|
|
364
385
|
this.renderScatterChart = function (chart) {
|
365
386
|
var chartOptions = {};
|
366
387
|
var options = jsOptions(chart.data, chart.options, chartOptions);
|
367
|
-
options.chart.type =
|
388
|
+
options.chart.type = "scatter";
|
368
389
|
options.chart.renderTo = chart.element.id;
|
369
390
|
options.series = chart.data;
|
370
391
|
new Highcharts.Chart(options);
|
@@ -441,7 +462,7 @@
|
|
441
462
|
};
|
442
463
|
adapters.push(HighchartsAdapter);
|
443
464
|
}
|
444
|
-
if (!GoogleChartsAdapter && window.google && window.google.setOnLoadCallback) {
|
465
|
+
if (!GoogleChartsAdapter && window.google && (window.google.setOnLoadCallback || window.google.charts)) {
|
445
466
|
GoogleChartsAdapter = new function () {
|
446
467
|
var google = window.google;
|
447
468
|
|
@@ -484,7 +505,12 @@
|
|
484
505
|
if (config.language) {
|
485
506
|
loadOptions.language = config.language;
|
486
507
|
}
|
487
|
-
|
508
|
+
|
509
|
+
if (window.google.setOnLoadCallback) {
|
510
|
+
google.load("visualization", "1", loadOptions);
|
511
|
+
} else {
|
512
|
+
google.charts.load("current", loadOptions);
|
513
|
+
}
|
488
514
|
}
|
489
515
|
};
|
490
516
|
|
@@ -1006,7 +1032,8 @@
|
|
1006
1032
|
} else if (day || timeDiff > 10) {
|
1007
1033
|
options.scales.xAxes[0].time.unit = "day";
|
1008
1034
|
step = 1;
|
1009
|
-
} else if (hour) {
|
1035
|
+
} else if (hour || timeDiff > 0.5) {
|
1036
|
+
options.scales.xAxes[0].time.displayFormats = {hour: "MMM D, h a"};
|
1010
1037
|
options.scales.xAxes[0].time.unit = "hour";
|
1011
1038
|
step = 1 / 24.0;
|
1012
1039
|
} else if (minute) {
|
@@ -1015,7 +1042,6 @@
|
|
1015
1042
|
step = 1 / 24.0 / 60.0;
|
1016
1043
|
}
|
1017
1044
|
|
1018
|
-
|
1019
1045
|
if (step && timeDiff > 0) {
|
1020
1046
|
var unitStepSize = Math.ceil(timeDiff / step / (chart.element.offsetWidth / 100.0));
|
1021
1047
|
if (week && step === 1) {
|
@@ -1385,7 +1411,14 @@
|
|
1385
1411
|
Timeline: function (element, dataSource, opts) {
|
1386
1412
|
setElement(this, element, dataSource, opts, processTimelineData);
|
1387
1413
|
},
|
1388
|
-
charts: {}
|
1414
|
+
charts: {},
|
1415
|
+
configure: function (options) {
|
1416
|
+
for (var key in options) {
|
1417
|
+
if (options.hasOwnProperty(key)) {
|
1418
|
+
config[key] = options[key];
|
1419
|
+
}
|
1420
|
+
}
|
1421
|
+
}
|
1389
1422
|
};
|
1390
1423
|
|
1391
1424
|
if (typeof module === "object" && typeof module.exports === "object") {
|
@@ -7,67 +7,65 @@ module PgHero
|
|
7
7
|
http_basic_authenticate_with name: ENV["PGHERO_USERNAME"], password: ENV["PGHERO_PASSWORD"] if ENV["PGHERO_PASSWORD"]
|
8
8
|
|
9
9
|
if respond_to?(:before_action)
|
10
|
-
|
11
|
-
before_action :set_current_database
|
10
|
+
before_action :set_database
|
12
11
|
before_action :set_query_stats_enabled
|
13
12
|
else
|
14
|
-
|
15
|
-
before_filter :set_current_database
|
13
|
+
before_filter :set_database
|
16
14
|
before_filter :set_query_stats_enabled
|
17
15
|
end
|
18
16
|
|
19
17
|
def index
|
20
18
|
@title = "Overview"
|
21
|
-
@query_stats =
|
22
|
-
@slow_queries =
|
23
|
-
@long_running_queries =
|
24
|
-
@index_hit_rate =
|
25
|
-
@table_hit_rate =
|
19
|
+
@query_stats = @database.query_stats(historical: true, start_at: 3.hours.ago)
|
20
|
+
@slow_queries = @database.slow_queries(query_stats: @query_stats)
|
21
|
+
@long_running_queries = @database.long_running_queries
|
22
|
+
@index_hit_rate = @database.index_hit_rate
|
23
|
+
@table_hit_rate = @database.table_hit_rate
|
26
24
|
@missing_indexes =
|
27
|
-
if
|
25
|
+
if @database.suggested_indexes_enabled?
|
28
26
|
[]
|
29
27
|
else
|
30
|
-
|
28
|
+
@database.missing_indexes
|
31
29
|
end
|
32
|
-
@unused_indexes =
|
33
|
-
@invalid_indexes =
|
34
|
-
@duplicate_indexes =
|
35
|
-
@good_cache_rate = @table_hit_rate >=
|
30
|
+
@unused_indexes = @database.unused_indexes.select { |q| q["index_scans"].to_i == 0 }
|
31
|
+
@invalid_indexes = @database.invalid_indexes
|
32
|
+
@duplicate_indexes = @database.duplicate_indexes if params[:duplicate_indexes]
|
33
|
+
@good_cache_rate = @table_hit_rate >= @database.cache_hit_rate_threshold.to_f / 100 && @index_hit_rate >= @database.cache_hit_rate_threshold.to_f / 100
|
36
34
|
unless @query_stats_enabled
|
37
|
-
@query_stats_available =
|
38
|
-
@query_stats_extension_enabled =
|
35
|
+
@query_stats_available = @database.query_stats_available?
|
36
|
+
@query_stats_extension_enabled = @database.query_stats_extension_enabled? if @query_stats_available
|
39
37
|
end
|
40
|
-
@total_connections =
|
41
|
-
@good_total_connections = @total_connections <
|
38
|
+
@total_connections = @database.total_connections
|
39
|
+
@good_total_connections = @total_connections < @database.total_connections_threshold
|
42
40
|
if @replica
|
43
|
-
@replication_lag =
|
41
|
+
@replication_lag = @database.replication_lag
|
44
42
|
@good_replication_lag = @replication_lag < 5
|
45
43
|
end
|
46
|
-
@transaction_id_danger =
|
44
|
+
@transaction_id_danger = @database.transaction_id_danger(threshold: 1500000000)
|
47
45
|
set_suggested_indexes((params[:min_average_time] || 20).to_f, (params[:min_calls] || 50).to_i)
|
48
46
|
@show_migrations = PgHero.show_migrations
|
49
|
-
@sequence_danger =
|
47
|
+
@sequence_danger = @database.sequence_danger(threshold: params[:sequence_threshold])
|
50
48
|
end
|
51
49
|
|
52
50
|
def index_usage
|
53
51
|
@title = "Index Usage"
|
54
|
-
@index_usage =
|
52
|
+
@index_usage = @database.index_usage
|
55
53
|
end
|
56
54
|
|
57
55
|
def space
|
58
56
|
@title = "Space"
|
59
|
-
@database_size =
|
60
|
-
@relation_sizes =
|
57
|
+
@database_size = @database.database_size
|
58
|
+
@relation_sizes = @database.relation_sizes
|
61
59
|
end
|
62
60
|
|
63
61
|
def live_queries
|
64
62
|
@title = "Live Queries"
|
65
|
-
@running_queries =
|
63
|
+
@running_queries = @database.running_queries
|
66
64
|
end
|
67
65
|
|
68
66
|
def queries
|
69
67
|
@title = "Queries"
|
70
|
-
@historical_query_stats_enabled =
|
68
|
+
@historical_query_stats_enabled = @database.historical_query_stats_enabled?
|
71
69
|
@sort = %w(average_time calls).include?(params[:sort]) ? params[:sort] : nil
|
72
70
|
@min_average_time = params[:min_average_time] ? params[:min_average_time].to_i : nil
|
73
71
|
@min_calls = params[:min_calls] ? params[:min_calls].to_i : nil
|
@@ -82,7 +80,7 @@ module PgHero
|
|
82
80
|
if @historical_query_stats_enabled && !request.xhr?
|
83
81
|
[]
|
84
82
|
else
|
85
|
-
|
83
|
+
@database.query_stats(
|
86
84
|
historical: true,
|
87
85
|
start_at: @start_at,
|
88
86
|
end_at: @end_at,
|
@@ -116,21 +114,21 @@ module PgHero
|
|
116
114
|
end
|
117
115
|
|
118
116
|
def cpu_usage
|
119
|
-
render json: [{name: "CPU", data:
|
117
|
+
render json: [{name: "CPU", data: @database.cpu_usage(system_params).map { |k, v| [k, v.round] }, library: chart_library_options}]
|
120
118
|
end
|
121
119
|
|
122
120
|
def connection_stats
|
123
|
-
render json: [{name: "Connections", data:
|
121
|
+
render json: [{name: "Connections", data: @database.connection_stats(system_params), library: chart_library_options}]
|
124
122
|
end
|
125
123
|
|
126
124
|
def replication_lag_stats
|
127
|
-
render json: [{name: "Lag", data:
|
125
|
+
render json: [{name: "Lag", data: @database.replication_lag_stats(system_params), library: chart_library_options}]
|
128
126
|
end
|
129
127
|
|
130
128
|
def load_stats
|
131
129
|
render json: [
|
132
|
-
{name: "Read IOPS", data:
|
133
|
-
{name: "Write IOPS", data:
|
130
|
+
{name: "Read IOPS", data: @database.read_iops_stats(system_params).map { |k, v| [k, v.round] }, library: chart_library_options},
|
131
|
+
{name: "Write IOPS", data: @database.write_iops_stats(system_params).map { |k, v| [k, v.round] }, library: chart_library_options}
|
134
132
|
]
|
135
133
|
end
|
136
134
|
|
@@ -141,8 +139,18 @@ module PgHero
|
|
141
139
|
# need to prevent CSRF and DoS
|
142
140
|
if request.post? && @query
|
143
141
|
begin
|
144
|
-
|
145
|
-
|
142
|
+
prefix =
|
143
|
+
case params[:commit]
|
144
|
+
when "Analyze"
|
145
|
+
"ANALYZE "
|
146
|
+
when "Visualize"
|
147
|
+
"(ANALYZE, COSTS, VERBOSE, BUFFERS, FORMAT JSON) "
|
148
|
+
else
|
149
|
+
""
|
150
|
+
end
|
151
|
+
@explanation = @database.explain("#{prefix}#{@query}")
|
152
|
+
@suggested_index = @database.suggested_indexes(queries: [@query]).first
|
153
|
+
@visualize = params[:commit] == "Visualize"
|
146
154
|
rescue ActiveRecord::StatementInvalid => e
|
147
155
|
@error = e.message
|
148
156
|
end
|
@@ -151,21 +159,23 @@ module PgHero
|
|
151
159
|
|
152
160
|
def tune
|
153
161
|
@title = "Tune"
|
154
|
-
@settings =
|
162
|
+
@settings = @database.settings
|
155
163
|
end
|
156
164
|
|
157
165
|
def connections
|
158
166
|
@title = "Connections"
|
159
|
-
@total_connections =
|
167
|
+
@total_connections = @database.total_connections
|
168
|
+
@connection_sources = @database.connection_sources(by_database_and_user: true)
|
160
169
|
end
|
161
170
|
|
162
171
|
def maintenance
|
163
172
|
@title = "Maintenance"
|
164
|
-
@maintenance_info =
|
173
|
+
@maintenance_info = @database.maintenance_info
|
174
|
+
@time_zone = PgHero.time_zone
|
165
175
|
end
|
166
176
|
|
167
177
|
def kill
|
168
|
-
if
|
178
|
+
if @database.kill(params[:pid])
|
169
179
|
redirect_to root_path, notice: "Query killed"
|
170
180
|
else
|
171
181
|
redirect_to :back, notice: "Query no longer running"
|
@@ -173,24 +183,24 @@ module PgHero
|
|
173
183
|
end
|
174
184
|
|
175
185
|
def kill_long_running_queries
|
176
|
-
|
186
|
+
@database.kill_long_running_queries
|
177
187
|
redirect_to :back, notice: "Queries killed"
|
178
188
|
end
|
179
189
|
|
180
190
|
def kill_all
|
181
|
-
|
191
|
+
@database.kill_all
|
182
192
|
redirect_to :back, notice: "Connections killed"
|
183
193
|
end
|
184
194
|
|
185
195
|
def enable_query_stats
|
186
|
-
|
196
|
+
@database.enable_query_stats
|
187
197
|
redirect_to :back, notice: "Query stats enabled"
|
188
198
|
rescue ActiveRecord::StatementInvalid
|
189
199
|
redirect_to :back, alert: "The database user does not have permission to enable query stats"
|
190
200
|
end
|
191
201
|
|
192
202
|
def reset_query_stats
|
193
|
-
|
203
|
+
@database.reset_query_stats
|
194
204
|
redirect_to :back, notice: "Query stats reset"
|
195
205
|
rescue ActiveRecord::StatementInvalid
|
196
206
|
redirect_to :back, alert: "The database user does not have permission to reset query stats"
|
@@ -201,33 +211,27 @@ module PgHero
|
|
201
211
|
def set_database
|
202
212
|
@databases = PgHero.databases.values
|
203
213
|
if params[:database]
|
204
|
-
PgHero.
|
205
|
-
yield
|
206
|
-
end
|
214
|
+
@database = PgHero.databases[params[:database]]
|
207
215
|
elsif @databases.size > 1
|
208
|
-
redirect_to url_for(params.slice(:controller, :action).merge(database:
|
216
|
+
redirect_to url_for(params.slice(:controller, :action).merge(database: @databases.first.id))
|
209
217
|
else
|
210
|
-
|
218
|
+
@database = @databases.first
|
211
219
|
end
|
212
220
|
end
|
213
221
|
|
214
|
-
def set_current_database
|
215
|
-
@current_database = PgHero.databases[PgHero.current_database]
|
216
|
-
end
|
217
|
-
|
218
222
|
def default_url_options
|
219
223
|
{database: params[:database]}
|
220
224
|
end
|
221
225
|
|
222
226
|
def set_query_stats_enabled
|
223
|
-
@query_stats_enabled =
|
224
|
-
@system_stats_enabled =
|
225
|
-
@replica =
|
227
|
+
@query_stats_enabled = @database.query_stats_enabled?
|
228
|
+
@system_stats_enabled = @database.system_stats_enabled?
|
229
|
+
@replica = @database.replica?
|
226
230
|
end
|
227
231
|
|
228
232
|
def set_suggested_indexes(min_average_time = 0, min_calls = 0)
|
229
|
-
@suggested_indexes_by_query =
|
230
|
-
@suggested_indexes =
|
233
|
+
@suggested_indexes_by_query = @database.suggested_indexes_by_query(query_stats: @query_stats.select { |qs| qs["average_time"].to_f >= min_average_time && qs["calls"].to_i >= min_calls })
|
234
|
+
@suggested_indexes = @database.suggested_indexes(suggested_indexes_by_query: @suggested_indexes_by_query)
|
231
235
|
@query_stats_by_query = @query_stats.index_by { |q| q["query"] }
|
232
236
|
@debug = params[:debug] == "true"
|
233
237
|
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
<!DOCTYPE html>
|
2
2
|
<html>
|
3
3
|
<head>
|
4
|
-
<title><%= [@databases.size > 1 ? @
|
4
|
+
<title><%= [@databases.size > 1 ? @database.name : "PgHero", @title].compact.join(" / ") %></title>
|
5
5
|
|
6
6
|
<meta charset="utf-8" />
|
7
7
|
<%= stylesheet_link_tag "pghero/application" %>
|
@@ -24,7 +24,7 @@
|
|
24
24
|
<div class="grid">
|
25
25
|
<div class="col-3-12">
|
26
26
|
<% if @databases.size > 1 %>
|
27
|
-
<p class="nav-header"><%= @
|
27
|
+
<p class="nav-header"><%= @database.name %></p>
|
28
28
|
<% end %>
|
29
29
|
|
30
30
|
<ul class="nav">
|
@@ -49,7 +49,7 @@
|
|
49
49
|
<p class="nav-header">Databases</p>
|
50
50
|
<ul class="nav">
|
51
51
|
<% @databases.each do |database| %>
|
52
|
-
<li class="<%= ("active-database" if
|
52
|
+
<li class="<%= ("active-database" if @database.id == database.id) %>">
|
53
53
|
<%= link_to database.name, database: database.id %>
|
54
54
|
</li>
|
55
55
|
<% end %>
|
@@ -15,7 +15,7 @@
|
|
15
15
|
<tbody>
|
16
16
|
<% connection_sources.each do |source| %>
|
17
17
|
<tr>
|
18
|
-
<td><%= source["source"] %> <div class="text-muted"><%= [source["database"], source["ip"]].compact.join(" ") %></div></td>
|
18
|
+
<td><%= source["source"] %> <div class="text-muted"><%= [source["user"], source["database"], source["ip"]].compact.join(" - ") %></div></td>
|
19
19
|
<td><%= number_with_delimiter(source["total_connections"]) %></td>
|
20
20
|
</tr>
|
21
21
|
<% end %>
|
@@ -38,7 +38,12 @@
|
|
38
38
|
</span>
|
39
39
|
</td>
|
40
40
|
<td><%= number_with_delimiter(query["average_time"].to_f.round) %> ms</td>
|
41
|
-
<td
|
41
|
+
<td>
|
42
|
+
<%= number_with_delimiter(query["calls"].to_i) %>
|
43
|
+
<% if query["user"] %>
|
44
|
+
<span class="user"><%= query["user"] %></span>
|
45
|
+
<% end %>
|
46
|
+
</td>
|
42
47
|
</tr>
|
43
48
|
<tr>
|
44
49
|
<td colspan="3" style="border-top: none; padding: 0;">
|