pghero 1.5.3 → 2.0.0

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 (66) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +2 -0
  3. data/CHANGELOG.md +75 -0
  4. data/Gemfile +1 -1
  5. data/README.md +8 -14
  6. data/app/assets/javascripts/pghero/Chart.bundle.js +7512 -5661
  7. data/app/assets/javascripts/pghero/application.js +9 -0
  8. data/app/assets/javascripts/pghero/chartkick.js +511 -152
  9. data/app/assets/javascripts/pghero/highlight.pack.js +2 -0
  10. data/app/assets/stylesheets/pghero/application.css +66 -2
  11. data/app/assets/stylesheets/pghero/arduino-light.css +86 -0
  12. data/app/controllers/pg_hero/home_controller.rb +166 -63
  13. data/app/helpers/pg_hero/base_helper.rb +15 -0
  14. data/app/views/layouts/pg_hero/application.html.erb +4 -3
  15. data/app/views/pg_hero/home/_connections_table.html.erb +2 -9
  16. data/app/views/pg_hero/home/_live_queries_table.html.erb +11 -8
  17. data/app/views/pg_hero/home/_queries_table.html.erb +21 -10
  18. data/app/views/pg_hero/home/_suggested_index.html.erb +1 -1
  19. data/app/views/pg_hero/home/connections.html.erb +19 -1
  20. data/app/views/pg_hero/home/explain.html.erb +1 -1
  21. data/app/views/pg_hero/home/index.html.erb +125 -66
  22. data/app/views/pg_hero/home/index_bloat.html.erb +69 -0
  23. data/app/views/pg_hero/home/live_queries.html.erb +3 -1
  24. data/app/views/pg_hero/home/maintenance.html.erb +11 -7
  25. data/app/views/pg_hero/home/queries.html.erb +10 -0
  26. data/app/views/pg_hero/home/relation_space.html.erb +9 -0
  27. data/app/views/pg_hero/home/show_query.html.erb +107 -0
  28. data/app/views/pg_hero/home/space.html.erb +64 -10
  29. data/app/views/pg_hero/home/system.html.erb +16 -4
  30. data/config/routes.rb +4 -2
  31. data/guides/Linux.md +20 -0
  32. data/guides/Rails.md +30 -4
  33. data/guides/Suggested-Indexes.md +1 -1
  34. data/lib/generators/pghero/config_generator.rb +13 -0
  35. data/lib/generators/pghero/query_stats_generator.rb +7 -1
  36. data/lib/generators/pghero/space_stats_generator.rb +7 -1
  37. data/lib/generators/pghero/templates/config.yml +23 -0
  38. data/lib/generators/pghero/templates/query_stats.rb +1 -1
  39. data/lib/generators/pghero/templates/space_stats.rb +1 -1
  40. data/lib/pghero/database.rb +6 -2
  41. data/lib/pghero/engine.rb +10 -7
  42. data/lib/pghero/methods/basic.rb +83 -11
  43. data/lib/pghero/methods/connections.rb +16 -56
  44. data/lib/pghero/methods/explain.rb +3 -5
  45. data/lib/pghero/methods/indexes.rb +179 -21
  46. data/lib/pghero/methods/kill.rb +2 -2
  47. data/lib/pghero/methods/maintenance.rb +34 -20
  48. data/lib/pghero/methods/queries.rb +28 -45
  49. data/lib/pghero/methods/query_stats.rb +97 -91
  50. data/lib/pghero/methods/{replica.rb → replication.rb} +20 -4
  51. data/lib/pghero/methods/sequences.rb +4 -5
  52. data/lib/pghero/methods/space.rb +101 -8
  53. data/lib/pghero/methods/suggested_indexes.rb +49 -108
  54. data/lib/pghero/methods/system.rb +23 -13
  55. data/lib/pghero/methods/tables.rb +9 -9
  56. data/lib/pghero/methods/users.rb +10 -12
  57. data/lib/pghero/version.rb +1 -1
  58. data/lib/pghero.rb +34 -36
  59. data/lib/tasks/pghero.rake +5 -0
  60. data/test/basic_test.rb +38 -0
  61. data/test/best_index_test.rb +3 -3
  62. data/test/suggested_indexes_test.rb +0 -2
  63. data/test/test_helper.rb +38 -40
  64. metadata +14 -7
  65. data/app/views/pg_hero/home/index_usage.html.erb +0 -27
  66. data/test/explain_test.rb +0 -18
