pghero 1.7.0 → 2.0.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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +2 -0
  3. data/CHANGELOG.md +31 -0
  4. data/README.md +2 -2
  5. data/app/assets/javascripts/pghero/Chart.bundle.js +7512 -5661
  6. data/app/assets/javascripts/pghero/application.js +9 -0
  7. data/app/assets/javascripts/pghero/highlight.pack.js +2 -0
  8. data/app/assets/stylesheets/pghero/application.css +54 -2
  9. data/app/assets/stylesheets/pghero/arduino-light.css +86 -0
  10. data/app/controllers/pg_hero/home_controller.rb +148 -52
  11. data/app/helpers/pg_hero/base_helper.rb +15 -0
  12. data/app/views/layouts/pg_hero/application.html.erb +1 -1
  13. data/app/views/pg_hero/home/_connections_table.html.erb +2 -2
  14. data/app/views/pg_hero/home/_live_queries_table.html.erb +11 -7
  15. data/app/views/pg_hero/home/_queries_table.html.erb +21 -10
  16. data/app/views/pg_hero/home/_suggested_index.html.erb +1 -1
  17. data/app/views/pg_hero/home/connections.html.erb +2 -14
  18. data/app/views/pg_hero/home/explain.html.erb +1 -1
  19. data/app/views/pg_hero/home/index.html.erb +58 -22
  20. data/app/views/pg_hero/home/index_bloat.html.erb +69 -0
  21. data/app/views/pg_hero/home/maintenance.html.erb +7 -7
  22. data/app/views/pg_hero/home/queries.html.erb +10 -0
  23. data/app/views/pg_hero/home/relation_space.html.erb +9 -0
  24. data/app/views/pg_hero/home/show_query.html.erb +107 -0
  25. data/app/views/pg_hero/home/space.html.erb +64 -10
  26. data/config/routes.rb +4 -2
  27. data/guides/Rails.md +28 -1
  28. data/guides/Suggested-Indexes.md +1 -1
  29. data/lib/pghero.rb +25 -36
  30. data/lib/pghero/database.rb +5 -1
  31. data/lib/pghero/methods/basic.rb +78 -13
  32. data/lib/pghero/methods/connections.rb +16 -56
  33. data/lib/pghero/methods/explain.rb +2 -6
  34. data/lib/pghero/methods/indexes.rb +173 -18
  35. data/lib/pghero/methods/kill.rb +2 -2
  36. data/lib/pghero/methods/maintenance.rb +23 -26
  37. data/lib/pghero/methods/queries.rb +1 -23
  38. data/lib/pghero/methods/query_stats.rb +95 -96
  39. data/lib/pghero/methods/{replica.rb → replication.rb} +17 -4
  40. data/lib/pghero/methods/sequences.rb +4 -5
  41. data/lib/pghero/methods/space.rb +101 -8
  42. data/lib/pghero/methods/suggested_indexes.rb +49 -108
  43. data/lib/pghero/methods/system.rb +14 -10
  44. data/lib/pghero/methods/tables.rb +8 -8
  45. data/lib/pghero/methods/users.rb +10 -12
  46. data/lib/pghero/version.rb +1 -1
  47. data/lib/tasks/pghero.rake +1 -1
  48. data/test/basic_test.rb +38 -0
  49. data/test/best_index_test.rb +3 -3
  50. data/test/suggested_indexes_test.rb +0 -2
  51. data/test/test_helper.rb +38 -40
  52. metadata +11 -6
  53. data/app/views/pg_hero/home/index_usage.html.erb +0 -27
  54. data/test/explain_test.rb +0 -18
@@ -0,0 +1,15 @@
1
+ module PgHero
2
+ module BaseHelper
3
+ def pghero_pretty_ident(table, schema: nil)
4
+ ident = table
5
+ if schema && schema != "public"
6
+ indent = "#{schema}.#{table}"
7
+ end
8
+ if ident =~ /\A[a-z0-9_]+\z/
9
+ ident
10
+ else
11
+ @database.quote_ident(ident)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -51,7 +51,7 @@
51
51
  <ul class="nav">
