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.

Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +8 -0
  3. data/README.md +8 -8
  4. data/app/assets/javascripts/pghero/Chart.bundle.js +183 -55
  5. data/app/assets/javascripts/pghero/chartkick.js +53 -20
  6. data/app/assets/stylesheets/pghero/application.css +7 -0
  7. data/app/controllers/pg_hero/home_controller.rb +61 -57
  8. data/app/views/layouts/pg_hero/application.html.erb +3 -3
  9. data/app/views/pg_hero/home/_connections_table.html.erb +1 -1
  10. data/app/views/pg_hero/home/_queries_table.html.erb +6 -1
  11. data/app/views/pg_hero/home/connections.html.erb +1 -1
  12. data/app/views/pg_hero/home/explain.html.erb +11 -2
  13. data/app/views/pg_hero/home/index.html.erb +4 -4
  14. data/app/views/pg_hero/home/maintenance.html.erb +2 -2
  15. data/app/views/pg_hero/home/system.html.erb +2 -2
  16. data/guides/Rails.md +8 -0
  17. data/lib/generators/pghero/space_stats_generator.rb +29 -0
  18. data/lib/generators/pghero/templates/space_stats.rb +13 -0
  19. data/lib/pghero.rb +109 -23
  20. data/lib/pghero/database.rb +46 -8
  21. data/lib/pghero/engine.rb +3 -1
  22. data/lib/pghero/methods/basic.rb +3 -42
  23. data/lib/pghero/methods/connections.rb +18 -1
  24. data/lib/pghero/methods/explain.rb +2 -0
  25. data/lib/pghero/methods/indexes.rb +2 -2
  26. data/lib/pghero/methods/kill.rb +1 -1
  27. data/lib/pghero/methods/queries.rb +2 -2
  28. data/lib/pghero/methods/query_stats.rb +81 -75
  29. data/lib/pghero/methods/space.rb +12 -1
  30. data/lib/pghero/methods/suggested_indexes.rb +71 -31
  31. data/lib/pghero/version.rb +1 -1
  32. data/lib/tasks/pghero.rake +5 -0
  33. metadata +4 -3
  34. 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.1
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 === '[object Date]') {
65
+ if (type === "[object Date]") {
66
66
  return input;
67
67
  }
68
- if (type !== '[object String]') {
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] === '-' ? -1 : 1;
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
- var $ = window.jQuery || window.Zepto || window.$;
168
- $.ajax({
169
- dataType: "json",
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 = chartType;
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 = 'scatter';
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
- google.load("visualization", "1", loadOptions);
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") {
@@ -102,6 +102,13 @@ hr {
102
102
  margin-left: 6px;
103
103
  }
104
104
 
105
+ .user {
106
+ color: #999;
107
+ font-size: 12px;
108
+ float: right;
109
+ line-height: 20px;
110
+ }
111
+
105
112
  .tiny {
106
113
  font-size: 12px;
107
114
  margin-left: 6px;
@@ -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
- around_action :set_database
11
- before_action :set_current_database
10
+ before_action :set_database
12
11
  before_action :set_query_stats_enabled
13
12
  else
14
- around_filter :set_database
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 = PgHero.query_stats(historical: true, start_at: 3.hours.ago)
22
- @slow_queries = PgHero.slow_queries(query_stats: @query_stats)
23
- @long_running_queries = PgHero.long_running_queries
24
- @index_hit_rate = PgHero.index_hit_rate
25
- @table_hit_rate = PgHero.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 PgHero.suggested_indexes_enabled?
25
+ if @database.suggested_indexes_enabled?
28
26
  []
29
27
  else
30
- PgHero.missing_indexes
28
+ @database.missing_indexes
31
29
  end
32
- @unused_indexes = PgHero.unused_indexes.select { |q| q["index_scans"].to_i == 0 }
33
- @invalid_indexes = PgHero.invalid_indexes
34
- @duplicate_indexes = PgHero.duplicate_indexes if params[:duplicate_indexes]
35
- @good_cache_rate = @table_hit_rate >= PgHero.cache_hit_rate_threshold.to_f / 100 && @index_hit_rate >= PgHero.cache_hit_rate_threshold.to_f / 100
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 = PgHero.query_stats_available?
38
- @query_stats_extension_enabled = PgHero.query_stats_extension_enabled? if @query_stats_available
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 = PgHero.total_connections
41
- @good_total_connections = @total_connections < PgHero.total_connections_threshold
38
+ @total_connections = @database.total_connections
39
+ @good_total_connections = @total_connections < @database.total_connections_threshold
42
40
  if @replica
43
- @replication_lag = PgHero.replication_lag
41
+ @replication_lag = @database.replication_lag
44
42
  @good_replication_lag = @replication_lag < 5
45
43
  end
46
- @transaction_id_danger = PgHero.transaction_id_danger(threshold: 1500000000)
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 = PgHero.sequence_danger(threshold: params[:sequence_threshold])
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 = PgHero.index_usage
52
+ @index_usage = @database.index_usage
55
53
  end
56
54
 
57
55
  def space
58
56
  @title = "Space"
59
- @database_size = PgHero.database_size
60
- @relation_sizes = PgHero.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 = PgHero.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 = PgHero.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
- PgHero.query_stats(
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: PgHero.cpu_usage(system_params).map { |k, v| [k, v.round] }, library: chart_library_options}]
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: PgHero.connection_stats(system_params), library: chart_library_options}]
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: PgHero.replication_lag_stats(system_params), library: chart_library_options}]
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: PgHero.read_iops_stats(system_params).map { |k, v| [k, v.round] }, library: chart_library_options},
133
- {name: "Write IOPS", data: PgHero.write_iops_stats(system_params).map { |k, v| [k, v.round] }, library: chart_library_options}
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
- @explanation = PgHero.explain("#{params[:commit] == "Analyze" ? "ANALYZE " : ""}#{@query}")
145
- @suggested_index = PgHero.suggested_indexes(queries: [@query]).first
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 = PgHero.settings
162
+ @settings = @database.settings
155
163
  end
156
164
 
157
165
  def connections
158
166
  @title = "Connections"
159
- @total_connections = PgHero.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 = PgHero.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 PgHero.kill(params[:pid])
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
- PgHero.kill_long_running_queries
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
- PgHero.kill_all
191
+ @database.kill_all
182
192
  redirect_to :back, notice: "Connections killed"
183
193
  end
184
194
 
185
195
  def enable_query_stats
186
- PgHero.enable_query_stats
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
- PgHero.reset_query_stats
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.with(params[:database]) do
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: PgHero.primary_database))
216
+ redirect_to url_for(params.slice(:controller, :action).merge(database: @databases.first.id))
209
217
  else
210
- yield
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 = PgHero.query_stats_enabled?
224
- @system_stats_enabled = PgHero.system_stats_enabled?
225
- @replica = PgHero.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 = PgHero.suggested_indexes_by_query(query_stats: @query_stats.select { |qs| qs["average_time"].to_f >= min_average_time && qs["calls"].to_i >= min_calls })
230
- @suggested_indexes = PgHero.suggested_indexes(suggested_indexes_by_query: @suggested_indexes_by_query)
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 ? @current_database.name : "PgHero", @title].compact.join(" / ") %></title>
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"><%= @current_database.name %></p>
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 PgHero.current_database == database.id) %>">
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><%= number_with_delimiter(query["calls"].to_i) %></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;">