@@ -0,0 +1,107 @@
1
+ <div class="content">
2
+ <pre><code style="max-height: 230px; overflow: hidden;" onclick="this.style.maxHeight = 'none';"><%= @query %></code></pre>
3
+ <script>
4
+ highlightQueries()
5
+ </script>
6
+
7
+ <% if @explainable_query %>
8
+ <p>
9
+ <% button_path, button_options = Rails.version >= "4.1" ? [explain_path, {params: {query: @explainable_query}}] : [explain_path(query: @explainable_query), {}] %>
10
+ <%= button_to "Explain", button_path, button_options.merge(form: {target: "_blank"}, class: "btn btn-info") %>
11
+ </p>
12
+ <% end %>
13
+
14
+ <% if @origins && @origins.keys.select { |k| k.length > 0 }.any? %>
15
+ <table style="table-layout: auto;">
16
+ <thead>
17
+ <tr>
18
+ <th colspan="2">
19
+ <div style="float: right;">Approx. Time</div>
20
+ Origin
21
+ </th>
22
+ </tr>
23
+ </thead>
24
+ <tbody>
25
+ <% @origins.sort_by { |o, c| [-c, o.to_s] }.each do |origin, count| %>
26
+ <tr>
27
+ <td class="origin" style="width: 90%;">
28
+ <% if origin.length > 0 %>
29
+ <%= origin %>
30
+ <% else %>
31
+ <span class="text-muted">Unknown</span>
32
+ <% end %>
33
+ </td>
34
+ <td style="text-align: right; width: 10%;">
35
+ <% pct = (100.0 * count / @total_count).round %>
36
+ <% if pct == 0 %>
37
+ &lt; 1%
38
+ <% else %>
39
+ <%= pct %>%
40
+ <% end %>
41
+ </td>
42
+ </tr>
43
+ <% end %>
44
+ </tbody>
45
+ </table>
46
+ <% end %>
47
+
48
+ <!-- chart -->
49
+ <% if @chart_data %>
50
+ <h1>Total Time <small>ms</small></h1>
51
+ <div id="chart-1" class="chart" style="margin-bottom: 20px;">Loading...</div>
52
+ <script>
53
+ new Chartkick.LineChart("chart-1", <%= json_escape(@chart_data.to_json).html_safe %>, {colors: ["#5bc0de"], legend: false})
54
+ </script>
55
+
56
+ <h1>Average Time <small>ms</small></h1>
57
+ <div id="chart-2" class="chart" style="margin-bottom: 20px;">Loading...</div>
58
+ <script>
59
+ new Chartkick.LineChart("chart-2", <%= json_escape(@chart2_data.to_json).html_safe %>, {colors: ["#5bc0de"], legend: false})
60
+ </script>
61
+
62
+ <h1>Calls</h1>
63
+ <div id="chart-3" class="chart" style="margin-bottom: 20px;">Loading...</div>
64
+ <script>
65
+ new Chartkick.LineChart("chart-3", <%= json_escape(@chart3_data.to_json).html_safe %>, {colors: ["#5bc0de"], legend: false})
66
+ </script>
67
+ <% else %>
68
+ <p>
69
+ Enable
70
+ <%= link_to "historical query stats", "https://github.com/ankane/pghero", target: "_blank" %>
71
+ to see more details
72
+ </p>
73
+ <% end %>
74
+
75
+ <!-- table info -->
76
+ <% if @tables.any? %>
77
+ <h1>Tables</h1>
78
+ <table>
79
+ <thead>
80
+ <tr>
81
+ <th style="width: 25%;">Name</th>
82
+ <th style="width: 25%;">Rows</th>
83
+ <th>Indexes</th>
84
+ </tr>
85
+ </thead>
86
+ <tbody>
87
+ <% @tables.each do |table| %>
88
+ <tr>
89
+ <td><%= table %></td>
90
+ <td><%= @row_counts[table] %></td>
91
+ <td>
92
+ <ul class="list-normal">
93
+ <% @indexes_by_table[table].to_a.sort_by { |i| [i[:primary] ? 0 : 1, i[:columns]] }.each do |i3| %>
94
+ <li>
95
+ <%= i3[:columns].join(", ") %><% if i3[:using] != "btree" %>
96
+ <%= i3[:using].to_s.upcase %><% end %>
97
+ <% if i3[:primary] %> PRIMARY<% elsif i3[:unique] %> UNIQUE<% end %>
98
+ </li>
99
+ <% end %>
100
+ </ul>
101
+ </td>
102
+ </tr>
103
+ <% end %>
104
+ </tbody>
105
+ </table>
106
+ <% end %>
107
+ </div>
@@ -3,25 +3,79 @@
3
3
 
4
4
  <p>Database Size: <%= @database_size %></p>
5
5
 
6
- <table class="table">
6
+ <% if @system_stats_enabled %>
7
+ <div id="chart-1" class="chart" style="margin-bottom: 20px;">Loading...</div>
8
+ <script>
9
+ new Chartkick.LineChart("chart-1", <%= json_escape(free_space_stats_path.to_json).html_safe %>, {colors: ["#5bc0de"]})
10
+ </script>
11
+ <% end %>
12
+
13
+ <!--
14
+ <% if @index_bloat.any? %>
15
+ <p>Check out <%= link_to "index bloat", index_bloat_path %> for an easy way to reclaim space.</p>
16
+ <% end %>
17
+ -->
18
+
19
+ <% if @unused_indexes.any? %>
20
+ <p>
21
+ <%= pluralize(@unused_indexes.size, "unused index") %>. Remove them
22
+ <% if @show_migrations %>
23
+ <a href="javascript: void(0);" onclick="document.getElementById('migration').style.display = 'block';">with a migration</a>
24
+ <% end %>
25
+ for faster writes.
26
+
27
+ <% if @database.replicating? %>
28
+ Check they aren’t used on replicas.
29
+ <% end %>
30
+ </p>
31
+
32
+ <div id="migration" style="display: none;">
33
+ <pre>rails g migration remove_unused_indexes</pre>
34
+ <p>And paste</p>
35
+ <pre style="overflow: scroll; white-space: pre; word-break: normal;"><% @unused_indexes.sort_by { |q| q[:index] }.each do |query| %>
36
+ remove_index <%= query[:table].to_sym.inspect %>, name: <%= query[:index].to_s.inspect %><% end %></pre>
37
+ </div>
38
+ <% end %>
39
+
40
+ <table class="table space-table">
7
41
  <thead>
8
42
  <tr>
9
- <th>Relation</th>
10
- <th style="width: 15%;"></th>
11
- <th style="width: 15%;">Size</th>
43
+ <th><%= link_to "Relation", {sort: "name"} %></th>
44
+ <th style="width: 15%;"><%= link_to "Size", {} %></th>
45
+ <% if @space_stats_enabled %>
46
+ <th style="width: 15%;"><%= link_to "#{@days}d Growth", {sort: "growth"} %></th>
47
+ <% end %>
12
48
  </tr>
13
49
  </thead>
14
50
  <tbody>
15
51
  <% @relation_sizes.each do |query| %>
16
52
  <tr>
17
- <td>
18
- <%= query["name"] %>
19
- <% if query["schema"] != "public" %>
20
- <span class="text-muted"><%= query["schema"] %></span>
53
+ <td style="<%= query[:type] == "index" ? "font-style: italic;" : "" %>">
54
+ <span style="word-break: break-all;">
55
+ <% name = query[:relation] || query[:table] %>
56
+ <% if @space_stats_enabled %>
57
+ <%= link_to name, relation_space_path(name), target: "_blank", style: "color: inherit;" %>
58
+ <% else %>
59
+ <%= name %>
60
+ <% end %>
61
+ </span>
62
+ <% if query[:schema] != "public" %>
63
+ <span class="text-muted"><%= query[:schema] %></span>
64
+ <% end %>
65
+ <% if @unused_index_names.include?(query[:name]) %>
66
+ <span class="unused-index">UNUSED</span>
21
67
  <% end %>
22
68
  </td>
23
- <td> <span class="text-muted"><%= query["type"] == "index" ? "index" : "" %></span></td>
24
- <td><%= query["size"] %></td>
69
+ <td><%= query[:size] %></td>
70
+ <% if @space_stats_enabled %>
71
+ <td>
72
+ <% if @growth_bytes_by_relation[query[:relation]] %>
73
+ <% if @growth_bytes_by_relation[query[:relation]] < 0 %>-<% end %><%= PgHero.pretty_size(@growth_bytes_by_relation[query[:relation]].abs) %>
74
+ <% else %>
75
+ <span class="text-muted">Unknown</span>
76
+ <% end %>
77
+ </td>
78
+ <% end %>
25
79
  </tr>
26
80
  <% end %>
27
81
  </tbody>
@@ -7,16 +7,28 @@
7
7
  <% path_options = {duration: params[:duration], period: params[:period]} %>
8
8
 
9
9
  <h1>CPU</h1>
10
- <div style="margin-bottom: 20px;"><%= line_chart cpu_usage_path(path_options), max: 100, colors: ["#5bc0de"] %></div>
10
+ <div id="chart-1" class="chart" style="margin-bottom: 20px;">Loading...</div>
11
+ <script>
12
+ new Chartkick.LineChart("chart-1", <%= json_escape(cpu_usage_path(path_options).to_json).html_safe %>, {max: 100, colors: ["#5bc0de"]})
13
+ </script>
11
14
 
12
15
  <h1>Load</h1>
13
- <div style="margin-bottom: 20px;"><%= line_chart load_stats_path(path_options), colors: ["#5bc0de", "#d9534f"] %></div>
16
+ <div id="chart-2" class="chart" style="margin-bottom: 20px;">Loading...</div>
17
+ <script>
18
+ new Chartkick.LineChart("chart-2", <%= json_escape(load_stats_path(path_options).to_json).html_safe %>, {colors: ["#5bc0de", "#d9534f"]})
19
+ </script>
14
20
 
15
21
  <h1>Connections</h1>
16
- <div style="margin-bottom: 20px;"><%= line_chart connection_stats_path(path_options), colors: ["#5bc0de"] %></div>
22
+ <div id="chart-3" class="chart" style="margin-bottom: 20px;">Loading...</div>
23
+ <script>
24
+ new Chartkick.LineChart("chart-3", <%= json_escape(connection_stats_path(path_options).to_json).html_safe %>, {colors: ["#5bc0de"]})
25
+ </script>
17
26
 
18
27
  <% if @database.replica? %>
19
28
  <h1>Replication Lag</h1>
20
- <div style="margin-bottom: 20px;"><%= line_chart replication_lag_stats_path(path_options), colors: ["#5bc0de"] %></div>
29
+ <div id="chart-4" class="chart" style="margin-bottom: 20px;">Loading...</div>
30
+ <script>
31
+ new Chartkick.LineChart("chart-4", <%= json_escape(replication_lag_stats_path(path_options).to_json).html_safe %>, {colors: ["#5bc0de"]})
32
+ </script>
21
33
  <% end %>
22
34
  </div>
data/config/routes.rb CHANGED
@@ -1,14 +1,17 @@
1
1
  PgHero::Engine.routes.draw do
2
2
  scope "(:database)", constraints: proc { |req| (PgHero.config["databases"].keys + [nil]).include?(req.params[:database]) } do
3
- get "index_usage", to: "home#index_usage"
4
3
  get "space", to: "home#space"