52
52
  <% @databases.each do |database| %>
53
53
  <li class="<%= ("active-database" if @database.id == database.id) %>">
54
- <%= link_to database.name, database: database.id %>
54
+ <%= link_to database.name, action_name == "show_query" ? root_path(database: database.id) : {database: database.id} %>
55
55
  </li>
56
56
  <% end %>
57
57
  </ul>
@@ -8,8 +8,8 @@
8
8
  <tbody>
9
9
  <% connection_sources.each do |source| %>
10
10
  <tr>
11
- <td><%= source["source"] %> <div class="text-muted"><%= [source["user"], source["database"], source["ip"]].compact.join(" - ") %></div></td>
12
- <td><%= number_with_delimiter(source["total_connections"]) %></td>
11
+ <td><%= source[:source] %> <div class="text-muted"><%= [source[:user], source[:database], source[:ip]].compact.join(" - ") %></div></td>
12
+ <td><%= number_with_delimiter(source[:total_connections]) %></td>
13
13
  </tr>
14
14
  <% end %>
15
15
  </tbody>
@@ -10,21 +10,25 @@
10
10
  <tbody>
11
11
  <% queries.reverse.each do |query| %>
12
12
  <tr>
13
- <td><%= query["pid"] %></td>
14
- <td><%= number_with_delimiter(query["duration_ms"].to_f.round) %> ms</td>
15
- <td><%= query["state"] %></td>
13
+ <td><%= query[:pid] %></td>
14
+ <td><%= number_with_delimiter(query[:duration_ms].round) %> ms</td>
15
+ <td><%= query[:state] %></td>
16
16
  <td class="text-right">
17
- <% button_path, button_options = Rails.version >= "4.1" ? [explain_path, {params: {query: query["query"]}}] : [explain_path(query: query["query"]), {}] %>
17
+ <% button_path, button_options = Rails.version >= "4.1" ? [explain_path, {params: {query: query[:query]}}] : [explain_path(query: query[:query]), {}] %>
18
18
  <%= button_to "Explain", button_path, button_options.merge(form: {target: "_blank"}, class: "btn btn-info") %>
19
- <%= button_to "Kill", kill_path(pid: query["pid"]), class: "btn btn-danger" %>
19
+ <%= button_to "Kill", kill_path(pid: query[:pid]), class: "btn btn-danger" %>
20
20
  </td>
21
21
  </tr>
22
22
  <tr>
23
23
  <td colspan="6" style="border-top: none; padding: 0;">
24
- <%= query["source"] %> <span class="text-muted"><%= query["user"] %></span>
25
- <pre><%= query["query"] %></pre>
24
+ <%= query[:source] %> <span class="text-muted"><%= query[:user] %></span>
25
+ <pre style="margin-top: 1em;"><code><%= query[:query] %></code></pre>
26
26
  </td>
27
27
  </tr>
28
28
  <% end %>
29
29
  </tbody>
30
30
  </table>
31
+
32
+ <script>
33
+ highlightQueries();
34
+ </script>
@@ -25,9 +25,9 @@
25
25
  <% queries.each do |query| %>
26
26
  <tr>
27
27
  <td>
28
- <%= number_with_delimiter(query["total_minutes"].to_f.round) %> min
28
+ <%= number_with_delimiter(query[:total_minutes].round) %> min
29
29
  <span class="percent">
30
- <% percent = query["total_percent"].to_f %>
30
+ <% percent = query[:total_percent] %>
31
31
  <% if percent > 1 %>
32
32
  <%= percent.round %>%
33
33
  <% elsif percent > 0.1 %>
@@ -37,21 +37,32 @@
37
37
  <% end %>
38
38
  </span>
39
39
  </td>
40
- <td><%= number_with_delimiter(query["average_time"].to_f.round) %> ms</td>
41
40
  <td>
