pghero_fork 2.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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>