pghero 1.6.2 → 1.6.3
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/CHANGELOG.md +9 -0
- data/README.md +5 -11
- data/app/assets/javascripts/pghero/chartkick.js +511 -152
- data/app/assets/stylesheets/pghero/application.css +11 -0
- data/app/controllers/pg_hero/home_controller.rb +3 -2
- data/app/views/pg_hero/home/_connections_table.html.erb +0 -7
- data/app/views/pg_hero/home/_live_queries_table.html.erb +1 -2
- data/app/views/pg_hero/home/connections.html.erb +29 -1
- data/app/views/pg_hero/home/explain.html.erb +2 -0
- data/app/views/pg_hero/home/index.html.erb +55 -1
- data/app/views/pg_hero/home/live_queries.html.erb +3 -1
- data/app/views/pg_hero/home/system.html.erb +16 -4
- data/guides/Rails.md +2 -3
- data/lib/generators/pghero/config_generator.rb +13 -0
- data/lib/generators/pghero/templates/config.yml +23 -0
- data/lib/pghero.rb +5 -3
- data/lib/pghero/database.rb +1 -1
- data/lib/pghero/methods/explain.rb +4 -1
- data/lib/pghero/methods/queries.rb +1 -5
- data/lib/pghero/methods/query_stats.rb +7 -0
- data/lib/pghero/version.rb +1 -1
- metadata +5 -3
@@ -47,6 +47,10 @@ h1, p {
|
|
47
47
|
margin-bottom: 20px;
|
48
48
|
}
|
49
49
|
|
50
|
+
h3 {
|
51
|
+
text-align: center;
|
52
|
+
}
|
53
|
+
|
50
54
|
ul {
|
51
55
|
list-style-type: none;
|
52
56
|
padding: 0;
|
@@ -422,3 +426,10 @@ body {
|
|
422
426
|
#periods a {
|
423
427
|
margin-right: 20px;
|
424
428
|
}
|
429
|
+
|
430
|
+
.chart {
|
431
|
+
height: 300px;
|
432
|
+
line-height: 300px;
|
433
|
+
text-align: center;
|
434
|
+
color: #999;
|
435
|
+
}
|
@@ -19,14 +19,15 @@ module PgHero
|
|
19
19
|
@extended = params[:extended]
|
20
20
|
@query_stats = @database.query_stats(historical: true, start_at: 3.hours.ago)
|
21
21
|
@slow_queries = @database.slow_queries(query_stats: @query_stats)
|
22
|
-
@long_running_queries = @database.long_running_queries
|
22
|
+
@autovacuum_queries, @long_running_queries = @database.long_running_queries.partition { |q| q["query"].starts_with?("autovacuum:") }
|
23
|
+
|
23
24
|
if @extended
|
24
25
|
@index_hit_rate = @database.index_hit_rate
|
25
26
|
@table_hit_rate = @database.table_hit_rate
|
26
27
|
@good_cache_rate = @table_hit_rate >= @database.cache_hit_rate_threshold.to_f / 100 && @index_hit_rate >= @database.cache_hit_rate_threshold.to_f / 100
|
27
28
|
end
|
28
29
|
|
29
|
-
@unused_indexes = @database.unused_indexes.select { |q| q["index_scans"].to_i == 0 }
|
30
|
+
@unused_indexes = @database.unused_indexes.select { |q| q["index_scans"].to_i == 0 } if @extended
|
30
31
|
@invalid_indexes = @database.invalid_indexes
|
31
32
|
@duplicate_indexes = @database.duplicate_indexes
|
32
33
|
unless @query_stats_enabled
|
@@ -1,10 +1,3 @@
|
|
1
|
-
<p><%= pluralize(total_connections, "connection") %></p>
|
2
|
-
<% if show_message %>
|
3
|
-
<p>
|
4
|
-
<%= 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.
|
5
|
-
</p>
|
6
|
-
<% end %>
|
7
|
-
|
8
1
|
<table class="table">
|
9
2
|
<thead>
|
10
3
|
<tr>
|
@@ -8,11 +8,10 @@
|
|
8
8
|
</tr>
|
9
9
|
</thead>
|
10
10
|
<tbody>
|
11
|
-
<% now = Time.now %>
|
12
11
|
<% queries.reverse.each do |query| %>
|
13
12
|
<tr>
|
14
13
|
<td><%= query["pid"] %></td>
|
15
|
-
<td><%=
|
14
|
+
<td><%= number_with_delimiter(query["duration_ms"].to_f.round) %> ms</td>
|
16
15
|
<td><%= query["state"] %></td>
|
17
16
|
<td class="text-right">
|
18
17
|
<% button_path, button_options = Rails.version >= "4.1" ? [explain_path, {params: {query: query["query"]}}] : [explain_path(query: query["query"]), {}] %>
|
@@ -1,5 +1,33 @@
|
|
1
1
|
<div class="content">
|
2
2
|
<h1>Connections</h1>
|
3
3
|
|
4
|
-
|
4
|
+
<p><%= pluralize(@total_connections, "connection") %></p>
|
5
|
+
|
6
|
+
<h3>By Database</h3>
|
7
|
+
|
8
|
+
<% top_connections = Hash.new(0) %>
|
9
|
+
<% @connection_sources.each do |source| %>
|
10
|
+
<% top_connections[source["database"]] += source["total_connections"].to_i %>
|
11
|
+
<% end %>
|
12
|
+
<% top_connections = top_connections.sort_by { |k, v| [-v, k] } %>
|
13
|
+
|
14
|
+
<div id="chart-1" class="chart" style="height: 260px; line-height: 260px; margin-bottom: 20px;">Loading...</div>
|
15
|
+
<script>
|
16
|
+
new Chartkick.PieChart("chart-1", <%= json_escape(top_connections.to_json).html_safe %>);
|
17
|
+
</script>
|
18
|
+
|
19
|
+
<h3>By User</h3>
|
20
|
+
|
21
|
+
<% top_connections = Hash.new(0) %>
|
22
|
+
<% @connection_sources.each do |source| %>
|
23
|
+
<% top_connections[source["user"]] += source["total_connections"].to_i %>
|
24
|
+
<% end %>
|
25
|
+
<% top_connections = top_connections.sort_by { |k, v| [-v, k] } %>
|
26
|
+
|
27
|
+
<div id="chart-2" class="chart" style="height: 260px; line-height: 260px; margin-bottom: 20px;">Loading...</div>
|
28
|
+
<script>
|
29
|
+
new Chartkick.PieChart("chart-2", <%= json_escape(top_connections.to_json).html_safe %>);
|
30
|
+
</script>
|
31
|
+
|
32
|
+
<%= render partial: "connections_table", locals: {connection_sources: @connection_sources} %>
|
5
33
|
</div>
|
@@ -23,5 +23,7 @@
|
|
23
23
|
<% end %>
|
24
24
|
<% elsif @error %>
|
25
25
|
<div class="alert alert-danger"><%= @error %></div>
|
26
|
+
<% else %>
|
27
|
+
<p class="text-muted">Note: The analyze and visualize buttons can add load to your database for long running queries, so be careful.</p>
|
26
28
|
<% end %>
|
27
29
|
</div>
|
@@ -15,6 +15,9 @@
|
|
15
15
|
<% else %>
|
16
16
|
No long running queries
|
17
17
|
<% end %>
|
18
|
+
<% if @autovacuum_queries.any? %>
|
19
|
+
<span class="tiny"><%= @autovacuum_queries.size %> autovacuum</span>
|
20
|
+
<% end %>
|
18
21
|
</div>
|
19
22
|
<% if @extended %>
|
20
23
|
<div class="alert alert-<%= @good_cache_rate ? "success" : "warning" %>">
|
@@ -81,6 +84,15 @@
|
|
81
84
|
No slow queries
|
82
85
|
<% end %>
|
83
86
|
</div>
|
87
|
+
<% if @extended %>
|
88
|
+
<div class="alert alert-<%= @unused_indexes.empty? ? "success" : "warning" %>">
|
89
|
+
<% if @unused_indexes.any? %>
|
90
|
+
<%= pluralize(@unused_indexes.size, "unused index", "unused indexes") %>
|
91
|
+
<% else %>
|
92
|
+
No unused indexes
|
93
|
+
<% end %>
|
94
|
+
</div>
|
95
|
+
<% end %>
|
84
96
|
</div>
|
85
97
|
|
86
98
|
<% if @replica && !@good_replication_lag %>
|
@@ -120,7 +132,11 @@
|
|
120
132
|
<% if !@good_total_connections %>
|
121
133
|
<div class="content">
|
122
134
|
<h1>High Number of Connections</h1>
|
123
|
-
|
135
|
+
<p><%= pluralize(@total_connections, "connection") %></p>
|
136
|
+
|
137
|
+
<p><%= link_to "Use connection pooling", "http://www.craigkerstiens.com/2014/05/22/on-connection-pooling/", target: "_blank" %> for better performance. <%= link_to "PgBouncer", "https://wiki.postgresql.org/wiki/PgBouncer", target: "_blank" %> is a solid option.</p>
|
138
|
+
|
139
|
+
<%= render partial: "connections_table", locals: {connection_sources: @database.connection_sources(by_database_and_user: true).first(10)} %>
|
124
140
|
</div>
|
125
141
|
<% end %>
|
126
142
|
|
@@ -310,3 +326,41 @@ pg_stat_statements.track = all</pre>
|
|
310
326
|
<%= render partial: "queries_table", locals: {queries: @slow_queries} %>
|
311
327
|
</div>
|
312
328
|
<% end %>
|
329
|
+
|
330
|
+
<% if @extended && @unused_indexes.any? %>
|
331
|
+
<div class="content">
|
332
|
+
<h1>Unused Indexes</h1>
|
333
|
+
|
334
|
+
<p>
|
335
|
+
Unused indexes cause unnecessary overhead. Remove them
|
336
|
+
<% if @show_migrations %>
|
337
|
+
<a href="javascript: void(0);" onclick="document.getElementById('migration').style.display = 'block';">with a migration</a>
|
338
|
+
<% end %>
|
339
|
+
for faster writes.
|
340
|
+
</p>
|
341
|
+
|
342
|
+
<div id="migration" style="display: none;">
|
343
|
+
<pre>rails g migration remove_unused_indexes</pre>
|
344
|
+
<p>And paste</p>
|
345
|
+
<pre style="overflow: scroll; white-space: pre; word-break: normal;"><% @unused_indexes.each do |query| %>
|
346
|
+
remove_index <%= query["table"].to_sym.inspect %>, name: <%= query["index"].to_s.inspect %><% end %></pre>
|
347
|
+
</div>
|
348
|
+
|
349
|
+
<table class="table">
|
350
|
+
<thead>
|
351
|
+
<tr>
|
352
|
+
<th>Name</th>
|
353
|
+
<th style="width: 20%;">Index Size</th>
|
354
|
+
</tr>
|
355
|
+
</thead>
|
356
|
+
<tbody>
|
357
|
+
<% @unused_indexes.each do |query| %>
|
358
|
+
<tr>
|
359
|
+
<td><%= query["index"] %><div class="text-muted">on <%= query["table"] %></div></td>
|
360
|
+
<td><%= query["index_size"] %></td>
|
361
|
+
</tr>
|
362
|
+
<% end %>
|
363
|
+
</tbody>
|
364
|
+
</table>
|
365
|
+
</div>
|
366
|
+
<% end %>
|
@@ -1,9 +1,11 @@
|
|
1
1
|
<div class="content">
|
2
2
|
<h1>Live Queries</h1>
|
3
3
|
|
4
|
+
<p><%= pluralize(@running_queries.size, "query") %></p>
|
5
|
+
|
4
6
|
<%= render partial: "live_queries_table", locals: {queries: @running_queries} %>
|
5
7
|
|
6
8
|
<p><%= button_to "Kill all connections", kill_all_path, class: "btn btn-danger" %></p>
|
7
9
|
|
8
|
-
<p class="text-muted">You may need to restart your
|
10
|
+
<p class="text-muted">You may need to restart your app server afterwards.</p>
|
9
11
|
</div>
|
@@ -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;"
|
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;"
|
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;"
|
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;"
|
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/guides/Rails.md
CHANGED
@@ -38,7 +38,7 @@ ENV["PGHERO_PASSWORD"] = "hyrule"
|
|
38
38
|
#### Devise
|
39
39
|
|
40
40
|
```ruby
|
41
|
-
authenticate :user,
|
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
|
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:
|
@@ -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
|
@@ -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)"
|
data/lib/pghero.rb
CHANGED
@@ -65,10 +65,12 @@ module PgHero
|
|
65
65
|
Thread.current[:pghero_config] ||= begin
|
66
66
|
path = "config/pghero.yml"
|
67
67
|
|
68
|
-
config =
|
69
|
-
|
68
|
+
config = YAML.load(ERB.new(File.read(path)).result) if File.exist?(path)
|
69
|
+
config ||= {}
|
70
70
|
|
71
|
-
if config
|
71
|
+
if config[env]
|
72
|
+
config[env]
|
73
|
+
elsif config["databases"] # preferred format
|
72
74
|
config
|
73
75
|
else
|
74
76
|
{
|
data/lib/pghero/database.rb
CHANGED
@@ -8,7 +8,10 @@ module PgHero
|
|
8
8
|
|
9
9
|
# use transaction for safety
|
10
10
|
connection_model.transaction do
|
11
|
-
|
11
|
+
# protect the DB with a 10 second timeout
|
12
|
+
# this could potentially increase the timeout, but 10 seconds should be okay
|
13
|
+
select_all("SET LOCAL statement_timeout = 10000")
|
14
|
+
if (sql.sub(/;\z/, "").include?(";") || sql.upcase.include?("COMMIT")) && !explain_safe
|
12
15
|
raise ActiveRecord::StatementInvalid, "Unsafe statement"
|
13
16
|
end
|
14
17
|
explanation = select_all("EXPLAIN #{sql}").map { |v| v["QUERY PLAN"] }.join("\n")
|
@@ -12,6 +12,7 @@ module PgHero
|
|
12
12
|
#{server_version_num >= 90600 ? "(wait_event IS NOT NULL) AS waiting" : "waiting"},
|
13
13
|
query,
|
14
14
|
COALESCE(query_start, xact_start) AS started_at,
|
15
|
+
EXTRACT(EPOCH FROM NOW() - COALESCE(query_start, xact_start)) * 1000.0 AS duration_ms,
|
15
16
|
usename AS user
|
16
17
|
FROM
|
17
18
|
pg_stat_activity
|
@@ -30,11 +31,6 @@ module PgHero
|
|
30
31
|
running_queries(min_duration: long_running_query_sec)
|
31
32
|
end
|
32
33
|
|
33
|
-
def slow_queries(options = {})
|
34
|
-
query_stats = options[:query_stats] || self.query_stats(options.except(:query_stats))
|
35
|
-
query_stats.select { |q| q["calls"].to_i >= slow_query_calls.to_i && q["average_time"].to_i >= slow_query_ms.to_i }
|
36
|
-
end
|
37
|
-
|
38
34
|
def locks
|
39
35
|
select_all <<-SQL
|
40
36
|
SELECT DISTINCT ON (pid)
|
@@ -66,6 +66,8 @@ module PgHero
|
|
66
66
|
# http://stackoverflow.com/questions/20582500/how-to-check-if-a-table-exists-in-a-given-schema
|
67
67
|
def historical_query_stats_enabled?
|
68
68
|
# TODO use schema from config
|
69
|
+
# make sure primary database is PostgreSQL first
|
70
|
+
["PostgreSQL", "PostGIS"].include?(stats_connection.adapter_name) &&
|
69
71
|
PgHero.truthy?(stats_connection.select_all(squish <<-SQL
|
70
72
|
SELECT EXISTS (
|
71
73
|
SELECT
|
@@ -148,6 +150,11 @@ module PgHero
|
|
148
150
|
end
|
149
151
|
end
|
150
152
|
|
153
|
+
def slow_queries(options = {})
|
154
|
+
query_stats = options[:query_stats] || self.query_stats(options.except(:query_stats))
|
155
|
+
query_stats.select { |q| q["calls"].to_i >= slow_query_calls.to_i && q["average_time"].to_i >= slow_query_ms.to_i }
|
156
|
+
end
|
157
|
+
|
151
158
|
private
|
152
159
|
|
153
160
|
def stats_connection
|
data/lib/pghero/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pghero
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.6.
|
4
|
+
version: 1.6.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew Kane
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2017-02-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -153,8 +153,10 @@ files:
|
|
153
153
|
- guides/Query-Stats.md
|
154
154
|
- guides/Rails.md
|
155
155
|
- guides/Suggested-Indexes.md
|
156
|
+
- lib/generators/pghero/config_generator.rb
|
156
157
|
- lib/generators/pghero/query_stats_generator.rb
|
157
158
|
- lib/generators/pghero/space_stats_generator.rb
|
159
|
+
- lib/generators/pghero/templates/config.yml
|
158
160
|
- lib/generators/pghero/templates/query_stats.rb
|
159
161
|
- lib/generators/pghero/templates/space_stats.rb
|
160
162
|
- lib/pghero.rb
|
@@ -206,7 +208,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
206
208
|
version: '0'
|
207
209
|
requirements: []
|
208
210
|
rubyforge_project:
|
209
|
-
rubygems_version: 2.
|
211
|
+
rubygems_version: 2.6.8
|
210
212
|
signing_key:
|
211
213
|
specification_version: 4
|
212
214
|
summary: A performance dashboard for Postgres
|