4
+ get "space/:relation", to: "home#relation_space", as: :relation_space
5
+ get "index_bloat", to: "home#index_bloat"
5
6
  get "live_queries", to: "home#live_queries"
6
7
  get "queries", to: "home#queries"
8
+ get "queries/:query_hash", to: "home#show_query", as: :show_query
7
9
  get "system", to: "home#system"
8
10
  get "cpu_usage", to: "home#cpu_usage"
9
11
  get "connection_stats", to: "home#connection_stats"
10
12
  get "replication_lag_stats", to: "home#replication_lag_stats"
11
13
  get "load_stats", to: "home#load_stats"
14
+ get "free_space_stats", to: "home#free_space_stats"
12
15
  get "explain", to: "home#explain"
13
16
  get "tune", to: "home#tune"
14
17
  get "connections", to: "home#connections"
@@ -23,7 +26,6 @@ PgHero::Engine.routes.draw do
23
26
  # legacy routes
24
27
  get "system_stats" => redirect("system")
25
28
  get "query_stats" => redirect("queries")
26
- get "indexes" => redirect("index_usage")
27
29
 
28
30
  root to: "home#index"
29
31
  end
data/guides/Linux.md CHANGED
@@ -186,6 +186,26 @@ sudo pghero config:set PGHERO_SECRET_ACCESS_KEY=secret123
186
186
  sudo pghero config:set PGHERO_DB_INSTANCE_IDENTIFIER=epona
187
187
  ```
188
188
 
189
+ ## Multiple Databases
190
+
191
+ Create a `pghero.yml` with:
192
+
193
+ ```yml
194
+ production:
195
+ databases:
196
+ primary:
197
+ url: postgres://...
198
+ replica:
199
+ url: postgres://...
200
+ ```
201
+
202
+ And run:
203
+
204
+ ```sh
205
+ cat pghero.yml | sudo pghero run sh -c "cat > config/pghero.yml"
206
+ sudo service pghero restart
207
+ ```
208
+
189
209
  ## Customize
190
210
 
191
211
  Minimum time for long running queries
data/guides/Rails.md CHANGED
@@ -19,7 +19,7 @@ Be sure to [secure the dashboard](#security) in production.
19
19
  PgHero can suggest indexes to add. To enable, add to your Gemfile:
20
20
 
21
21
  ```ruby
22
- gem 'pg_query'
22
+ gem 'pg_query', '>= 0.9.0'
23
23
  ```
24
24
 
25
25
  and make sure [query stats](#query-stats) are enabled. Read about how it works [here](Suggested-Indexes.md).
@@ -38,7 +38,7 @@ ENV["PGHERO_PASSWORD"] = "hyrule"
38
38
  #### Devise
39
39
 
40
40
  ```ruby
41
- authenticate :user, lambda { |user| user.admin? } do
41
+ authenticate :user, -> (user) { user.admin? } do
42
42
  mount PgHero::Engine, at: "pghero"
43
43
  end
44
44
  ```
@@ -78,11 +78,10 @@ ENV["PGHERO_STATS_DATABASE_URL"]
78
78
 
79
79
  ## System Stats
80
80
 
81
- CPU usage is available for Amazon RDS. Add these lines to your application’s Gemfile:
81
+ CPU usage, IOPS, and other stats are available for Amazon RDS. Add these lines to your application’s Gemfile:
82
82
 
83
83
  ```ruby
84
84
  gem 'aws-sdk'
