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.

@@ -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><%= query["started_at"] ? "#{number_with_delimiter(((now - Time.parse(query["started_at"])) * 1000).round)} ms" : nil %></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
- <%= render partial: "connections_table", locals: {total_connections: @total_connections, connection_sources: @connection_sources, show_message: false} %>
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
- <%= render partial: "connections_table", locals: {total_connections: @total_connections, connection_sources: @database.connection_sources(by_database_and_user: true).first(10), show_message: true} %>
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 Rails server afterwards.</p>
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;"><%= 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/guides/Rails.md CHANGED
@@ -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:
@@ -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
- (YAML.load(ERB.new(File.read(path)).result)[env] if File.exist?(path))
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
  {
@@ -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
@@ -8,7 +8,10 @@ module PgHero
8
8
 
9
9
  # use transaction for safety
10
10
  connection_model.transaction do
11
- if !explain_safe && (sql.sub(/;\z/, "").include?(";") || sql.upcase.include?("COMMIT"))
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
@@ -1,3 +1,3 @@
1
1
  module PgHero
2
- VERSION = "1.6.2"
2
+ VERSION = "1.6.3"
3
3
  end
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.2
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: 2016-10-27 00:00:00.000000000 Z
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.5.1
211
+ rubygems_version: 2.6.8
210
212
  signing_key:
211
213
  specification_version: 4
212
214
  summary: A performance dashboard for Postgres