pghero 1.5.3 → 1.6.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: 822e5e1791b9a405db29590569bad48b312738aa
4
- data.tar.gz: 628fdcaad3d38fedd644bd2574582c3676cfb37b
3
+ metadata.gz: b86a04757fce4dfc91177eb7794c20ae289ec2d5
4
+ data.tar.gz: ba8f9f333064208ea84fd6a4c222cd531f18637b
5
5
  SHA512:
6
- metadata.gz: f586c4e66499887b8694463665b0be0bf4b90f4428b470635750cd65d83e1071018730f9954819e8203b8b5cafefad360d254e7e843174c3afb4d3a940ffcd51
7
- data.tar.gz: 2a0ef31b0a3a9510ae835535cf9563180974781f9d4d761a2224866a82b5e845ac5750d00709c054700416479bc3492d09d0cc28e978f05cdb76dfd5bc63d628
6
+ metadata.gz: d97627eb0174decf527ea8ca4554fcb8d45bc95c28eeb4fc250b7921c544f685af741ec85c5cfd1815a7749af36b4f75a4e90ef2b261adae873a9718ded34de3
7
+ data.tar.gz: e8fedfa37a433af1d4c2b647242b06bc6cc8ba1697f45e564123d3d77a8db599e722322a047d7c51b4c528805324df5db1f5f08b7bda5ae62ffda093d7f4a718
@@ -1,3 +1,10 @@
1
+ ## 1.6.0
2
+
3
+ - Removed mostly inactionable items (cache hit rate and index usage)
4
+ - Restored duplicate indexes to homepage
5
+ - Fixed issue with exact duplicate indexes
6
+ - Way better `blocked_queries` method
7
+
1
8
  ## 1.5.3
2
9
 
3
10
  - Fixed Rails 5 error with multiple databases
data/README.md CHANGED
@@ -1,10 +1,10 @@
1
1
  # PgHero
2
2
 
3
- A performance dashboard for Postgres
3
+ A performance dashboard for Postgres - health checks, suggested indexes, and more :tada:
4
4
 
