pghero 1.1.4 → 1.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: bb4fc6839e13e1fcb1d5e1ca1dee05c34b215dc2
4
- data.tar.gz: c362762b0b918bbb786adc12c185b20f4bbf9562
3
+ metadata.gz: c032f7f0fd5b3d6484cae193d125d3899ee87594
4
+ data.tar.gz: 02db1720b5201380dff89ac2dceba86c42d8dc49
5
5
  SHA512:
6
- metadata.gz: 4604d456402f86697ba18e0ffa261ce4747cb3eb9e9284c8b35ef8e1e9ce6856745a6b6f43fbbd3b510ea6c138fbc31ca475a9b0e2ac3ab5f92d60796244c84e
7
- data.tar.gz: 422a78d125a1ad063e50823cead73194acf7aeb82ebab57b01d3680ef1eb7f5d6c2d9263909b7bd58c512d937a42f30cb32bb99e9dfcfcf419a9283ae5ab3ca3
6
+ metadata.gz: f07fe4ece79f34731bb6bc28dc19cd09d365a9f9968caeb0918fb347d9be9d050ce5c1286b09feccc0312d2e379afbf3278fd65ca5c802f4c817e6b1ee7f83c0
7
+ data.tar.gz: dd334b3251a68372218f765b2c5d986e4bd71172e3a428511f05f4dfb7580123b38421a08eebacc204360cc51b60a9b24bc74dea31889eda9f1d6013bd4bc204
@@ -1,3 +1,13 @@
1
+ ## 1.2.0
2
+
3
+ - Added suggested indexes
4
+ - Added duplicate indexes
5
+ - Added maintenance tab
6
+ - Added load stats for RDS
7
+ - Added `table_caching` and `index_caching` methods
8
+ - Added configurable cache hit rate threshold
9
+ - Show all connections in connections tab
10
+
1
11
  ## 1.1.4
2
12
 
3
13
  - Added check for transaction ID wraparound failure
data/Gemfile CHANGED
@@ -2,3 +2,5 @@ source "https://rubygems.org"
2
2
 
3
3
  # Specify your gem's dependencies in pghero.gemspec
4
4
  gemspec
5
+
6
+ gem "pg_query"
@@ -11,24 +11,27 @@ module PgHero
11
11
 
12
12
  def index
13
13
  @title = "Overview"
14
- @slow_queries = PgHero.slow_queries(historical: true, start_at: 3.hours.ago)
14
+ @query_stats = PgHero.query_stats(historical: true, start_at: 3.hours.ago)
15
+ @slow_queries = PgHero.slow_queries(query_stats: @query_stats)
15
16
  @long_running_queries = PgHero.long_running_queries
16
17
  @index_hit_rate = PgHero.index_hit_rate
17
18
  @table_hit_rate = PgHero.table_hit_rate
18
19
  @missing_indexes = PgHero.missing_indexes
19
20
  @unused_indexes = PgHero.unused_indexes.select { |q| q["index_scans"].to_i == 0 }
20
21
  @invalid_indexes = PgHero.invalid_indexes
21
- @good_cache_rate = @table_hit_rate >= 0.99 && @index_hit_rate >= 0.99
22
+ @duplicate_indexes = PgHero.duplicate_indexes
23
+ @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
22
24
  @query_stats_available = PgHero.query_stats_available?
23
25
  @total_connections = PgHero.total_connections
24
26
  @good_total_connections = @total_connections < PgHero.total_connections_threshold
25
- @replica = PgHero.replica?
26
27
  if @replica
27
28
  @replication_lag = PgHero.replication_lag
28
29
  @good_replication_lag = @replication_lag < 5
29
30
  end
30
31
  @transaction_id_danger = PgHero.transaction_id_danger
31
32
  @autovacuum_danger = PgHero.autovacuum_danger
33
+ set_suggested_indexes
34
+ @show_migrations = PgHero.show_migrations
32
35
  end
33
36
 
34
37
  def index_usage
@@ -51,6 +54,9 @@ module PgHero
51
54
  @title = "Queries"
52
55
  @historical_query_stats_enabled = PgHero.historical_query_stats_enabled?
53
56
  @sort = %w[average_time calls].include?(params[:sort]) ? params[:sort] : nil
57
+ @min_average_time = params[:min_average_time] ? params[:min_average_time].to_i : nil
58
+ @min_calls = params[:min_calls] ? params[:min_calls].to_i : nil
59
+ @debug = params[:debug] == "true"
54
60
 
55
61
  @query_stats =
56
62
  begin
@@ -62,13 +68,22 @@ module PgHero
62
68
  if @historical_query_stats_enabled && !request.xhr?
63
69
  []
64
70
  else
65
- PgHero.query_stats(historical: true, start_at: @start_at, end_at: @end_at, sort: @sort)
71
+ PgHero.query_stats(
72
+ historical: true,
73
+ start_at: @start_at,
74
+ end_at: @end_at,
75
+ sort: @sort,
76
+ min_average_time: @min_average_time,
77
+ min_calls: @min_calls
78
+ )
66
79
  end
67
80
  rescue
68
81
  @error = true
69
82
  []
70
83
  end
71
84
 
85
+ set_suggested_indexes
86
+
72
87
  if request.xhr?
73
88
  render layout: false, partial: "queries_table", locals: {queries: @query_stats, xhr: true}
74
89
  end
@@ -90,6 +105,13 @@ module PgHero
90
105
  render json: PgHero.replication_lag_stats
91
106
  end
92
107
 
108
+ def load_stats
109
+ render json: [
110
+ {name: "Read IOPS", data: PgHero.read_iops_stats.map { |k, v| [k, v.round] }},
111
+ {name: "Write IOPS", data: PgHero.write_iops_stats.map { |k, v| [k, v.round] }}
112
+ ]
113
+ end
114
+
93
115
  def explain
94
116
  @title = "Explain"
95
117
  @query = params[:query]
@@ -98,6 +120,7 @@ module PgHero
98
120
  if request.post? && @query
99
121
  begin
100
122
  @explanation = PgHero.explain("#{params[:commit] == "Analyze" ? "ANALYZE " : ""}#{@query}")
123
+ @suggested_index = PgHero.suggested_indexes(queries: [@query]).first
101
124
  rescue ActiveRecord::StatementInvalid => e
102
125
  @error = e.message
103
126
  end
@@ -114,6 +137,11 @@ module PgHero
114
137
  @total_connections = PgHero.total_connections
115
138
  end
116
139
 
140
+ def maintenance
141
+ @title = "Maintenance"
142
+ @maintenance_info = PgHero.maintenance_info
143
+ end
144
+
117
145
  def kill
118
146
  if PgHero.kill(params[:pid])
119
147
  redirect_to root_path, notice: "Query killed"
@@ -168,6 +196,12 @@ module PgHero
168
196
  def set_query_stats_enabled
169
197
  @query_stats_enabled = PgHero.query_stats_enabled?
170
198
  @system_stats_enabled = PgHero.system_stats_enabled?
199
+ @replica = PgHero.replica?
200
+ end
201
+
202
+ def set_suggested_indexes
203
+ @suggested_indexes_by_query = PgHero.suggested_indexes_by_query(query_stats: @query_stats)
204
+ @suggested_indexes = PgHero.suggested_indexes(suggested_indexes_by_query: @suggested_indexes_by_query)
171
205
  end
172
206
  end
173
207
  end
@@ -78,6 +78,12 @@
78
78
  padding: 10px;
79
79
  }
80
80
 
81
+ hr {
82
+ border: none;
83
+ height: 0;
84
+ border-top: solid 1px #ddd;
85
+ }
86
+
81
87
  .container {
82
88
  max-width: 1000px;
83
89
  margin-left: auto;
@@ -414,6 +420,9 @@
414
420
  <li class="<%= controller.action_name == "index_usage" ? "active" : "" %>"><%= link_to "Index Usage", index_usage_path %></li>
415
421
  <li class="<%= controller.action_name == "space" ? "active" : "" %>"><%= link_to "Space", space_path %></li>
416
422
  <li class="<%= controller.action_name == "connections" ? "active" : "" %>"><%= link_to "Connections", connections_path %></li>
423
+ <% unless @replica %>
424
+ <li class="<%= controller.action_name == "maintenance" ? "active" : "" %>"><%= link_to "Maintenance", maintenance_path %></li>
425
+ <% end %>
417
426
  <li class="<%= controller.action_name == "live_queries" ? "active" : "" %>"><%= link_to "Live Queries", live_queries_path %></li>
418
427
  <li class="<%= controller.action_name == "explain" ? "active" : "" %>"><%= link_to "Explain", explain_path %></li>
419
428
  <li class="<%= controller.action_name == "tune" ? "active" : "" %>"><%= link_to "Tune", tune_path %></li>
@@ -13,9 +13,9 @@
13
13
  </tr>
14
14
  </thead>
15
15
  <tbody>
16
- <% PgHero.connection_sources.first(10).each do |source| %>
16
+ <% connection_sources.each do |source| %>
17
17
  <tr>
18
- <td><%= source["source"] %> <span class="text-muted"><%= source["ip"] %></span></td>
18
+ <td><%= source["source"] %> <div class="text-muted"><%= [source["database"], source["ip"]].compact.join(" ") %></div></td>
19
19
  <td><%= number_with_delimiter(source["total_connections"]) %></td>
20
20
  </tr>
21
21
  <% end %>
@@ -46,6 +46,16 @@
46
46
  <% if query["query"] == "<insufficient privilege>" %>
47
47
  <p class="text-muted">For security reasons, only superusers can see queries executed by other users.</p>
48
48
  <% end %>
49
+ <% if (i2 = @suggested_indexes_by_query[query["query"]]) %>
50
+ <% if (index = i2[:index]) && !i2[:covering_index] %>
51
+ <%= render partial: "suggested_index", locals: {index: index} %>
52
+ <% end %>
53
+ <% if @debug %>
54
+ <code><pre style="color: #f0ad4e; background-color: #333;"><% if i2[:explanation] %><%= i2[:explanation] %><% else %>Rows: <%= i2[:rows] %>
55
+ Row estimates: <%= i2[:row_estimates].to_a.map { |k, v| "#{k}=#{v}" }.join(", ") %>
56
+ Row progresssion: <%= i2[:row_progression].to_a.join(", ") %><% end %></pre></code>
57
+ <% end %>
58
+ <% end %>
49
59
  </td>
50
60
  </tr>
51
61
  <% end %>
@@ -43,6 +43,9 @@
43
43
  }
44
44
 
45
45
  var sort = <%= @sort.to_json.html_safe %>;
46
+ var minAverageTime = <%= @min_average_time.to_json %>;
47
+ var minCalls = <%= @min_calls.to_json %>;
48
+ var debug = <%= @debug.to_json %>;
46
49
 
47
50
  var months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
48
51
 
@@ -100,7 +103,7 @@
100
103
 