42
- <%= number_with_delimiter(query["calls"].to_i) %>
43
- <% if query["user"] %>
44
- <span class="user"><%= query["user"] %></span>
45
- <% end %>
41
+ <%= number_with_delimiter(query[:average_time].round) %> ms
42
+ </td>
43
+ <td>
44
+ <%= number_with_delimiter(query[:calls]) %>
45
+
46
+ <span class="user">
47
+ <% if query[:user] %>
48
+ <%= query[:user] %>
49
+ <% if @show_details %>
50
+ &middot;
51
+ <% end %>
52
+ <% end %>
53
+ <% if @show_details %>
54
+ <%= link_to "details", show_query_path(query[:query_hash]), target: "_blank" %>
55
+ <% end %>
56
+ </span>
46
57
  </td>
47
58
  </tr>
48
59
  <tr>
49
60
  <td colspan="3" style="border-top: none; padding: 0;">
50
- <code><pre style="max-height: 230px; overflow: hidden;" onclick="this.style.maxHeight = 'none';"><%= query["query"] %></pre></code>
51
- <% if query["query"] == "<insufficient privilege>" %>
61
+ <pre><code style="max-height: 230px; overflow: hidden;" onclick="this.style.maxHeight = 'none';"><%= query[:query] %></code></pre>
62
+ <% if query[:query] == "<insufficient privilege>" %>
52
63
  <p class="text-muted">For security reasons, only superusers can see queries executed by other users.</p>
53
64
  <% end %>
54
- <% if local_assigns[:suggested_indexes] != false && (details = @suggested_indexes_by_query[query["query"]]) %>
65
+ <% if local_assigns[:suggested_indexes] != false && (details = @suggested_indexes_by_query[query[:query]]) && (details[:index] || @debug) %>
55
66
  <%= render partial: "suggested_index", locals: {index: details[:index], details: details} %>
56
67
  <% end %>
57
68
  </td>
@@ -13,6 +13,6 @@ Row estimates
13
13
  <%= details[:row_estimates].to_a.map { |k, v| "- #{k}: #{v}" }.join("\n") %><% end %><% if details[:table_indexes] %>
14
14
 
15
15
  Existing indexes
16
- <% details[:table_indexes].sort_by { |i| [PgHero.truthy?(i["primary"]) ? 0 : 1, i["columns"]] }.each do |i3| %>- <%= i3["columns"].join(", ") %><% if i3["using"] != "btree" %> <%= i3["using"].to_s.upcase %><% end %><% if PgHero.truthy?(i3["primary"]) %> PRIMARY<% elsif PgHero.truthy?(i3["unique"]) %> UNIQUE<% end %>
16
+ <% 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 %>
17
17
  <% end %><% end %></pre></code>
18
18
  </div>
@@ -6,28 +6,16 @@
6
6
  <% if @total_connections > 0 %>
7
7
  <h3>By Database</h3>
8
8
 
9
- <% top_connections = Hash.new(0) %>
10
- <% @connection_sources.each do |source| %>
11
- <% top_connections[source["database"]] += source["total_connections"].to_i %>
12
- <% end %>
13
- <% top_connections = top_connections.sort_by { |k, v| [-v, k] } %>
14
-
15
9
  <div id="chart-1" class="chart" style="height: 260px; line-height: 260px; margin-bottom: 20px;">Loading...</div>
16
10
  <script>
17
- new Chartkick.PieChart("chart-1", <%= json_escape(top_connections.to_json).html_safe %>);
11
+ new Chartkick.PieChart("chart-1", <%= json_escape(@connections_by_database.to_json).html_safe %>);
18
12
  </script>
19
13
 
20
14
  <h3>By User</h3>
21
15
 
22
- <% top_connections = Hash.new(0) %>
23
- <% @connection_sources.each do |source| %>
24
- <% top_connections[source["user"]] += source["total_connections"].to_i %>
25
- <% end %>
26
- <% top_connections = top_connections.sort_by { |k, v| [-v, k] } %>
27
-
28
16
  <div id="chart-2" class="chart" style="height: 260px; line-height: 260px; margin-bottom: 20px;">Loading...</div>
29
17
  <script>
30
- new Chartkick.PieChart("chart-2", <%= json_escape(top_connections.to_json).html_safe %>);
18
+ new Chartkick.PieChart("chart-2", <%= json_escape(@connections_by_user.to_json).html_safe %>);
31
19
  </script>
32
20
 
33
21
  <%= render partial: "connections_table", locals: {connection_sources: @connection_sources} %>
@@ -14,7 +14,7 @@
14
14
  <% if @visualize %>
15
15
  <p>Paste the output below into the <%= link_to "Postgres Explain Visualizer", "http://tatiyants.com/pev/#/plans/new", target: "_blank" %></p>
16
16
  <% end %>
17
- <pre><%= @explanation %></pre>
17
+ <pre><code><%= @explanation %></code></pre>
18
18
  <% unless @visualize %>
19
19
  <p><%= link_to "See how to interpret this", "http://www.postgresql.org/docs/current/static/using-explain.html", target: "_blank" %></p>
20
20
  <% end %>
@@ -8,6 +8,10 @@
8
8
  <% end %>
9
9
  <span class="tiny"><%= number_with_delimiter((@replication_lag * 1000).round) %> ms</span>
10
10
  </div>
11
+ <% elsif @inactive_replication_slots.any? %>
12
+ <div class="alert alert-warning">
13
+ <%= pluralize(@inactive_replication_slots.size, "inactive replication slot") %>
14
+ </div>
11
15
  <% end %>
12
16
  <div class="alert alert-<%= @long_running_queries.empty? ? "success" : "warning" %>">
13
17
  <% if @long_running_queries.any? %>
@@ -103,6 +107,29 @@
103
107
  </div>
104
108
  <% end %>
105
109
 
110
+ <% if @inactive_replication_slots && @inactive_replication_slots.any? %>
111
+ <div class="content">
112
+ <h1>Inactive Replication Slots</h1>
113
+ <p>Inactive replication slots can cause a lot of disk space to be consumed.</p>
114
+ <p>For each, run:</p>
115
+ <pre><code>SELECT pg_drop_replication_slot('slot_name');</code></pre>
116
+ <table class="table">
117
+ <thead>
118
+ <tr>
119
+ <th>Name</th>
120
+ </tr>
121
+ </thead>
122
+ <tbody>
123
+ <% @inactive_replication_slots.each do |slot| %>
124
+ <tr>
125
+ <td><%= slot[:slot_name] %></td>
126
+ </tr>
127
+ <% end %>
128
+ </tbody>
129
+ </table>
130
+ </div>
131
+ <% end %>
132
+
106
133
  <% if @long_running_queries.any? %>
107
134
  <div class="content">
108
135
  <%= button_to "Kill All", kill_long_running_queries_path, class: "btn btn-danger", style: "float: right;" %>
@@ -136,7 +163,7 @@
136
163
 
137
164
  <p><%= link_to "Use connection pooling", "http://www.craigkerstiens.com/2014/05/22/on-connection-pooling/", target: "_blank" %> for better performance. <%= link_to "PgBouncer", "https://wiki.postgresql.org/wiki/PgBouncer", target: "_blank" %> is a solid option.</p>
138
165
 
139
- <%= render partial: "connections_table", locals: {connection_sources: @database.connection_sources(by_database_and_user: true).first(10)} %>
166
+ <%= render partial: "connections_table", locals: {connection_sources: @database.connection_sources.first(10)} %>
140
167
  </div>
141
168
  <% end %>
142
169
 
@@ -145,7 +172,7 @@
145
172
  <h2>Vacuuming Needed</h2>
146
173
  <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>
147
174
  <p>For each table, run:</p>
148
- <code><pre>VACUUM FREEZE VERBOSE table;</pre></code>
175
+ <pre><code>VACUUM FREEZE VERBOSE table;</code></pre>
149
176
  <table class="table">
150
177
  <thead>
151
178
  <tr>
@@ -156,8 +183,13 @@
156
183
  <tbody>
157
184
  <% @transaction_id_danger.each do |query| %>
158
185
  <tr>
159
- <td><%= query["table"] %></td>
160
- <td><%= number_with_delimiter(query["transactions_before_shutdown"]) %></td>
186
+ <td>
187
+ <%= query[:table] %>
188
+ <% if query[:schema] != "public" %>
189
+ <span class="text-muted"><%= query[:schema] %></span>
190
+ <% end %>
191
+ </td>
192
+ <td><%= number_with_delimiter(query[:transactions_left]) %></td>
161
193
  </tr>
162
194
  <% end %>
163
195
  </tbody>
@@ -179,19 +211,19 @@
179
211
  </tr>
180
212
  </thead>
181
213
  <tbody>
182
- <% @sequence_danger.sort_by { |s| [s["table"], s["column"]] }.each do |query| %>
214
+ <% @sequence_danger.sort_by { |s| [s[:table], s[:column]] }.each do |query| %>
183
215
  <tr>
184
216
  <td>
185
- <%= query["table"] %>.<%= query["column"] %>
217
+ <%= query[:table] %>.<%= query[:column] %>
186
218
  </td>
187
219
  <td>
188
- <%= query["column_type"] %>
220
+ <%= query[:column_type] %>
189
221
  </td>
190
222
  <td>
191
- <%= number_with_delimiter(query["max_value"].to_i - query["last_value"].to_i) %>
223
+ <%= number_with_delimiter(query[:max_value] - query[:last_value]) %>
192
224
  </td>
193
225
  <td>
194
- <%= number_to_percentage((query["max_value"].to_i - query["last_value"].to_i) * 100.0 / query["max_value"].to_i, precision: 2, significant: true) %>
226
+ <%= number_to_percentage((query[:max_value] - query[:last_value]) * 100.0 / query[:max_value], precision: 2, significant: true) %>
195
227
  </td>
196
228
  </tr>
197
229
  <% end %>
@@ -213,9 +245,9 @@
213
245
  </tr>
214
246
  </thead>
215
247
  <tbody>
216
- <% @invalid_indexes.each do |query| %>
248
+ <% @invalid_indexes.each do |index| %>
217
249
  <tr>
218
- <td><%= query["index"] %></td>
250
+ <td><%= index[:name] %></td>
219
251
  </tr>
220
252
  <% end %>
221
253
  </tbody>
@@ -239,10 +271,10 @@
239
271
  <pre>rails g migration remove_unneeded_indexes</pre>
240
272
  <p>And paste</p>
241
273
  <pre style="overflow: scroll; white-space: pre; word-break: normal;"><% @duplicate_indexes.each do |query| %>
242
- remove_index <%= query["unneeded_index"]["table"].to_sym.inspect %>, name: <%= query["unneeded_index"]["name"].to_s.inspect %><% end %></pre>
274
+ remove_index <%= query[:unneeded_index][:table].to_sym.inspect %>, name: <%= query[:unneeded_index][:name].to_s.inspect %><% end %></pre>
243
275
  </div>
244
276
 
245
- <table class="table">
277
+ <table class="table duplicate-indexes">
246
278
  <thead>
247
279
  <tr>
248
280
  <th>Details</th>
@@ -250,14 +282,14 @@ remove_index <%= query["unneeded_index"]["table"].to_sym.inspect %>, name: <%= q
250
282
  </thead>
251
283
  <tbody>
252
284
  <% @duplicate_indexes.each do |index| %>
253
- <% unneeded_index = index["unneeded_index"] %>
254
- <% covering_index = index["covering_index"] %>
285
+ <% unneeded_index = index[:unneeded_index] %>
286
+ <% covering_index = index[:covering_index] %>
255
287
  <tr>
256
- <td style="padding-top: 15px; padding-bottom: 5px;">
257
- On <%= unneeded_index["table"] %>
258
- <pre><%= unneeded_index["name"] %> (<%= unneeded_index["columns"].join(", ") %>)</pre>
288
+ <td>
289
+ On <%= unneeded_index[:table] %>
290
+ <pre><%= unneeded_index[:name] %> (<%= unneeded_index[:columns].join(", ") %>)</pre>
259
291
  is covered by
260
- <pre><%= covering_index["name"] %> (<%= covering_index["columns"].join(", ") %>)</pre>
292
+ <pre><%= covering_index[:name] %> (<%= covering_index[:columns].join(", ") %>)</pre>
261
293
  </td>
262
294
  </tr>
263
295
  <% end %>
@@ -343,7 +375,7 @@ pg_stat_statements.track = all</pre>
343
375
  <pre>rails g migration remove_unused_indexes</pre>
344
376
  <p>And paste</p>
345
377
  <pre style="overflow: scroll; white-space: pre; word-break: normal;"><% @unused_indexes.each do |query| %>
346
- remove_index <%= query["table"].to_sym.inspect %>, name: <%= query["index"].to_s.inspect %><% end %></pre>
378
+ remove_index <%= query[:table].to_sym.inspect %>, name: <%= query[:index].to_s.inspect %><% end %></pre>
347
379
  </div>
348
380
 
349
381
  <table class="table">
@@ -356,11 +388,15 @@ remove_index <%= query["table"].to_sym.inspect %>, name: <%= query["index"].to_s
356
388
  <tbody>
357
389
  <% @unused_indexes.each do |query| %>
358
390
  <tr>
359
- <td><%= query["index"] %><div class="text-muted">on <%= query["table"] %></div></td>
360
- <td><%= query["index_size"] %></td>
391
+ <td><%= query[:index] %><div class="text-muted">on <%= query[:table] %></div></td>
392
+ <td><%= query[:size] %></td>
361
393
  </tr>
362
394
  <% end %>
363
395
  </tbody>
364
396
  </table>
365
397
  </div>
366
398
  <% end %>
399
+
400
+ <script>
401
+ highlightQueries();
402
+ </script>
@@ -0,0 +1,69 @@
1
+ <div class="content">
2
+ <h1>Index Bloat</h1>
3
+
4
+ <% if @index_bloat.any? %>
5
+
6
+ <p>Indexes can become <%= link_to "bloated over time", "https://www.compose.com/articles/postgresql-bloat-origins-monitoring-and-managing/", target: "_blank" %>. Recreate them to remove bloat.</p>
7
+
8
+ <p>For each index, run:</p>
9
+ <pre><code>CREATE INDEX CONCURRENTLY new_index ...;
10
+ ANALYZE table;
11
+ DROP INDEX CONCURRENTLY index;
12
+ ANALYZE table;
13
+ ALTER INDEX new_index RENAME TO index;</code></pre>
14
+
15
+ <p>
16
+ <% if @show_sql %>
17
+ <%= link_to "Hide SQL", {} %>
18
+ <% else %>
19
+ <%= link_to "Show SQL", {sql: "t"} %>
20
+ <% end %>
21
+ for each index
22
+ </p>
23
+
24
+ <table class="table">
25
+ <thead>
26
+ <tr>
27
+ <th>Index</th>
28
+ <th style="width: 15%;">Bloat</th>
29
+ <th style="width: 15%;">Size</th>
30
+ </tr>
31
+ </thead>
32
+ <tbody>
33
+ <% @index_bloat.each do |index| %>
34
+ <tr>
35
+ <td>
36
+ <span style="word-break: break-all;"><%= index[:index] %></span>
37
+ </td>
38
+ <td><%= PgHero.pretty_size(index[:bloat_bytes]) %></td>
39
+ <td><%= PgHero.pretty_size(index[:index_bytes]) %></td>
40
+ </tr>
41
+ <% if @show_sql %>
42
+ <tr>
43
+ <td colspan="3" style="border-top: none; padding: 0;">
44
+ <% new_index = "new_#{index[:index]}".first(63) %>
45
+ <pre><code><%= index[:definition].sub(" INDEX ", " INDEX CONCURRENTLY \n ").sub(index[:index], new_index) %>;
46
+
47
+ ANALYZE <%= pghero_pretty_ident(index[:table], schema: index[:schema]) %>;
48
+
49
+ DROP INDEX CONCURRENTLY
50
+ <%= pghero_pretty_ident(index[:index]) %>;
51
+
52
+ ANALYZE <%= pghero_pretty_ident(index[:table], schema: index[:schema]) %>;
53
+
54
+ ALTER INDEX <%= pghero_pretty_ident(new_index) %>
55
+ RENAME TO <%= pghero_pretty_ident(index[:index]) %>;</code></pre>
56
+ </td>
57
+ </tr>
58
+ <% end %>
59
+ <% end %>
60
+ </tbody>
61
+ </table>
62
+ <% else %>
63
+ <p>No significant index bloat!</p>
64
+ <% end %>
65
+ </div>
66
+
67
+ <script>
68
+ highlightQueries();
69
+ </script>
@@ -13,23 +13,23 @@
13
13
  <% @maintenance_info.each do |table| %>