5
5
  [See it in action](https://pghero.herokuapp.com/)
6
6
 
7
- [![Screenshot](https://pghero.herokuapp.com/assets/screenshot-a54dead9c9bfc4c1176b184c5bd97ca1.png)](https://pghero.herokuapp.com/)
7
+ [![Screenshot](https://pghero.herokuapp.com/assets/screenshot-5a368624ada55b32e7668c96926840f9.png)](https://pghero.herokuapp.com/)
8
8
 
9
9
  :speech_balloon: Get [handcrafted updates](http://chartkick.us7.list-manage.com/subscribe?u=952c861f99eb43084e0a49f98&id=6ea6541e8e&group[0][16]=true) for new features
10
10
 
@@ -16,21 +16,19 @@ module PgHero
16
16
 
17
17
  def index
18
18
  @title = "Overview"
19
+ @extended = params[:extended]
19
20
  @query_stats = @database.query_stats(historical: true, start_at: 3.hours.ago)
20
21
  @slow_queries = @database.slow_queries(query_stats: @query_stats)
21
22
  @long_running_queries = @database.long_running_queries
22
- @index_hit_rate = @database.index_hit_rate
23
- @table_hit_rate = @database.table_hit_rate
24
- @missing_indexes =
25
- if @database.suggested_indexes_enabled?
26
- []
27
- else
28
- @database.missing_indexes
29
- end
23
+ if @extended
24
+ @index_hit_rate = @database.index_hit_rate
25
+ @table_hit_rate = @database.table_hit_rate
26
+ @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
27
+ end
28
+
30
29
  @unused_indexes = @database.unused_indexes.select { |q| q["index_scans"].to_i == 0 }
31
30
  @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
31
+ @duplicate_indexes = @database.duplicate_indexes
34
32
  unless @query_stats_enabled
35
33
  @query_stats_available = @database.query_stats_available?
36
34
  @query_stats_extension_enabled = @database.query_stats_extension_enabled? if @query_stats_available
@@ -36,7 +36,6 @@
36
36
  <% if @query_stats_enabled %>
37
37
  <li class="<%= controller.action_name == "queries" ? "active" : "" %>"><%= link_to "Queries", queries_path %></li>
38
38
  <% end %>
39
- <li class="<%= controller.action_name == "index_usage" ? "active" : "" %>"><%= link_to "Index Usage", index_usage_path %></li>
40
39
  <li class="<%= controller.action_name == "space" ? "active" : "" %>"><%= link_to "Space", space_path %></li>
41
40
  <li class="<%= controller.action_name == "connections" ? "active" : "" %>"><%= link_to "Connections", connections_path %></li>
42
41
  <li class="<%= controller.action_name == "live_queries" ? "active" : "" %>"><%= link_to "Live Queries", live_queries_path %></li>
@@ -16,16 +16,18 @@
16
16
  No long running queries
17
17
  <% end %>
18
18
  </div>
19
- <div class="alert alert-<%= @good_cache_rate ? "success" : "warning" %>">
20
- <% if @good_cache_rate %>
21
- Cache hit rate above <%= @database.cache_hit_rate_threshold %>%
22
- <% else %>
23
- Low cache hit rate
24
- <% end %>
25
- </div>
19
+ <% if @extended %>
20
+ <div class="alert alert-<%= @good_cache_rate ? "success" : "warning" %>">
21
+ <% if @good_cache_rate %>
22
+ Cache hit rate above <%= @database.cache_hit_rate_threshold %>%
23
+ <% else %>
24
+ Low cache hit rate
25
+ <% end %>
26
+ </div>
27
+ <% end %>
26
28
  <div class="alert alert-<%= @good_total_connections ? "success" : "warning" %>">
27
29
  <% if @good_total_connections %>
28
- Healthy number of connections
30
+ Number of connections healthy
29
31
  <% else %>
30
32
  High number of connections
31
33
  <% end %>
@@ -42,7 +44,7 @@
42
44
  <% if @sequence_danger.any? %>
43
45
  <%= pluralize(@sequence_danger.size, "columns") %> approaching overflow
44
46
  <% else %>
45
- No columns near overflow
47
+ No columns near integer overflow
46
48
  <% end %>
47
49
  </div>
48
50
  <div class="alert alert-<%= @invalid_indexes.empty? ? "success" : "warning" %>">
@@ -69,14 +71,6 @@
69
71
  No suggested indexes
70
72
  <% end %>
71
73
  </div>
72
- <% else %>
73
- <div class="alert alert-<%= @missing_indexes.empty? ? "success" : "warning" %>">
74
- <% if @missing_indexes.any? %>
75
- <%= pluralize(@missing_indexes.size, "table appears", "tables appear") %> to be missing indexes
76
- <% else %>
77
- No missing indexes
78
- <% end %>
79
- </div>
80
74
  <% end %>
81
75
  <div class="alert alert-<%= @query_stats_enabled && @slow_queries.empty? ? "success" : "warning" %>">
82
76
  <% if !@query_stats_enabled %>
@@ -106,7 +100,7 @@
106
100
  </div>
107
101
  <% end %>
108
102
 
109
- <% if !@good_cache_rate %>
103
+ <% if @extended && !@good_cache_rate %>
110
104
  <div class="content">
111
105
  <h1>Low Cache Hit Rate</h1>
112
106
 
@@ -135,7 +129,7 @@
135
129
  <h2>Vacuuming Needed</h2>
136
130
  <p>The database <strong>will shutdown</strong> when there are fewer than 1,000,000 transactions left. <%= link_to "Read more", "http://www.postgresql.org/docs/9.1/static/routine-vacuuming.html#VACUUM-FOR-WRAPAROUND", target: "_blank" %>.</p>
137
131
  <p>For each table, run:</p>
138
- <code><pre>VACUUM FREEZE VERBOSE table</pre></code>
132
+ <code><pre>VACUUM FREEZE VERBOSE table;</pre></code>
139
133
  <table class="table">
140
134
  <thead>
141
135
  <tr>
@@ -270,11 +264,13 @@ remove_index <%= query["unneeded_index"]["table"].to_sym.inspect %>, name: <%= q
270
264
  <div id="migration3" style="display: none;">
271
265
  <pre>rails g migration add_suggested_indexes</pre>
272
266
  <p>And paste</p>
273
- <pre style="overflow: scroll; white-space: pre; word-break: normal;"><% @suggested_indexes.each do |index| %>
267
+ <pre style="overflow: scroll; white-space: pre; word-break: normal;">commit_db_transaction
268
+ <% @suggested_indexes.each do |index| %>
274
269
  <% if index[:using] == "gist" %>
275
270
  connection.execute("CREATE INDEX CONCURRENTLY ON <%= index[:table] %><% if index[:using] %> USING <%= index[:using] %><% end %> (<%= index[:columns].join(", ") %>)")
276
271
  <% else %>
277
- add_index <%= index[:table].to_sym.inspect %>, [<%= index[:columns].map(&:to_sym).map(&:inspect).join(", ") %>], algorithm: :concurrently<% end %><% end %></pre>
272
+ add_index <%= index[:table].to_sym.inspect %>, [<%= index[:columns].map(&:to_sym).map(&:inspect).join(", ") %>], algorithm: :concurrently<% end %>
273
+ <% end %></pre>
278
274
  </div>
279
275
 
280
276
  <% @suggested_indexes.each_with_index do |index, i| %>
@@ -286,33 +282,6 @@ add_index <%= index[:table].to_sym.inspect %>, [<%= index[:columns].map(&:to_sym
286
282
  </div>
287
283
  <% end %>
288
284
 
289
- <% if @missing_indexes.any? %>
290
- <div class="content">
291
- <h1>Missing Indexes</h1>
292
-
293
- <p>These tables have a large number of rows but indexes are not used often. Add indexes for faster queries.</p>
294
-
295
- <table class="table">
296
- <thead>
297
- <tr>
298
- <th>Table</th>
299
- <th>% of Time Index Used</th>
300
- <th>Rows in Table</th>
301
- </tr>
302
- </thead>
303
- <tbody>
304
- <% @missing_indexes.each do |query| %>
305
- <tr>
306
- <td><%= query["table"] %></td>
307
- <td style="width: 30%;"><%= query["percent_of_times_index_used"] %></td>
308
- <td style="width: 20%;"><%= number_with_delimiter(query["rows_in_table"]) %></td>
309
- </tr>
310
- <% end %>
311
- </tbody>
312
- </table>
313
- </div>
314
- <% end %>
315
-
316
285
  <% if !@query_stats_enabled %>
317
286
  <div class="content">
318
287
  <h1>Query Stats</h1>
@@ -22,12 +22,16 @@
22
22
  <% time = [table["last_autovacuum"], table["last_vacuum"]].compact.max %>
23
23
  <% if time %>
24
24
  <%= @time_zone.parse(time).strftime("%m/%-e %l:%M %P") %>
25
+ <% else %>
26
+ <span class="text-muted">Unknown</span>
25
27
  <% end %>
26
28
  </td>
27
29
  <td>
28
30
  <% time = [table["last_autoanalyze"], table["last_analyze"]].compact.max %>
29
31
  <% if time %>
30
32
  <%= @time_zone.parse(time).strftime("%m/%-e %l:%M %P") %>
33
+ <% else %>
34
+ <span class="text-muted">Unknown</span>
31
35
  <% end %>
32
36
  </td>
33
37
  </tr>
@@ -186,6 +186,26 @@ sudo pghero config:set PGHERO_SECRET_ACCESS_KEY=secret123
186
186
  sudo pghero config:set PGHERO_DB_INSTANCE_IDENTIFIER=epona
187
187
  ```
188
188
 
189
+ ## Multiple Databases
190
+
191
+ Create a `pghero.yml` with:
192
+
193
+ ```yml
194
+ production:
195
+ databases:
196
+ primary:
197
+ url: postgres://...
198
+ replica:
199
+ url: postgres://...
200
+ ```
201
+
202
+ And run:
203
+
204
+ ```sh
205
+ cat pghero.yml | sudo pghero run sh -c "cat > config/pghero.yml"
206
+ sudo service pghero restart
207
+ ```
208
+
189
209
  ## Customize
190
210
 
191
211
  Minimum time for long running queries
@@ -142,7 +142,7 @@ module PgHero
142
142
  indexes_by_table = self.indexes.group_by { |i| i["table"] }
143
143
  indexes_by_table.values.flatten.select { |i| PgHero.falsey?(i["primary"]) && PgHero.falsey?(i["unique"]) && !i["indexprs"] && !i["indpred"] && PgHero.truthy?(i["valid"]) }.each do |index|
144
144
  covering_index = indexes_by_table[index["table"]].find { |i| index_covers?(i["columns"], index["columns"]) && i["using"] == index["using"] && i["name"] != index["name"] && !i["indexprs"] && !i["indpred"] && PgHero.truthy?(i["valid"]) }
145
- if covering_index
145
+ if covering_index && (covering_index["columns"] != index["columns"] || index["name"] > covering_index["name"])
146
146
  indexes << {"unneeded_index" => index, "covering_index" => covering_index}
147
147
  end
148
148
  end
@@ -57,28 +57,37 @@ module PgHero
57
57
  end
58
58
 
59
59
  # from https://wiki.postgresql.org/wiki/Lock_Monitoring
60
+ # and http://big-elephants.com/2013-09/exploring-query-locks-in-postgres/
60
61
  def blocked_queries
61
62
  select_all <<-SQL
62
63
  SELECT
63
- bl.pid AS blocked_pid,
64
- a.usename AS blocked_user,
65
- ka.query AS current_or_recent_query_in_blocking_process,
66
- ka.state AS state_of_blocking_process,
67
- age(now(), ka.query_start) AS blocking_duration,
68
- kl.pid AS blocking_pid,
69
- ka.usename AS blocking_user,
70
- a.query AS blocked_query,
71
- age(now(), a.query_start) AS blocked_duration
64
+ COALESCE(blockingl.relation::regclass::text,blockingl.locktype) as locked_item,
65
+ blockeda.pid AS blocked_pid,
66
+ blockeda.usename AS blocked_user,
67
+ blockeda.query as blocked_query,
68
+ age(now(), blockeda.query_start) AS blocked_duration,
69
+ blockedl.mode as blocked_mode,
70
+ blockinga.pid AS blocking_pid,
71
+ blockinga.usename AS blocking_user,
72
+ blockinga.state AS state_of_blocking_process,
73
+ blockinga.query AS current_or_recent_query_in_blocking_process,
74
+ age(now(), blockinga.query_start) AS blocking_duration,
75
+ blockingl.mode as blocking_mode
72
76
  FROM
73
- pg_catalog.pg_locks bl
74
- JOIN
75
- pg_catalog.pg_stat_activity a ON a.pid = bl.pid
76
- JOIN
77
- pg_catalog.pg_locks kl ON kl.transactionid = bl.transactionid AND kl.pid != bl.pid
78
- JOIN
79
- pg_catalog.pg_stat_activity ka ON ka.pid = kl.pid
77
+ pg_catalog.pg_locks blockedl
78
+ LEFT JOIN
79
+ pg_stat_activity blockeda ON blockedl.pid = blockeda.pid
80
+ LEFT JOIN
81
+ pg_catalog.pg_locks blockingl ON blockedl.pid != blockingl.pid AND (
82
+ blockingl.transactionid = blockedl.transactionid
83
+ OR (blockingl.relation = blockedl.relation AND blockingl.locktype = blockedl.locktype)
84
+ )
85
+ LEFT JOIN
86
+ pg_stat_activity blockinga ON blockingl.pid = blockinga.pid AND blockinga.datid = blockeda.datid
80
87
  WHERE
81
- NOT bl.GRANTED
88
+ NOT blockedl.granted
89
+ AND blockeda.query <> '<insufficient privilege>'
90
+ AND blockeda.datname = current_database()
82
91
  ORDER BY
83
92
  blocked_duration DESC
84
93
  SQL
@@ -1,3 +1,3 @@
1
1
  module PgHero
2
- VERSION = "1.5.3"
2
+ VERSION = "1.6.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pghero
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.3
4
+ version: 1.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kane
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-10-07 00:00:00.000000000 Z
11
+ date: 2016-10-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord