pghero_fork 2.7.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (75) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +391 -0
  3. data/CONTRIBUTING.md +42 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +3 -0
  6. data/app/assets/images/pghero/favicon.png +0 -0
  7. data/app/assets/javascripts/pghero/Chart.bundle.js +20755 -0
  8. data/app/assets/javascripts/pghero/application.js +158 -0
  9. data/app/assets/javascripts/pghero/chartkick.js +2436 -0
  10. data/app/assets/javascripts/pghero/highlight.pack.js +2 -0
  11. data/app/assets/javascripts/pghero/jquery.js +10872 -0
  12. data/app/assets/javascripts/pghero/nouislider.js +2672 -0
  13. data/app/assets/stylesheets/pghero/application.css +514 -0
  14. data/app/assets/stylesheets/pghero/arduino-light.css +86 -0
  15. data/app/assets/stylesheets/pghero/nouislider.css +310 -0
  16. data/app/controllers/pg_hero/home_controller.rb +449 -0
  17. data/app/helpers/pg_hero/home_helper.rb +30 -0
  18. data/app/views/layouts/pg_hero/application.html.erb +68 -0
  19. data/app/views/pg_hero/home/_connections_table.html.erb +16 -0
  20. data/app/views/pg_hero/home/_live_queries_table.html.erb +51 -0
  21. data/app/views/pg_hero/home/_queries_table.html.erb +72 -0
  22. data/app/views/pg_hero/home/_query_stats_slider.html.erb +16 -0
  23. data/app/views/pg_hero/home/_suggested_index.html.erb +18 -0
  24. data/app/views/pg_hero/home/connections.html.erb +32 -0
  25. data/app/views/pg_hero/home/explain.html.erb +27 -0
  26. data/app/views/pg_hero/home/index.html.erb +518 -0
  27. data/app/views/pg_hero/home/index_bloat.html.erb +72 -0
  28. data/app/views/pg_hero/home/live_queries.html.erb +11 -0
  29. data/app/views/pg_hero/home/maintenance.html.erb +55 -0
  30. data/app/views/pg_hero/home/queries.html.erb +33 -0
  31. data/app/views/pg_hero/home/relation_space.html.erb +14 -0
  32. data/app/views/pg_hero/home/show_query.html.erb +106 -0
  33. data/app/views/pg_hero/home/space.html.erb +83 -0
  34. data/app/views/pg_hero/home/system.html.erb +34 -0
  35. data/app/views/pg_hero/home/tune.html.erb +53 -0
  36. data/config/routes.rb +32 -0
  37. data/lib/generators/pghero/config_generator.rb +13 -0
  38. data/lib/generators/pghero/query_stats_generator.rb +18 -0
  39. data/lib/generators/pghero/space_stats_generator.rb +18 -0
  40. data/lib/generators/pghero/templates/config.yml.tt +46 -0
  41. data/lib/generators/pghero/templates/query_stats.rb.tt +15 -0
  42. data/lib/generators/pghero/templates/space_stats.rb.tt +13 -0
  43. data/lib/pghero.rb +246 -0
  44. data/lib/pghero/connection.rb +5 -0
  45. data/lib/pghero/database.rb +175 -0
  46. data/lib/pghero/engine.rb +16 -0
  47. data/lib/pghero/methods/basic.rb +160 -0
  48. data/lib/pghero/methods/connections.rb +77 -0
  49. data/lib/pghero/methods/constraints.rb +30 -0
  50. data/lib/pghero/methods/explain.rb +29 -0
  51. data/lib/pghero/methods/indexes.rb +332 -0
  52. data/lib/pghero/methods/kill.rb +28 -0
  53. data/lib/pghero/methods/maintenance.rb +93 -0
  54. data/lib/pghero/methods/queries.rb +75 -0
  55. data/lib/pghero/methods/query_stats.rb +349 -0
  56. data/lib/pghero/methods/replication.rb +74 -0
  57. data/lib/pghero/methods/sequences.rb +124 -0
  58. data/lib/pghero/methods/settings.rb +37 -0
  59. data/lib/pghero/methods/space.rb +141 -0
  60. data/lib/pghero/methods/suggested_indexes.rb +329 -0
  61. data/lib/pghero/methods/system.rb +287 -0
  62. data/lib/pghero/methods/tables.rb +68 -0
  63. data/lib/pghero/methods/users.rb +87 -0
  64. data/lib/pghero/query_stats.rb +5 -0
  65. data/lib/pghero/space_stats.rb +5 -0
  66. data/lib/pghero/stats.rb +6 -0
  67. data/lib/pghero/version.rb +3 -0
  68. data/lib/tasks/pghero.rake +27 -0
  69. data/licenses/LICENSE-chart.js.txt +9 -0
  70. data/licenses/LICENSE-chartkick.js.txt +22 -0
  71. data/licenses/LICENSE-highlight.js.txt +29 -0
  72. data/licenses/LICENSE-jquery.txt +20 -0
  73. data/licenses/LICENSE-moment.txt +22 -0
  74. data/licenses/LICENSE-nouislider.txt +21 -0
  75. metadata +130 -0
@@ -0,0 +1,30 @@
1
+ module PgHero
2
+ module HomeHelper
3
+ def pghero_pretty_ident(table, schema: nil)
4
+ ident = table
5
+ if schema && schema != "public"
6
+ ident = "#{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
+
15
+ def pghero_js_var(name, value)
16
+ "var #{name} = #{json_escape(value.to_json(root: false))};".html_safe
17
+ end
18
+
19
+ def pghero_remove_index(query)
20
+ if query[:columns]
21
+ columns = query[:columns].map(&:to_sym)
22
+ columns = columns.first if columns.size == 1
23
+ end
24
+ ret = String.new("remove_index #{query[:table].to_sym.inspect}")
25
+ ret << ", name: #{(query[:name] || query[:index]).to_s.inspect}"
26
+ ret << ", column: #{columns.inspect}" if columns
27
+ ret
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,68 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title><%= [@databases.size > 1 ? @database.name : "PgHero", @title].compact.join(" / ") %></title>
5
+
6
+ <meta charset="utf-8" />
7
+ <link rel="shortcut icon" type="image/x-icon" href="<%= root_path %>favicon.png">
8
+ <link rel="stylesheet" media="screen" href="<%= root_path %>application.css">
9
+ <script type="text/javascript" src="<%= root_path %>application.js"></script>
10
+ </head>
11
+ <body>
12
+ <div class="container">
13
+ <div class="alert alert-<%= alert ? "danger" : "info" %>">
14
+ <% if alert %>
15
+ <%= alert %>
16
+ <% elsif notice %>
17
+ <%= notice %>
18
+ <% elsif Rails.env.development? %>
19
+ Do not use development information to make decisions about your production environment
20
+ <% else %>
21
+ <%= link_to "PgHero", root_path %>
22
+ <% end %>
23
+ </div>
24
+
25
+ <div class="grid">
26
+ <div class="col-3-12">
27
+ <% if @databases.size > 1 %>
28
+ <p class="nav-header"><%= @database.name %></p>
29
+ <% end %>
30
+
31
+ <ul class="nav">
32
+ <!-- poor man's active_link_to -->
33
+ <li class="<%= controller.action_name == "index" ? "active" : "" %>"><%= link_to "Overview", root_path %></li>
34
+ <% if @system_stats_enabled %>
35
+ <li class="<%= controller.action_name == "system" ? "active" : "" %>"><%= link_to "System", system_path %></li>
36
+ <% end %>
37
+ <% if @query_stats_enabled %>
38
+ <li class="<%= controller.action_name == "queries" ? "active" : "" %>"><%= link_to "Queries", queries_path %></li>
39
+ <% end %>
40
+ <li class="<%= controller.action_name == "space" ? "active" : "" %>"><%= link_to "Space", space_path %></li>
41
+ <li class="<%= controller.action_name == "connections" ? "active" : "" %>"><%= link_to "Connections", connections_path %></li>
42
+ <li class="<%= controller.action_name == "live_queries" ? "active" : "" %>"><%= link_to "Live Queries", live_queries_path %></li>
43
+ <% unless @database.replica? %>
44
+ <li class="<%= controller.action_name == "maintenance" ? "active" : "" %>"><%= link_to "Maintenance", maintenance_path %></li>
45
+ <% end %>
46
+ <li class="<%= controller.action_name == "explain" ? "active" : "" %>"><%= link_to "Explain", explain_path %></li>
47
+ <li class="<%= controller.action_name == "tune" ? "active" : "" %>"><%= link_to "Tune", tune_path %></li>
48
+ </ul>
49
+
50
+ <% if @databases.size > 1 %>
51
+ <p class="nav-header">Databases</p>
52
+ <ul class="nav">
53
+ <% @databases.each do |database| %>
54
+ <li class="<%= ("active-database" if @database.id == database.id) %>">
55
+ <%= link_to database.name, action_name == "show_query" ? root_path(database: database.id) : {database: database.id} %>
56
+ </li>
57
+ <% end %>
58
+ </ul>
59
+ <% end %>
60
+ </div>
61
+
62
+ <div class="col-9-12">
63
+ <%= yield %>
64
+ </div>
65
+ </div>
66
+ </div>
67
+ </body>
68
+ </html>
@@ -0,0 +1,16 @@
1
+ <table class="table">
2
+ <thead>
3
+ <tr>
4
+ <th>Top Sources</th>
5
+ <th style="width: 20%;">Connections</th>
6
+ </tr>
7
+ </thead>
8
+ <tbody>
9
+ <% connection_sources.each do |source| %>
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>
13
+ </tr>
14
+ <% end %>
15
+ </tbody>
16
+ </table>
@@ -0,0 +1,51 @@
1
+ <table class="table queries">
2
+ <thead>
3
+ <tr>
4
+ <th style="width: 25%;">Pid</th>
5
+ <th style="width: 25%;">Duration</th>
6
+ <th style="width: 25%;">State</th>
7
+ <th style="width: 25%;"></th>
8
+ </tr>
9
+ </thead>
10
+ <tbody>
11
+ <% queries.reverse.each do |query| %>
12
+ <tr>
13
+ <td><%= query[:pid] %></td>
14
+ <td>
15
+ <% sec = query[:duration_ms] / 1000.0 %>
16
+ <% if sec < 1.minute %>
17
+ <%= sec.round(1) %> s
18
+ <% elsif sec < 1.day %>
19
+ <%= Time.at(sec).utc.strftime("%H:%M:%S") %>
20
+ <% else %>
21
+ <% days = (sec / 1.day).floor %>
22
+ <%= days %>d <%= Time.at(sec - days.days).utc.strftime("%H:%M:%S") %>
23
+ <% end %>
24
+ </td>
25
+ <td>
26
+ <%= query[:state] %>
27
+ <% if vacuum_progress[query[:pid]] %>
28
+ <br />
29
+ <strong><%= vacuum_progress[query[:pid]][:phase] %></strong>
30
+ <% end %>
31
+ </td>
32
+ <td class="text-right">
33
+ <% unless @database.filter_data %>
34
+ <%= button_to "Explain", explain_path, params: {query: query[:query]}, form: {target: "_blank"}, class: "btn btn-info" %>
35
+ <% end %>
36
+ <%= button_to "Kill", kill_path(pid: query[:pid]), class: "btn btn-danger" %>
37
+ </td>
38
+ </tr>
39
+ <tr>
40
+ <td colspan="6" style="border-top: none; padding: 0;">
41
+ <%= query[:source] %> <span class="text-muted"><%= query[:user] %></span>
42
+ <pre style="margin-top: 1em;"><code><%= query[:query] %></code></pre>
43
+ </td>
44
+ </tr>
45
+ <% end %>
46
+ </tbody>
47
+ </table>
48
+
49
+ <script>
50
+ highlightQueries();
51
+ </script>
@@ -0,0 +1,72 @@
1
+ <table class="table queries-table">
2
+ <% unless local_assigns[:xhr] %>
3
+ <thead>
4
+ <tr>
5
+ <th style="width: 33.33%;"><%= local_assigns[:sort_headers] ? link_to("Total Time", {sort: nil}, data: {sort: nil}) : "Total Time" %></th>
6
+ <th style="width: 33.33%;"><%= local_assigns[:sort_headers] ? link_to("Average Time", {sort: "average_time"}, data: {sort: "average_time"}) : "Average Time" %></th>
7
+ <th style="width: 33.33%;"><%= local_assigns[:sort_headers] ? link_to("Calls", {sort: "calls"}, data: {sort: "calls"}) : "Calls" %></th>
8
+ </tr>
9
+ </thead>
10
+ <% end %>
11
+ <tbody id="queries">
12
+ <% if queries.empty? %>
13
+ <tr>
14
+ <td colspan="3">
15
+ <p class="queries-info text-muted">
16
+ <% if local_assigns[:xhr] %>
17
+ No data available for this time.
18
+ <% else %>
19
+ ...
20
+ <% end %>
21
+ </p>
22
+ </td>
23
+ </tr>
24
+ <% end %>
25
+ <% queries.each do |query| %>
26
+ <tr>
27
+ <td>
28
+ <%= number_with_delimiter(query[:total_minutes].round) %> min
29
+ <span class="percent">
30
+ <% percent = query[:total_percent] %>
31
+ <% if percent > 1 %>
32
+ <%= percent.round %>%
33
+ <% elsif percent > 0.1 %>
34
+ <%= percent.round(1) %>%
35
+ <% else %>
36
+ &lt; 0.1%
37
+ <% end %>
38
+ </span>
39
+ </td>
40
+ <td>
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 && query[:query_hash] %>
54
+ <%= link_to "details", show_query_path(query[:query_hash], user: query[:user]), target: "_blank" %>
55
+ <% end %>
56
+ </span>
57
+ </td>
58
+ </tr>
59
+ <tr>
60
+ <td colspan="3" style="border-top: none; padding: 0;">
61
+ <pre><code style="max-height: 230px; overflow: hidden;" onclick="this.style.maxHeight = 'none';"><%= query[:query] %></code></pre>
62
+ <% if query[:query] == "<insufficient privilege>" %>
63
+ <p class="text-muted">For security reasons, only superusers can see queries executed by other users.</p>
64
+ <% end %>
65
+ <% if local_assigns[:suggested_indexes] != false && (details = @suggested_indexes_by_query[query[:query]]) && (details[:index] || @debug) %>
66
+ <%= render partial: "suggested_index", locals: {index: details[:index], details: details} %>
67
+ <% end %>
68
+ </td>
69
+ </tr>
70
+ <% end %>
71
+ </tbody>
72
+ </table>
@@ -0,0 +1,16 @@
1
+ <div id="slider-container">
2
+ <div id="slider"></div>
3
+ <div id="range-end"></div>
4
+ <div id="range-start"></div>
5
+ </div>
6
+
7
+ <script>
8
+ <%= pghero_js_var("sort", @sort) %>
9
+ <%= pghero_js_var("minAverageTime", @min_average_time) %>
10
+ <%= pghero_js_var("minCalls", @min_calls) %>
11
+ <%= pghero_js_var("debug", @debug) %>
12
+ <%= pghero_js_var("startAt", params[:start_at] ? @start_at.to_i * 1000 : nil) %>
13
+ <%= pghero_js_var("endAt", @end_at.to_i * 1000) %>
14
+
15
+ initSlider();
16
+ </script>
@@ -0,0 +1,18 @@
1
+ <% if index && !details[:covering_index] %>
2
+ <% unless @debug %>
3
+ <div style="float: right; color: #f0ad4e; margin-top: 0px; padding: 10px; cursor: pointer;" onclick="document.getElementById('details-<%= index.object_id %>').style.display = 'block'; this.style.display = 'none';">Details</div>
4
+ <% end %>
5
+ <code><pre style="color: #eee; background-color: #333;">CREATE INDEX CONCURRENTLY ON <%= index[:table] %><% if index[:using] %> USING <%= index[:using] %><% end %> (<%= index[:columns].join(", ") %>)</pre></code>
6
+ <% end %>
7
+ <div id="details-<%= index.object_id %>" style="<%= "display: none;" unless @debug %>">
8
+ <code><pre style="color: #f0ad4e; background-color: #333;"><% if details[:explanation] %><%= details[:explanation] %>
9
+ <% end %><% if details[:row_estimates] %>Rows: <%= details[:rows] %>
10
+ Row progression: <%= details[:row_progression].to_a.join(", ") %>
11
+
12
+ Row estimates
13
+ <%= details[:row_estimates].to_a.map { |k, v| "- #{k}: #{v}" }.join("\n") %><% end %><% if details[:table_indexes] %>
14
+
15
+ Existing indexes
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
+ <% end %><% end %></pre></code>
18
+ </div>
@@ -0,0 +1,32 @@
1
+ <div class="content">
2
+ <h1>Connections</h1>
3
+
4
+ <p><%= pluralize(@total_connections, "connection") %></p>
5
+
6
+ <% if @total_connections > 0 %>
7
+ <h3>By Database</h3>
8
+
9
+ <div id="chart-1" class="chart" style="height: 260px; line-height: 260px; margin-bottom: 20px;">Loading...</div>
10
+ <script>
11
+ new Chartkick.PieChart("chart-1", <%= json_escape(@connections_by_database.to_json).html_safe %>);
12
+ </script>
13
+
14
+ <h3>By User</h3>
15
+
16
+ <div id="chart-2" class="chart" style="height: 260px; line-height: 260px; margin-bottom: 20px;">Loading...</div>
17
+ <script>
18
+ new Chartkick.PieChart("chart-2", <%= json_escape(@connections_by_user.to_json).html_safe %>);
19
+ </script>
20
+
21
+ <% if @connections_by_ssl_status %>
22
+ <h3>By Security</h3>
23
+
24
+ <div id="chart-3" class="chart" style="height: 260px; line-height: 260px; margin-bottom: 20px;">Loading...</div>
25
+ <script>
26
+ new Chartkick.PieChart("chart-3", <%= json_escape(@connections_by_ssl_status.to_json).html_safe %>);
27
+ </script>
28
+ <% end %>
29
+
30
+ <%= render partial: "connections_table", locals: {connection_sources: @connection_sources} %>
31
+ <% end %>
32
+ </div>
@@ -0,0 +1,27 @@
1
+ <div class="content">
2
+ <h1>Explain</h1>
3
+
4
+ <%= form_tag explain_path do %>
5
+ <div class="field"><%= text_area_tag :query, @query, placeholder: "Enter a SQL query" %></div>
6
+ <p>
7
+ <%= submit_tag "Explain", class: "btn btn-info", style: "margin-right: 10px;" %>
8
+ <%= submit_tag "Analyze", class: "btn btn-danger", style: "margin-right: 10px;" %>
9
+ <%= submit_tag "Visualize", class: "btn btn-danger" %>
10
+ </p>
11
+ <% end %>
12
+
13
+ <% if @explanation %>
14
+ <% if @visualize %>
15
+ <p>Paste the output below into the <%= link_to "Postgres Explain Visualizer", "https://tatiyants.com/pev/#/plans/new", target: "_blank" %></p>
16
+ <% end %>
17
+ <pre><code><%= @explanation %></code></pre>
18
+ <% unless @visualize %>
19
+ <p><%= link_to "See how to interpret this", "https://www.postgresql.org/docs/current/static/using-explain.html", target: "_blank" %></p>
20
+ <% end %>
21
+ <% if (index = @suggested_index) %>
22
+ <%= render partial: "suggested_index", locals: {index: index, details: index[:details]} %>
23
+ <% end %>
24
+ <% elsif @error %>
25
+ <div class="alert alert-danger"><%= @error %></div>
26
+ <% end %>
27
+ </div>
@@ -0,0 +1,518 @@
1
+ <div id="status">
2
+ <% if @replica %>
3
+ <div class="alert alert-<%= @good_replication_lag ? "success" : "warning" %>">
4
+ <% if @replication_lag %>
5
+ <% if @good_replication_lag %>
6
+ Healthy replication lag
7
+ <% else %>
8
+ High replication lag
9
+ <% end %>
10
+ <span class="tiny"><%= number_with_delimiter((@replication_lag * 1000).round) %> ms</span>
11
+ <% else %>
12
+ Replication lag not supported
13
+ <% end %>
14
+ </div>
15
+ <% elsif @inactive_replication_slots.any? %>
16
+ <div class="alert alert-warning">
17
+ <%= pluralize(@inactive_replication_slots.size, "inactive replication slot") %>
18
+ </div>
19
+ <% end %>
20
+ <div class="alert alert-<%= @long_running_queries.empty? ? "success" : "warning" %>">
21
+ <% if @long_running_queries.any? %>
22
+ <%= pluralize(@long_running_queries.size, "long running query") %>
23
+ <% else %>
24
+ No long running queries
25
+ <% end %>
26
+ <% if @autovacuum_queries.any? %>
27
+ <span class="tiny"><%= @autovacuum_queries.size %> autovacuum</span>
28
+ <% end %>
29
+ </div>
30
+ <% if @extended %>
31
+ <div class="alert alert-<%= @good_cache_rate ? "success" : "warning" %>">
32
+ <% if @good_cache_rate %>
33
+ Cache hit rate above <%= @database.cache_hit_rate_threshold %>%
34
+ <% else %>
35
+ Low cache hit rate
36
+ <% end %>
37
+ </div>
38
+ <% end %>
39
+ <div class="alert alert-<%= @good_total_connections && @good_idle_connections ? "success" : "warning" %>">
40
+ <% if !@good_total_connections %>
41
+ High number of connections <span class="tiny"><%= @total_connections %></span>
42
+ <% elsif !@good_idle_connections %>
43
+ High number of connections idle in transaction <span class="tiny"><%= @idle_connections %></span>
44
+ <% else %>
45
+ Connections healthy <span class="tiny"><%= @total_connections %></span>
46
+ <% end %>
47
+ </div>
48
+ <div class="alert alert-<%= @transaction_id_danger.empty? ? "success" : "warning" %>">
49
+ <% if @transaction_id_danger.any? %>
50
+ <%= pluralize(@transaction_id_danger.size, "table") %> not vacuuming properly
51
+ <% else %>
52
+ Vacuuming healthy
53
+ <% end %>
54
+ </div>
55
+ <div class="alert alert-<%= @sequence_danger && @sequence_danger.empty? ? "success" : "warning" %>">
56
+ <% if @sequence_danger.any? %>
57
+ <%= pluralize(@sequence_danger.size, "column") %> approaching overflow
58
+ <% else %>
59
+ No columns near integer overflow
60
+ <% end %>
61
+ <% if @unreadable_sequences.any? %>
62
+ (<%= link_to pluralize(@unreadable_sequences.size, "unreadable sequence", "unreadable sequences"), {unreadable: "t"} %>)
63
+ <% end %>
64
+ </div>
65
+ <div class="alert alert-<%= @invalid_indexes.empty? && @invalid_constraints.empty? ? "success" : "warning" %>">
66
+ <% if @invalid_indexes.empty? && @invalid_constraints.empty? %>
67
+ No invalid indexes or constraints
68
+ <% else %>
69
+ <% if @invalid_indexes.any? %>
70
+ <%= pluralize(@invalid_indexes.size, "invalid index", "invalid indexes") %>
71
+ <% end %>
72
+ <% if @invalid_constraints.any? %>
73
+ <% if @invalid_indexes.any? %>and<% end %>
74
+ <%= pluralize(@invalid_constraints.size, "invalid constraint", "invalid constraints") %>
75
+ <% end %>
76
+ <% end %>
77
+ </div>
78
+ <% if @duplicate_indexes %>
79
+ <div class="alert alert-<%= @duplicate_indexes.empty? ? "success" : "warning" %>">
80
+ <% if @duplicate_indexes.any? %>
81
+ <%= pluralize(@duplicate_indexes.size, "duplicate index", "duplicate indexes") %>
82
+ <% else %>
83
+ No duplicate indexes
84
+ <% end %>
85
+ </div>
86
+ <% end %>
87
+ <% if @database.suggested_indexes_enabled? %>
88
+ <div class="alert alert-<%= @suggested_indexes.empty? ? "success" : "warning" %>">
89
+ <% if @suggested_indexes.any? %>
90
+ <%= pluralize(@suggested_indexes.size, "suggested index", "suggested indexes") %>
91
+ <% else %>
92
+ No suggested indexes
93
+ <% end %>
94
+ </div>
95
+ <% end %>
96
+ <div class="alert alert-<%= @query_stats_enabled && @slow_queries.empty? ? "success" : "warning" %>">
97
+ <% if !@query_stats_enabled %>
98
+ Query stats must be enabled for slow queries
99
+ <% elsif @slow_queries.any? %>
100
+ <%= pluralize(@slow_queries.size, "slow query") %>
101
+ <% else %>
102
+ No slow queries
103
+ <% end %>
104
+ </div>
105
+ <% if @extended %>
106
+ <div class="alert alert-<%= @unused_indexes.empty? ? "success" : "warning" %>">
107
+ <% if @unused_indexes.any? %>
108
+ <%= pluralize(@unused_indexes.size, "unused index", "unused indexes") %>
109
+ <% else %>
110
+ No unused indexes
111
+ <% end %>
112
+ </div>
113
+ <% end %>
114
+ </div>
115
+
116
+ <% if params[:unreadable] && @unreadable_sequences.any? %>
117
+ <div class="content">
118
+ <h1>Unreadable Sequences</h1>
119
+
120
+ <p>This is likely due to missing privileges. Make sure your user has the SELECT privilege for each sequence.</p>
121
+
122
+ <table class="table">
123
+ <thead>
124
+ <tr>
125
+ <th style="width: 33%;">Column</th>
126
+ <th>Sequence</th>
127
+ </tr>
128
+ </thead>
129
+ <tbody>
130
+ <% @unreadable_sequences.each do |sequence| %>
131
+ <tr>
132
+ <td>
133
+ <%= sequence[:table] %>.<%= sequence[:column] %>
134
+ <% if sequence[:table_schema] != "public" %>
135
+ <span class="text-muted"><%= sequence[:table_schema] %></span>
136
+ <% end %>
137
+ </td>
138
+ <td>
139
+ <% if sequence[:sequence] %>
140
+ <%= sequence[:sequence] %>
141
+ <% if sequence[:schema] && sequence[:schema] != "public" %>
142
+ <span class="text-muted"><%= sequence[:schema] %></span>
143
+ <% end %>
144
+ <% else %>
145
+ Unable to parse: <%= sequence[:default_value] %>
146
+ <% end %>
147
+ </td>
148
+ </tr>
149
+ <% end %>
150
+ </tbody>
151
+ </table>
152
+ </div>
153
+ <% end %>
154
+
155
+ <% if @replica && !@good_replication_lag %>
156
+ <div class="content">
157
+ <h1>High Replication Lag</h1>
158
+
159
+ <p><%= pluralize(@replication_lag.round, "second") %></p>
160
+ </div>
161
+ <% end %>
162
+
163
+ <% if @inactive_replication_slots && @inactive_replication_slots.any? %>
164
+ <div class="content">
165
+ <h1>Inactive Replication Slots</h1>
166
+ <p>Inactive replication slots can cause a lot of disk space to be consumed.</p>
167
+ <p>For each, run:</p>
168
+ <pre><code>SELECT pg_drop_replication_slot('slot_name');</code></pre>
169
+ <table class="table">
170
+ <thead>
171
+ <tr>
172
+ <th>Name</th>
173
+ </tr>
174
+ </thead>
175
+ <tbody>
176
+ <% @inactive_replication_slots.each do |slot| %>
177
+ <tr>
178
+ <td><%= slot[:slot_name] %></td>
179
+ </tr>
180
+ <% end %>
181
+ </tbody>
182
+ </table>
183
+ </div>
184
+ <% end %>
185
+
186
+ <% if @long_running_queries.any? %>
187
+ <div class="content">
188
+ <%= button_to "Kill All", kill_long_running_queries_path, class: "btn btn-danger", style: "float: right;" %>
189
+ <h1>Long Running Queries</h1>
190
+
191
+ <p>We recommend setting a statement timeout on all non-superusers with:</p>
192
+
193
+ <pre><code>ALTER ROLE &lt;user&gt; SET statement_timeout TO '60s';</code></pre>
194
+
195
+ <%= render partial: "live_queries_table", locals: {queries: @long_running_queries, vacuum_progress: {}} %>
196
+ </div>
197
+ <% end %>
198
+
199
+ <% if @extended && !@good_cache_rate %>
200
+ <div class="content">
201
+ <h1>Low Cache Hit Rate</h1>
202
+
203
+ <p>
204
+ Index Hit Rate: <%= (@index_hit_rate * 100).round(1) %>%
205
+ <br />
206
+ Table Hit Rate: <%= (@table_hit_rate * 100).round(1) %>%
207
+ </p>
208
+
209
+ <p>
210
+ The cache hit rate <%= link_to "should be above 99%", "https://devcenter.heroku.com/articles/understanding-postgres-data-caching", target: "_blank" %> in most cases. You can often increase this by adding more memory.
211
+ <!-- TODO better suggestions -->
212
+ </p>
213
+ </div>
214
+ <% end %>
215
+
216
+ <% if !@good_total_connections %>
217
+ <div class="content">
218
+ <h1>High Number of Connections</h1>
219
+ <p><%= pluralize(@total_connections, "connection") %></p>
220
+
221
+ <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>
222
+
223
+ <%= render partial: "connections_table", locals: {connection_sources: @database.connection_sources.first(10)} %>
224
+ </div>
225
+ <% end %>
226
+
227
+ <% if !@good_idle_connections %>
228
+ <div class="content">
229
+ <h1>High Number of Connections Idle in Transaction</h1>
230
+ <p><%= pluralize(@idle_connections, "connection") %> idle in transaction</p>
231
+ <p>Avoid opening transactions and doing work outside the database.</p>
232
+ <p><%= link_to "View queries", live_queries_path(state: "idle in transaction") %></p>
233
+ </div>
234
+ <% end %>
235
+
236
+ <% if @transaction_id_danger.any? %>
237
+ <div class="content">
238
+ <h2>Vacuuming Needed</h2>
239
+ <p>The database <strong>will shutdown</strong> when there are fewer than 1,000,000 transactions left. <%= link_to "Read more", "https://www.postgresql.org/docs/current/static/routine-vacuuming.html#VACUUM-FOR-WRAPAROUND", target: "_blank" %>.</p>
240
+
241
+ <p>Try <%= link_to "tuning autovacuum", "https://blog.2ndquadrant.com/autovacuum-tuning-basics/", target: "_blank" %> - specifically autovacuum_vacuum_cost_limit.</p>
242
+
243
+ <p>If that doesn’t work, for each table, run:</p>
244
+ <pre><code>VACUUM FREEZE VERBOSE table;</code></pre>
245
+ <table class="table">
246
+ <thead>
247
+ <tr>
248
+ <th>Table</th>
249
+ <th style="width: 20%;">Transactions Left</th>
250
+ </tr>
251
+ </thead>
252
+ <tbody>
253
+ <% @transaction_id_danger.each do |query| %>
254
+ <tr>
255
+ <td>
256
+ <%= query[:table] %>
257
+ <% if query[:schema] != "public" %>
258
+ <span class="text-muted"><%= query[:schema] %></span>
259
+ <% end %>
260
+ </td>
261
+ <td><%= number_with_delimiter(query[:transactions_left]) %></td>
262
+ </tr>
263
+ <% end %>
264
+ </tbody>
265
+ </table>
266
+ </div>
267
+ <% end %>
268
+
269
+ <% if @sequence_danger && @sequence_danger.any? %>
270
+ <div class="content">
271
+ <h2>Columns Near Overflow</h2>
272
+ <p>Consider changing columns to bigint to support a larger range of values.</p>
273
+ <table class="table">
274
+ <thead>
275
+ <tr>
276
+ <th>Column</th>
277
+ <th style="width: 20%;">Type</th>
278
+ <th style="width: 20%;">Values Left</th>
279
+ <th style="width: 10%;">% Left</th>
280
+ </tr>
281
+ </thead>
282
+ <tbody>
283
+ <% @sequence_danger.sort_by { |s| [s[:table], s[:column]] }.each do |query| %>
284
+ <tr>
285
+ <td>
286
+ <%= query[:table] %>.<%= query[:column] %>
287
+ <% if query[:table_schema] != "public" %>
288
+ <span class="text-muted"><%= query[:table_schema] %></span>
289
+ <% end %>
290
+ </td>
291
+ <td>
292
+ <%= query[:column_type] %>
293
+ </td>
294
+ <td>
295
+ <%= number_with_delimiter(query[:max_value] - query[:last_value]) %>
296
+ </td>
297
+ <td>
298
+ <%= number_to_percentage((query[:max_value] - query[:last_value]) * 100.0 / query[:max_value], precision: 2, significant: true) %>
299
+ </td>
300
+ </tr>
301
+ <% end %>
302
+ </tbody>
303
+ </table>
304
+ </div>
305
+ <% end %>
306
+
307
+ <% if @invalid_indexes.any? %>
308
+ <div class="content">
309
+ <h1>Invalid Indexes</h1>
310
+
311
+ <p>These indexes exist, but can’t be used. You should recreate them.</p>
312
+
313
+ <table class="table">
314
+ <thead>
315
+ <tr>
316
+ <th>Name</th>
317
+ </tr>
318
+ </thead>
319
+ <tbody>
320
+ <% @invalid_indexes.each do |index| %>
321
+ <tr>
322
+ <td>
323
+ <%= index[:name] %>
324
+ <% if index[:schema] != "public" %>
325
+ <span class="text-muted"><%= index[:schema] %></span>
326
+ <% end %>
327
+ </td>
328
+ </tr>
329
+ <tr>
330
+ <td style="border-top: none; padding: 0;">
331
+ <pre><code>DROP INDEX CONCURRENTLY <%= pghero_pretty_ident(index[:name], schema: index[:schema]) %>;
332
+ <%= index[:definition].sub("CREATE INDEX ", "CREATE INDEX CONCURRENTLY ") %>;</code></pre>
333
+ </td>
334
+ </tr>
335
+ <% end %>
336
+ </tbody>
337
+ </table>
338
+ </div>
339
+ <% end %>
340
+
341
+ <% if @invalid_constraints.any? %>
342
+ <div class="content">
343
+ <h1>Invalid Constraints</h1>
344
+
345
+ <p>These constraints are marked as <code>NOT VALID</code>. You should validate them.</p>
346
+
347
+ <table class="table">
348
+ <thead>
349
+ <tr>
350
+ <th>Name</th>
351
+ </tr>
352
+ </thead>
353
+ <tbody>
354
+ <% @invalid_constraints.each do |constraint| %>
355
+ <tr>
356
+ <td>
357
+ <%= constraint[:name] %>
358
+ <% if constraint[:schema] != "public" %>
359
+ <span class="text-muted"><%= constraint[:schema] %></span>
360
+ <% end %>
361
+ </td>
362
+ </tr>
363
+ <tr>
364
+ <td style="border-top: none; padding: 0;">
365
+ <pre><code>ALTER TABLE <%= pghero_pretty_ident(constraint[:table], schema: constraint[:schema]) %> VALIDATE CONSTRAINT <%= pghero_pretty_ident(constraint[:name]) %>;</code></pre>
366
+ </td>
367
+ </tr>
368
+ <% end %>
369
+ </tbody>
370
+ </table>
371
+ </div>
372
+ <% end %>
373
+
374
+ <% if @duplicate_indexes && @duplicate_indexes.any? %>
375
+ <div class="content">
376
+ <h1>Duplicate Indexes</h1>
377
+
378
+ <p>
379
+ These indexes exist, but aren’t needed. Remove them
380
+ <% if @show_migrations %>
381
+ <a href="javascript: void(0);" onclick="document.getElementById('migration2').style.display = 'block';">with a migration</a>
382
+ <% end %>
383
+ for faster writes.
384
+ </p>
385
+
386
+ <div id="migration2" style="display: none;">
387
+ <pre>rails generate migration remove_unneeded_indexes</pre>
388
+ <p>And paste</p>
389
+ <pre style="overflow: scroll; white-space: pre; word-break: normal;"><% @duplicate_indexes.each do |query| %>
390
+ <%= pghero_remove_index(query[:unneeded_index]) %><% end %></pre>
391
+ </div>
392
+
393
+ <table class="table duplicate-indexes">
394
+ <thead>
395
+ <tr>
396
+ <th>Details</th>
397
+ </tr>
398
+ </thead>
399
+ <tbody>
400
+ <% @duplicate_indexes.each do |index| %>
401
+ <% unneeded_index = index[:unneeded_index] %>
402
+ <% covering_index = index[:covering_index] %>
403
+ <tr>
404
+ <td>
405
+ On <%= unneeded_index[:table] %>
406
+ <pre><%= unneeded_index[:name] %> (<%= unneeded_index[:columns].join(", ") %>)</pre>
407
+ is covered by
408
+ <pre><%= covering_index[:name] %> (<%= covering_index[:columns].join(", ") %>)</pre>
409
+ </td>
410
+ </tr>
411
+ <% end %>
412
+ </tbody>
413
+ </table>
414
+ </div>
415
+ <% end %>
416
+
417
+ <% if @suggested_indexes.any? %>
418
+ <div class="content">
419
+ <h1>Suggested Indexes</h1>
420
+ <p>
421
+ Add indexes to speed up queries.
422
+ <% if @show_migrations %>
423
+ Here’s a
424
+ <a href="javascript: void(0);" onclick="document.getElementById('migration3').style.display = 'block';">migration</a> to help.
425
+ <% end %>
426
+ </p>
427
+
428
+ <div id="migration3" style="display: none;">
429
+ <pre>rails generate migration add_suggested_indexes</pre>
430
+ <p>And paste</p>
431
+ <pre style="overflow: scroll; white-space: pre; word-break: normal;">commit_db_transaction
432
+ <% @suggested_indexes.each do |index| %>
433
+ <% if index[:using] && index[:using] != "btree" %>
434
+ add_index <%= index[:table].to_sym.inspect %>, <%= index[:columns].first.inspect %>, using: <%= index[:using].inspect %>, algorithm: :concurrently
435
+ <% else %>
436
+ add_index <%= index[:table].to_sym.inspect %>, [<%= index[:columns].map(&:to_sym).map(&:inspect).join(", ") %>], algorithm: :concurrently<% end %>
437
+ <% end %></pre>
438
+ </div>
439
+
440
+ <% @suggested_indexes.each_with_index do |index, i| %>
441
+ <hr />
442
+ <%= render partial: "suggested_index", locals: {index: index, details: index[:details]} %>
443
+ <p>to speed up</p>
444
+ <%= render partial: "queries_table", locals: {queries: index[:queries].map { |q| @query_stats_by_query[q] }, suggested_indexes: false} %>
445
+ <% end %>
446
+ </div>
447
+ <% end %>
448
+
449
+ <% if !@query_stats_enabled %>
450
+ <div class="content">
451
+ <h1>Query Stats</h1>
452
+
453
+ <% if @query_stats_available && !@query_stats_extension_enabled %>
454
+ <p>
455
+ Query stats are available but not enabled.
456
+ <%= button_to "Enable", enable_query_stats_path, class: "btn btn-info" %>
457
+ </p>
458
+ <% else %>
459
+ <p>Make them available by adding the following lines to <code>postgresql.conf</code>:</p>
460
+ <pre>shared_preload_libraries = 'pg_stat_statements'
461
+ pg_stat_statements.track = all</pre>
462
+ <p>Restart the server for the changes to take effect.</p>
463
+ <% end %>
464
+ </div>
465
+ <% end %>
466
+
467
+ <% if @query_stats_enabled && @slow_queries.any? %>
468
+ <div class="content">
469
+ <h1>Slow Queries</h1>
470
+
471
+ <p>Slow queries take <%= @database.slow_query_ms %> ms or more on average and have been called at least <%= @database.slow_query_calls %> times.</p>
472
+ <p><%= link_to "Explain queries", explain_path %> to see where to add indexes.</p>
473
+
474
+ <%= render partial: "queries_table", locals: {queries: @slow_queries} %>
475
+ </div>
476
+ <% end %>
477
+
478
+ <% if @extended && @unused_indexes.any? %>
479
+ <div class="content">
480
+ <h1>Unused Indexes</h1>
481
+
482
+ <p>
483
+ Unused indexes cause unnecessary overhead. Remove them
484
+ <% if @show_migrations %>
485
+ <a href="javascript: void(0);" onclick="document.getElementById('migration').style.display = 'block';">with a migration</a>
486
+ <% end %>
487
+ for faster writes.
488
+ </p>
489
+
490
+ <div id="migration" style="display: none;">
491
+ <pre>rails generate migration remove_unused_indexes</pre>
492
+ <p>And paste</p>
493
+ <pre style="overflow: scroll; white-space: pre; word-break: normal;"><% @unused_indexes.each do |query| %>
494
+ <%= pghero_remove_index(query)%><% end %></pre>
495
+ </div>
496
+
497
+ <table class="table">
498
+ <thead>
499
+ <tr>
500
+ <th>Name</th>
501
+ <th style="width: 20%;">Index Size</th>
502
+ </tr>
503
+ </thead>
504
+ <tbody>
505
+ <% @unused_indexes.each do |query| %>
506
+ <tr>
507
+ <td><%= query[:index] %><div class="text-muted">on <%= query[:table] %></div></td>
508
+ <td><%= query[:size] %></td>
509
+ </tr>
510
+ <% end %>
511
+ </tbody>
512
+ </table>
513
+ </div>
514
+ <% end %>
515
+
516
+ <script>
517
+ highlightQueries();
518
+ </script>