pghero 1.7.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of pghero might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/.travis.yml +2 -0
- data/CHANGELOG.md +31 -0
- data/README.md +2 -2
- data/app/assets/javascripts/pghero/Chart.bundle.js +7512 -5661
- data/app/assets/javascripts/pghero/application.js +9 -0
- data/app/assets/javascripts/pghero/highlight.pack.js +2 -0
- data/app/assets/stylesheets/pghero/application.css +54 -2
- data/app/assets/stylesheets/pghero/arduino-light.css +86 -0
- data/app/controllers/pg_hero/home_controller.rb +148 -52
- data/app/helpers/pg_hero/base_helper.rb +15 -0
- data/app/views/layouts/pg_hero/application.html.erb +1 -1
- data/app/views/pg_hero/home/_connections_table.html.erb +2 -2
- data/app/views/pg_hero/home/_live_queries_table.html.erb +11 -7
- data/app/views/pg_hero/home/_queries_table.html.erb +21 -10
- data/app/views/pg_hero/home/_suggested_index.html.erb +1 -1
- data/app/views/pg_hero/home/connections.html.erb +2 -14
- data/app/views/pg_hero/home/explain.html.erb +1 -1
- data/app/views/pg_hero/home/index.html.erb +58 -22
- data/app/views/pg_hero/home/index_bloat.html.erb +69 -0
- data/app/views/pg_hero/home/maintenance.html.erb +7 -7
- data/app/views/pg_hero/home/queries.html.erb +10 -0
- data/app/views/pg_hero/home/relation_space.html.erb +9 -0
- data/app/views/pg_hero/home/show_query.html.erb +107 -0
- data/app/views/pg_hero/home/space.html.erb +64 -10
- data/config/routes.rb +4 -2
- data/guides/Rails.md +28 -1
- data/guides/Suggested-Indexes.md +1 -1
- data/lib/pghero.rb +25 -36
- data/lib/pghero/database.rb +5 -1
- data/lib/pghero/methods/basic.rb +78 -13
- data/lib/pghero/methods/connections.rb +16 -56
- data/lib/pghero/methods/explain.rb +2 -6
- data/lib/pghero/methods/indexes.rb +173 -18
- data/lib/pghero/methods/kill.rb +2 -2
- data/lib/pghero/methods/maintenance.rb +23 -26
- data/lib/pghero/methods/queries.rb +1 -23
- data/lib/pghero/methods/query_stats.rb +95 -96
- data/lib/pghero/methods/{replica.rb → replication.rb} +17 -4
- data/lib/pghero/methods/sequences.rb +4 -5
- data/lib/pghero/methods/space.rb +101 -8
- data/lib/pghero/methods/suggested_indexes.rb +49 -108
- data/lib/pghero/methods/system.rb +14 -10
- data/lib/pghero/methods/tables.rb +8 -8
- data/lib/pghero/methods/users.rb +10 -12
- data/lib/pghero/version.rb +1 -1
- data/lib/tasks/pghero.rake +1 -1
- data/test/basic_test.rb +38 -0
- data/test/best_index_test.rb +3 -3
- data/test/suggested_indexes_test.rb +0 -2
- data/test/test_helper.rb +38 -40
- metadata +11 -6
- data/app/views/pg_hero/home/index_usage.html.erb +0 -27
- data/test/explain_test.rb +0 -18
@@ -0,0 +1,9 @@
|
|
1
|
+
<div class="content">
|
2
|
+
<h1><%= @relation %></h1>
|
3
|
+
|
4
|
+
<h1>Size <small>MB</small></h1>
|
5
|
+
<div id="chart-1" class="chart" style="margin-bottom: 20px;">Loading...</div>
|
6
|
+
<script>
|
7
|
+
new Chartkick.LineChart("chart-1", <%= json_escape(@chart_data.to_json).html_safe %>, {colors: ["#5bc0de"], legend: false, min: null})
|
8
|
+
</script>
|
9
|
+
</div>
|
@@ -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
|
+
< 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
|
-
|
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
|
10
|
-
<th style="width: 15%;"
|
11
|
-
|
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
|
-
|
19
|
-
|
20
|
-
|
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
|
24
|
-
|
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>
|
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/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).
|
@@ -241,6 +241,33 @@ PgHero.drop_user("ganondorf")
|
|
241
241
|
|
242
242
|
## Upgrading
|
243
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
|
+
|
244
271
|
### 1.5.0
|
245
272
|
|
246
273
|
For query stats grouping by user, create a migration with:
|
data/guides/Suggested-Indexes.md
CHANGED
@@ -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/
|
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
|
|
data/lib/pghero.rb
CHANGED
@@ -10,7 +10,7 @@ require "pghero/methods/kill"
|
|
10
10
|
require "pghero/methods/maintenance"
|
11
11
|
require "pghero/methods/queries"
|
12
12
|
require "pghero/methods/query_stats"
|
13
|
-
require "pghero/methods/
|
13
|
+
require "pghero/methods/replication"
|
14
14
|
require "pghero/methods/sequences"
|
15
15
|
require "pghero/methods/space"
|
16
16
|
require "pghero/methods/suggested_indexes"
|
@@ -26,6 +26,8 @@ require "pghero/connection"
|
|
26
26
|
require "pghero/query_stats"
|
27
27
|
|
28
28
|
module PgHero
|
29
|
+
class NotEnabled < StandardError; end
|
30
|
+
|
29
31
|
# settings
|
30
32
|
class << self
|
31
33
|
attr_accessor :long_running_query_sec, :slow_query_ms, :slow_query_calls, :total_connections_threshold, :cache_hit_rate_threshold, :env, :show_migrations
|
@@ -40,16 +42,16 @@ module PgHero
|
|
40
42
|
|
41
43
|
class << self
|
42
44
|
extend Forwardable
|
43
|
-
def_delegators :
|
45
|
+
def_delegators :primary_database, :access_key_id, :analyze, :analyze_tables, :autoindex, :autovacuum_danger,
|
44
46
|
:best_index, :blocked_queries, :connection_sources, :connection_stats,
|
45
47
|
:cpu_usage, :create_user, :database_size, :db_instance_identifier, :disable_query_stats, :drop_user,
|
46
48
|
:duplicate_indexes, :enable_query_stats, :explain, :historical_query_stats_enabled?, :index_caching,
|
47
49
|
:index_hit_rate, :index_usage, :indexes, :invalid_indexes, :kill, :kill_all, :kill_long_running_queries,
|
48
|
-
:
|
50
|
+
:last_stats_reset_time, :long_running_queries, :maintenance_info, :missing_indexes, :query_stats,
|
49
51
|
:query_stats_available?, :query_stats_enabled?, :query_stats_extension_enabled?, :query_stats_readable?,
|
50
52
|
:rds_stats, :read_iops_stats, :region, :relation_sizes, :replica?, :replication_lag, :replication_lag_stats,
|
51
|
-
:reset_query_stats, :running_queries, :secret_access_key, :sequence_danger, :sequences, :settings,
|
52
|
-
:slow_queries, :ssl_used?, :stats_connection, :suggested_indexes, :suggested_indexes_by_query,
|
53
|
+
:reset_query_stats, :reset_stats, :running_queries, :secret_access_key, :sequence_danger, :sequences, :settings,
|
54
|
+
:slow_queries, :space_growth, :ssl_used?, :stats_connection, :suggested_indexes, :suggested_indexes_by_query,
|
53
55
|
:suggested_indexes_enabled?, :system_stats_enabled?, :table_caching, :table_hit_rate, :table_stats,
|
54
56
|
:total_connections, :transaction_id_danger, :unused_indexes, :unused_tables, :write_iops_stats
|
55
57
|
|
@@ -62,16 +64,20 @@ module PgHero
|
|
62
64
|
end
|
63
65
|
|
64
66
|
def config
|
65
|
-
|
67
|
+
@config ||= begin
|
66
68
|
path = "config/pghero.yml"
|
67
69
|
|
68
|
-
|
70
|
+
config_file_exists = File.exist?(path)
|
71
|
+
|
72
|
+
config = YAML.load(ERB.new(File.read(path)).result) if config_file_exists
|
69
73
|
config ||= {}
|
70
74
|
|
71
75
|
if config[env]
|
72
76
|
config[env]
|
73
77
|
elsif config["databases"] # preferred format
|
74
78
|
config
|
79
|
+
elsif config_file_exists
|
80
|
+
raise "Invalid config file"
|
75
81
|
else
|
76
82
|
{
|
77
83
|
"databases" => {
|
@@ -89,7 +95,7 @@ module PgHero
|
|
89
95
|
@databases ||= begin
|
90
96
|
Hash[
|
91
97
|
config["databases"].map do |id, c|
|
92
|
-
[id, PgHero::Database.new(id, c)]
|
98
|
+
[id.to_sym, PgHero::Database.new(id, c)]
|
93
99
|
end
|
94
100
|
]
|
95
101
|
end
|
@@ -99,26 +105,6 @@ module PgHero
|
|
99
105
|
databases.values.first
|
100
106
|
end
|
101
107
|
|
102
|
-
def current_database
|
103
|
-
Thread.current[:pghero_current_database] ||= primary_database
|
104
|
-
end
|
105
|
-
|
106
|
-
def current_database=(database)
|
107
|
-
raise "Database not found" unless databases[database.to_s]
|
108
|
-
Thread.current[:pghero_current_database] = databases[database.to_s]
|
109
|
-
database
|
110
|
-
end
|
111
|
-
|
112
|
-
def with(database)
|
113
|
-
previous_database = current_database
|
114
|
-
begin
|
115
|
-
self.current_database = database
|
116
|
-
yield
|
117
|
-
ensure
|
118
|
-
self.current_database = previous_database.id
|
119
|
-
end
|
120
|
-
end
|
121
|
-
|
122
108
|
def capture_query_stats
|
123
109
|
databases.each do |_, database|
|
124
110
|
database.capture_query_stats
|
@@ -133,20 +119,23 @@ module PgHero
|
|
133
119
|
true
|
134
120
|
end
|
135
121
|
|
136
|
-
def analyze_all
|
137
|
-
databases.each do |_, database|
|
138
|
-
database.analyze_tables
|
122
|
+
def analyze_all(**options)
|
123
|
+
databases.reject { |_, d| d.replica? }.each do |_, database|
|
124
|
+
database.analyze_tables(**options)
|
139
125
|
end
|
140
126
|
true
|
141
127
|
end
|
142
128
|
|
143
|
-
|
144
|
-
|
145
|
-
|
129
|
+
def autoindex_all(create: false)
|
130
|
+
databases.each do |_, database|
|
131
|
+
puts "Autoindexing #{database}..."
|
132
|
+
database.autoindex(create: create)
|
133
|
+
end
|
134
|
+
true
|
146
135
|
end
|
147
136
|
|
148
|
-
def
|
149
|
-
value
|
137
|
+
def pretty_size(value)
|
138
|
+
ActiveSupport::NumberHelper.number_to_human_size(value, precision: 3)
|
150
139
|
end
|
151
140
|
end
|
152
141
|
end
|
data/lib/pghero/database.rb
CHANGED
@@ -8,7 +8,7 @@ module PgHero
|
|
8
8
|
include Methods::Maintenance
|
9
9
|
include Methods::Queries
|
10
10
|
include Methods::QueryStats
|
11
|
-
include Methods::
|
11
|
+
include Methods::Replication
|
12
12
|
include Methods::Sequences
|
13
13
|
include Methods::Space
|
14
14
|
include Methods::SuggestedIndexes
|
@@ -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/methods/basic.rb
CHANGED
@@ -4,44 +4,78 @@ module PgHero
|
|
4
4
|
def settings
|
5
5
|
names =
|
6
6
|
if server_version_num >= 90500
|
7
|
-
%
|
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
|
-
%
|
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,
|
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
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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
|
-
|
36
|
+
select_one("SELECT current_database()")
|
34
37
|
end
|
35
38
|
|
36
39
|
def server_version
|
37
|
-
|
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
|
-
|
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:]]+/, " ")
|
@@ -73,12 +117,33 @@ module PgHero
|
|
73
117
|
end
|
74
118
|
end
|
75
119
|
|
76
|
-
def
|
120
|
+
def with_transaction(lock_timeout: nil, statement_timeout: nil, rollback: false)
|
77
121
|
connection_model.transaction do
|
78
|
-
select_all "SET LOCAL
|
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
|
79
124
|
yield
|
125
|
+
raise ActiveRecord::Rollback if rollback
|
80
126
|
end
|
81
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
|
82
147
|
end
|
83
148
|
end
|
84
149
|
end
|