85
- gem 'chartkick'
86
85
  ```
87
86
 
88
87
  And add these variables to your environment:
@@ -242,6 +241,33 @@ PgHero.drop_user("ganondorf")
242
241
 
243
242
  ## Upgrading
244
243
 
244
+ ### 2.0.0
245
+
246
+ New features
247
+
248
+ - Query details page
249
+
250
+ Breaking changes
251
+
252
+ - Methods now return symbols for keys instead of strings
253
+ - Methods raise `PgHero::NotEnabled` error when a feature isn’t enabled
254
+ - Requires pg_query 0.9.0+ for suggested indexes
255
+ - Historical query stats require the `pghero_query_stats` table to have `query_hash` and `user` columns
256
+ - Removed `with` option - use:
257
+
258
+ ```ruby
259
+ PgHero.databases[:database2].running_queries
260
+ ```
261
+
262
+ instead of
263
+
264
+ ```ruby
265
+ PgHero.with(:database2) { PgHero.running_queries }
266
+ ```
267
+
268
+ - Removed options from `connection_sources` method
269
+ - Removed `locks` method
270
+
245
271
  ### 1.5.0
246
272
 
247
273
  For query stats grouping by user, create a migration with:
@@ -1,6 +1,6 @@
1
1
  # How PgHero Suggests Indexes
2
2
 
3
- 1. Get the most time-consuming queries from [pg_stat_statements](http://www.postgresql.org/docs/9.3/static/pgstatstatements.html).
3
+ 1. Get the most time-consuming queries from [pg_stat_statements](http://www.postgresql.org/docs/current/static/pgstatstatements.html).
4
4
 
5
5
  2. Parse queries with [pg_query](https://github.com/lfittl/pg_query). Look for a single table with a `WHERE` clause that consists of only `=`, `IN`, `IS NULL` or `IS NOT NULL` and/or an `ORDER BY` clause.
6
6
 
@@ -0,0 +1,13 @@
1
+ require "rails/generators"
2
+
3
+ module Pghero
4
+ module Generators
5
+ class ConfigGenerator < Rails::Generators::Base
6
+ source_root File.expand_path("../templates", __FILE__)
7
+
8
+ def create_initializer
9
+ template "config.yml", "config/pghero.yml"
10
+ end
11
+ end
12
+ end
13
+ end
@@ -22,7 +22,13 @@ module Pghero
22
22
  end
23
23
 
24
24
  def copy_migration
25
- migration_template "query_stats.rb", "db/migrate/create_pghero_query_stats.rb"
25
+ migration_template "query_stats.rb", "db/migrate/create_pghero_query_stats.rb", migration_version: migration_version
26
+ end
27
+
28
+ def migration_version
29
+ if ActiveRecord::VERSION::MAJOR >= 5
30
+ "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
31
+ end
26
32
  end
27
33
  end
28
34
  end
@@ -22,7 +22,13 @@ module Pghero
22
22
  end
23
23
 
24
24
  def copy_migration
25
- migration_template "space_stats.rb", "db/migrate/create_pghero_space_stats.rb"
25
+ migration_template "space_stats.rb", "db/migrate/create_pghero_space_stats.rb", migration_version: migration_version
26
+ end
27
+
28
+ def migration_version
29
+ if ActiveRecord::VERSION::MAJOR >= 5
30
+ "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
31
+ end
26
32
  end
27
33
  end
28
34
  end
@@ -0,0 +1,23 @@
1
+ databases:
2
+ main:
3
+ # Database URL (defaults to app database)
4
+ # url: <%= ENV["DATABASE_URL"] %>
5
+
6
+ # Minimum time for long running queries
7
+ # long_running_query_sec: 60
8
+
9
+ # Minimum average time for slow queries
10
+ # slow_query_ms: 20
11
+
12
+ # Minimum calls for slow queries
13
+ # slow_query_calls: 100
14
+
15
+ # Minimum connections for high connections warning
16
+ # total_connections_threshold: 100
17
+
18
+ # Add more databases
19
+ # other:
20
+ # url: <%= ENV["OTHER_DATABASE_URL"] %>
21
+
22
+ # Time zone (defaults to app time zone)
23
+ # time_zone: "Pacific Time (US & Canada)"
@@ -1,4 +1,4 @@
1
- class <%= migration_class_name %> < ActiveRecord::Migration
1
+ class <%= migration_class_name %> < ActiveRecord::Migration<%= migration_version %>
2
2
  def change
3
3
  create_table :pghero_query_stats do |t|
4
4
  t.text :database
@@ -1,4 +1,4 @@
1
- class <%= migration_class_name %> < ActiveRecord::Migration
1
+ class <%= migration_class_name %> < ActiveRecord::Migration<%= migration_version %>
2
2
  def change
3
3
  create_table :pghero_space_stats do |t|
4
4
  t.text :database
@@ -8,7 +8,7 @@ module PgHero
8
8
  include Methods::Maintenance
9
9
  include Methods::Queries
10
10
  include Methods::QueryStats
11
- include Methods::Replica
11
+ include Methods::Replication
12
12
  include Methods::Sequences
13
13
  include Methods::Space
14
14
  include Methods::SuggestedIndexes
@@ -20,7 +20,7 @@ module PgHero
20
20
 
21
21
  def initialize(id, config)
22
22
  @id = id
23
- @config = config
23
+ @config = config || {}
24
24
  end
25
25
 
26
26
  def name
@@ -55,6 +55,10 @@ module PgHero
55
55
  (config["long_running_query_sec"] || PgHero.config["long_running_query_sec"] || PgHero.long_running_query_sec).to_i
56
56
  end
57
57
 
58
+ def index_bloat_bytes
59
+ (config["index_bloat_bytes"] || PgHero.config["index_bloat_bytes"] || 100.megabytes).to_i
60
+ end
61
+
58
62
  private
59
63
 
60
64
  def connection_model
data/lib/pghero/engine.rb CHANGED
@@ -3,13 +3,16 @@ module PgHero
3
3
  isolate_namespace PgHero
4
4
 
5
5
  initializer "pghero", group: :all do |app|
6
- if defined?(Sprockets) && Sprockets::VERSION >= "4"
7
- app.config.assets.precompile << "pghero/application.js"
8
- app.config.assets.precompile << "pghero/application.css"
9
- else
10
- # use a proc instead of a string
11
- app.config.assets.precompile << proc { |path| path == "pghero/application.js" }
12
- app.config.assets.precompile << proc { |path| path == "pghero/application.css" }
6
+ # check if Rails api mode
7
+ if app.config.respond_to?(:assets)
8
+ if defined?(Sprockets) && Sprockets::VERSION >= "4"
9
+ app.config.assets.precompile << "pghero/application.js"
10
+ app.config.assets.precompile << "pghero/application.css"
11
+ else
12
+ # use a proc instead of a string
13
+ app.config.assets.precompile << proc { |path| path == "pghero/application.js" }
14
+ app.config.assets.precompile << proc { |path| path == "pghero/application.css" }
15
+ end
13
16
  end
14
17
 
15
18
  PgHero.time_zone = PgHero.config["time_zone"] if PgHero.config["time_zone"]
@@ -4,44 +4,78 @@ module PgHero
4
4
  def settings
5
5
  names =
6
6
  if server_version_num >= 90500
7
- %w(
7
+ %i(
8
8
  max_connections shared_buffers effective_cache_size work_mem
9
9
  maintenance_work_mem min_wal_size max_wal_size checkpoint_completion_target
10
10
  wal_buffers default_statistics_target
11
11
  )
12
12
  else
13
- %w(
13
+ %i(
14
14
  max_connections shared_buffers effective_cache_size work_mem
15
15
  maintenance_work_mem checkpoint_segments checkpoint_completion_target
16
16
  wal_buffers default_statistics_target
17
17
  )
18
18
  end
19
- Hash[names.map { |name| [name, select_all("SHOW #{name}").first[name]] }]
19
+ Hash[names.map { |name| [name, select_one("SHOW #{name}")] }]
20
20
  end
21
21
 
22
22
  def ssl_used?
23
23
  ssl_used = nil
24
- connection_model.transaction do
25
- execute("CREATE EXTENSION IF NOT EXISTS sslinfo")
26
- ssl_used = PgHero.truthy?(select_all("SELECT ssl_is_used()").first["ssl_is_used"])
27
- raise ActiveRecord::Rollback
24
+ with_transaction(rollback: true) do
25
+ begin
26
+ execute("CREATE EXTENSION IF NOT EXISTS sslinfo")
27
+ rescue ActiveRecord::StatementInvalid
28
+ # not superuser
29
+ end
30
+ ssl_used = select_one("SELECT ssl_is_used()")
28
31
  end
29
32
  ssl_used
30
33
  end
31
34
 
32
35
  def database_name
33
- select_all("SELECT current_database()").first["current_database"]
36
+ select_one("SELECT current_database()")
34
37
  end
35
38
 
36
39
  def server_version
37
- select_all("SHOW server_version").first["server_version"]
40
+ @server_version ||= select_one("SHOW server_version")
41
+ end
42
+
43
+ def server_version_num
44
+ @server_version_num ||= select_one("SHOW server_version_num").to_i
45
+ end
46
+
47
+ def quote_ident(value)
48
+ quote_table_name(value)
38
49
  end
39
50
 
40
51
  private
41
52
 
42
- def select_all(sql)
53
+ def select_all(sql, conn = nil)
54
+ conn ||= connection
43
55
  # squish for logs
44
- connection.select_all(squish(sql)).to_a
56
+ result = conn.select_all(squish(sql))
57
+ cast_method = ActiveRecord::VERSION::MAJOR < 5 ? :type_cast : :cast_value
58
+ result.map { |row| Hash[row.map { |col, val| [col.to_sym, result.column_types[col].send(cast_method, val)] }] }
59
+ end
60
+
61
+ def select_all_stats(sql)
62
+ select_all(sql, stats_connection)
63
+ end
64
+
65
+ def select_all_size(sql)
66
+ result = select_all(sql)
67
+ result.each do |row|
68
+ row[:size] = PgHero.pretty_size(row[:size_bytes])
69
+ end
70
+ result
71
+ end
72
+
73
+ def select_one(sql, conn = nil)
74
+ select_all(sql, conn).first.values.first
75
+ end
76
+
77
+ def select_one_stats(sql)
78
+ select_one(sql, stats_connection)
45
79
  end
46
80
 
47
81
  def execute(sql)
@@ -52,6 +86,16 @@ module PgHero
52
86
  connection_model.connection
53
87
  end
54
88
 
89
+ def stats_connection
90
+ ::PgHero::QueryStats.connection
91
+ end
92
+
93
+ def insert_stats(table, columns, values)
94
+ values = values.map { |v| "(#{v.map { |v2| quote(v2) }.join(",")})" }.join(",")
95
+ columns = columns.map { |v| quote_table_name(v) }.join(",")
96
+ stats_connection.execute("INSERT INTO #{quote_table_name(table)} (#{columns}) VALUES #{values}")
97
+ end
98
+
55
99
  # from ActiveSupport
56
100
  def squish(str)
57
101
  str.to_s.gsub(/\A[[:space:]]+/, "").gsub(/[[:space:]]+\z/, "").gsub(/[[:space:]]+/, " ")
@@ -72,6 +116,34 @@ module PgHero
72
116
  part
73
117
  end
74
118
  end
119
+
120
+ def with_transaction(lock_timeout: nil, statement_timeout: nil, rollback: false)
121
+ connection_model.transaction do
122
+ select_all "SET LOCAL statement_timeout = #{statement_timeout.to_i}" if statement_timeout
123
+ select_all "SET LOCAL lock_timeout = #{lock_timeout.to_i}" if lock_timeout
124
+ yield
125
+ raise ActiveRecord::Rollback if rollback
126
+ end
127
+ end
128
+
129
+ def table_exists?(table)
130
+ ["PostgreSQL", "PostGIS"].include?(stats_connection.adapter_name) &&
131
+ select_one_stats(<<-SQL
132
+ SELECT EXISTS (
133
+ SELECT
134
+ 1
135
+ FROM
136
+ pg_catalog.pg_class c
137
+ INNER JOIN
138
+ pg_catalog.pg_namespace n ON n.oid = c.relnamespace
139
+ WHERE
140
+ n.nspname = 'public'
141
+ AND c.relname = #{quote(table)}
142
+ AND c.relkind = 'r'
143
+ )
144
+ SQL
145
+ )
146
+ end
75
147
  end
76
148
  end
77
149
  end