14
14
  <tr>
15
15
  <td>
16
- <%= table["table"] %>
17
- <% if table["schema"] != "public" %>
18
- <span class="text-muted"><%= table["schema"] %></span>
16
+ <%= table[:table] %>
17
+ <% if table[:schema] != "public" %>
18
+ <span class="text-muted"><%= table[:schema] %></span>
19
19
  <% end %>
20
20
  </td>
21
21
  <td>
22
- <% time = [table["last_autovacuum"], table["last_vacuum"]].compact.max %>
22
+ <% time = [table[:last_autovacuum], table[:last_vacuum]].compact.max %>
23
23
  <% if time %>
24
- <%= @time_zone.parse(time).strftime("%-m/%-e %l:%M %P") %>
24
+ <%= time.in_time_zone(@time_zone).strftime("%-m/%-e %l:%M %P") %>
25
25
  <% else %>
26
26
  <span class="text-muted">Unknown</span>
27
27
  <% end %>
28
28
  </td>
29
29
  <td>
30
- <% time = [table["last_autoanalyze"], table["last_analyze"]].compact.max %>
30
+ <% time = [table[:last_autoanalyze], table[:last_analyze]].compact.max %>
31
31
  <% if time %>
32
- <%= @time_zone.parse(time).strftime("%-m/%-e %l:%M %P") %>
32
+ <%= time.in_time_zone(@time_zone).strftime("%-m/%-e %l:%M %P") %>
33
33
  <% else %>
34
34
  <span class="text-muted">Unknown</span>
35
35
  <% end %>
@@ -7,6 +7,13 @@
7
7
 
8
8
  <% if @historical_query_stats_enabled %>
9
9
  <%= render partial: "query_stats_slider" %>
10
+ <% elsif @database.query_stats_table_exists? && (columns = @database.missing_query_stats_columns).any? %>
11
+ <div style="clear: both;">
12
+ <p>Add missing columns to re-enable historical query stats.</p>
13
+ <pre><code><% @database.missing_query_stats_columns.each do |column| %>ALTER TABLE pghero_query_stats ADD COLUMN "<%= column %>" <%= column == "query_hash" ? "bigint" : "text" %>;
14
+ <% end %></code></pre>
15
+ <p>Then restart the web server.</p>
16
+ </div>
10
17
  <% end %>
11
18
 
12
19
  <% if @query_stats_enabled %>
@@ -14,6 +21,9 @@
14
21
  <div class="alert alert-danger">Cannot understand start or end time.</div>
15
22
  <% elsif @query_stats.any? || @historical_query_stats_enabled %>
16
23
  <%= render partial: "queries_table", locals: {queries: @query_stats, sort_headers: true} %>
24
+ <script>
25
+ highlightQueries();
26
+ </script>
17
27
  <% else %>
18
28
  <p>Stats are not available yet. Come back soon!</p>
19
29
  <% end %>