101
104
  function queriesPath(params) {
102
105
  var path = "queries";
103
- if (params.start_at || params.end_at || params.sort) {
106
+ if (params.start_at || params.end_at || params.sort || params.min_average_time || params.min_calls || params.debug) {
104
107
  path += "?" + $.param(params);
105
108
  }
106
109
  return path;
@@ -121,14 +124,32 @@
121
124
  if (sort) {
122
125
  params.sort = sort;
123
126
  }
127
+ if (minAverageTime) {
128
+ params.min_average_time = minAverageTime;
129
+ }
130
+ if (minCalls) {
131
+ params.min_calls = minCalls;
132
+ }
133
+ if (debug) {
134
+ params.debug = debug;
135
+ }
124
136
 
125
137
  var path = queriesPath(params);
126
138
 
127
139
  $(".queries-table th a").each( function () {
128
- var p = $.extend({}, params, {sort: $(this).data("sort")});
140
+ var p = $.extend({}, params, {sort: $(this).data("sort"), min_average_time: minAverageTime, min_calls: minCalls, debug: debug});
129
141
  if (!p.sort) {
130
142
  delete p.sort;
131
143
  }
144
+ if (!p.min_average_time) {
145
+ delete p.min_average_time;
146
+ }
147
+ if (!p.min_calls) {
148
+ delete p.min_calls;
149
+ }
150
+ if (!p.debug) {
151
+ delete p.debug;
152
+ }
132
153
  $(this).attr("href", queriesPath(p));
133
154
  });
134
155
 
@@ -0,0 +1 @@
1
+ <code><pre style="color: #eee; background-color: #333;">CREATE INDEX CONCURRENTLY ON <%= index[:table] %> (<%= index[:columns].join(", ") %>)</pre></code>
@@ -1,5 +1,5 @@
1
1
  <div class="content">
2
2
  <h1>Connections</h1>
3
3
 
4
- <%= render partial: "connections_table", locals: {total_connections: @total_connections, show_message: false} %>
4
+ <%= render partial: "connections_table", locals: {total_connections: @total_connections, connection_sources: PgHero.connection_sources(by_database: true), show_message: false} %>
5
5
  </div>
@@ -9,6 +9,9 @@
9
9
  <% if @explanation %>
10
10
  <pre><%= @explanation %></pre>
11
11
  <p><%= link_to "See how to interpret this", "http://www.postgresql.org/docs/current/static/using-explain.html", target: "_blank" %></p>
12
+ <% if (index = @suggested_index) %>
13
+ <%= render partial: "suggested_index", locals: {index: index} %>
14
+ <% end %>
12
15
  <% elsif @error %>
13
16
  <div class="alert alert-danger"><%= @error %></div>
14
17
  <% end %>
@@ -43,9 +43,18 @@
43
43
  No long running queries
44
44
  <% end %>
45
45
  </div>
46
+ <% if PgHero.suggested_indexes_enabled? %>
47
+ <div class="alert alert-<%= @suggested_indexes.empty? ? "success" : "warning" %>">
48
+ <% if @suggested_indexes.any? %>
49
+ <%= pluralize(@suggested_indexes.size, "suggested index", "suggested indexes") %>
50
+ <% else %>
51
+ No suggested indexes
52
+ <% end %>
53
+ </div>
54
+ <% end %>
46
55
  <div class="alert alert-<%= @good_cache_rate ? "success" : "warning" %>">
47
56
  <% if @good_cache_rate %>
48
- Cache hit rate above 99%
57
+ Cache hit rate above <%= PgHero.cache_hit_rate_threshold %>%
49
58
  <% else %>
50
59
  Low cache hit rate
51
60
  <% end %>
@@ -81,6 +90,13 @@
81
90
  No invalid indexes
82
91
  <% end %>
83
92
  </div>
93
+ <div class="alert alert-<%= @duplicate_indexes.empty? ? "success" : "warning" %>">
94
+ <% if @duplicate_indexes.any? %>
95
+ <%= pluralize(@duplicate_indexes.size, "duplicate index", "duplicate indexes") %>
96
+ <% else %>
97
+ No duplicate indexes
98
+ <% end %>
99
+ </div>
84
100
  <div class="alert alert-<%= @missing_indexes.empty? ? "success" : "warning" %>">
85
101
  <% if @missing_indexes.any? %>
86
102
  <%= pluralize(@missing_indexes.size, "table appears", "tables appear") %> to be missing indexes
@@ -114,6 +130,35 @@
114
130
  </div>
115
131
  <% end %>
116
132
 
133
+ <% if @suggested_indexes.any? %>
134
+ <div class="content">
135
+ <h1>Suggested Indexes</h1>
136
+ <p>
137
+ Add indexes to speed up queries.
138
+ <% if @show_migrations %>
139
+ Here’s a
140
+ <a href="javascript: void(0);" onclick="document.getElementById('migration3').style.display = 'block';">migration</a> to help.
141
+ <% end %>
142
+ </p>
143
+
144
+ <div id="migration3" style="display: none;">
145
+ <pre>rails g migration add_suggested_indexes</pre>
146
+ <p>And paste</p>
147
+ <pre style="overflow: scroll; white-space: pre; word-break: normal;"><% @suggested_indexes.each do |index| %>
148
+ add_index <%= index[:table].to_sym.inspect %>, [<%= index[:columns].map(&:to_sym).map(&:inspect).join(" ,") %>], algorithm: :concurrently<% end %></pre>
149
+ </div>
150
+
151
+ <% @suggested_indexes.each_with_index do |index, i| %>
152
+ <hr />
153
+ <%= render partial: "suggested_index", locals: {index: index} %>
154
+ <p>to speed up</p>
155
+ <% index[:queries].each do |query| %>
156
+ <code><pre><%= query %></pre></code>
157
+ <% end %>
158
+ <% end %>
159
+ </div>
160
+ <% end %>
161
+
117
162
  <% if !@good_cache_rate %>
118
163
  <div class="content">
119
164
  <h1>Low Cache Hit Rate</h1>
@@ -134,7 +179,7 @@
134
179
  <% if !@good_total_connections %>
135
180
  <div class="content">
136
181
  <h1>High Number of Connections</h1>
137
- <%= render partial: "connections_table", locals: {total_connections: @total_connections, show_message: true} %>
182
+ <%= render partial: "connections_table", locals: {total_connections: @total_connections, connection_sources: PgHero.connection_sources(by_database: true).first(10), show_message: true} %>
138
183
  </div>
139
184
  <% end %>
140
185
 
@@ -215,6 +260,49 @@ pg_stat_statements.track = all</pre>
215
260
  </div>
216
261
  <% end %>
217
262
 
263
+ <% if @duplicate_indexes.any? %>
264
+ <div class="content">
265
+ <h1>Duplicate Indexes</h1>
266
+
267
+ <p>
268
+ These indexes exist, but aren’t needed. Remove them
269
+ <% if @show_migrations %>
270
+ <a href="javascript: void(0);" onclick="document.getElementById('migration2').style.display = 'block';">with a migration</a>
271
+ <% end %>
272
+ for faster writes.
273
+ </p>
274
+
275
+ <div id="migration2" style="display: none;">
276
+ <pre>rails g migration remove_unneeded_indexes</pre>
277
+ <p>And paste</p>
278
+ <pre style="overflow: scroll; white-space: pre; word-break: normal;"><% @duplicate_indexes.each do |query| %>
279
+ remove_index <%= query["unneeded_index"]["table"].to_sym.inspect %>, name: <%= query["unneeded_index"]["name"].to_s.inspect %><% end %></pre>
280
+ </div>
281
+
282
+ <table class="table">
283
+ <thead>
284
+ <tr>
285
+ <th>Details</th>
286
+ </tr>
287
+ </thead>
288
+ <tbody>
289
+ <% @duplicate_indexes.each do |index| %>
290
+ <% unneeded_index = index["unneeded_index"] %>
291
+ <% covering_index = index["covering_index"] %>
292
+ <tr>
293
+ <td style="padding-top: 15px; padding-bottom: 5px;">
294
+ On <%= unneeded_index["table"] %>
295
+ <pre><%= unneeded_index["name"] %> (<%= unneeded_index["columns"].join(", ") %>)</pre>
296
+ is covered by
297
+ <pre><%= covering_index["name"] %> (<%= covering_index["columns"].join(", ") %>)</pre>
298
+ </td>
299
+ </tr>
300
+ <% end %>
301
+ </tbody>
302
+ </table>
303
+ </div>
304
+ <% end %>
305
+
218
306
  <% if @missing_indexes.any? %>
219
307
  <div class="content">
220
308
  <h1>Missing Indexes</h1>
@@ -248,7 +336,9 @@ pg_stat_statements.track = all</pre>
248
336
 
249
337
  <p>
250
338
  Unused indexes cause unnecessary overhead. Remove them
339
+ <% if @show_migrations %>
251
340
  <a href="javascript: void(0);" onclick="document.getElementById('migration').style.display = 'block';">with a migration</a>
341
+ <% end %>
252
342
  for faster writes.
253
343
  </p>
254
344
 
@@ -0,0 +1,32 @@
1
+ <div class="content">
2
+ <h1>Maintenance</h1>
3
+
4
+ <table class="table">
5
+ <thead>
6
+ <tr>
7
+ <th>Table</th>
8
+ <th style="width: 20%;">Last Vacuum</th>
9
+ <th style="width: 20%;">Last Analyze</th>
10
+ </tr>
11
+ </thead>
12
+ <tbody>
13
+ <% @maintenance_info.each do |table| %>
14
+ <tr>
15
+ <td><%= table["table"] %></td>
16
+ <td>
17
+ <% time = [table["last_autovacuum"], table["last_vacuum"]].compact.max %>
18
+ <% if time %>
19
+ <%= PgHero.time_zone.parse(time).strftime("%m/%-e %l:%M %P") %>
20
+ <% end %>
21
+ </td>
22
+ <td>
23
+ <% time = [table["last_autoanalyze"], table["last_analyze"]].compact.max %>
24
+ <% if time %>
25
+ <%= PgHero.time_zone.parse(time).strftime("%m/%-e %l:%M %P") %>
26
+ <% end %>
27
+ </td>
28
+ </tr>
29
+ <% end %>
30
+ </tbody>
31
+ </table>
32
+ </div>
@@ -9,6 +9,10 @@
9
9
  <%= render partial: "query_stats_slider" %>
10
10
  <% end %>
11
11
 
12
+ <% if PgHero.suggested_indexes_enabled? %>
13
+ <p style="text-align: center; margin-top: 7px;">PgHero now suggests indexes. <%= link_to "See how it thinks", {debug: true} %>.</p>
14
+ <% end %>
15
+
12
16
  <% if @query_stats_enabled %>
13
17
  <% if @error %>
14
18
  <div class="alert alert-danger">Cannot understand start or end time.</div>