pghero 2.0.8 → 2.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 553643782ed72cb0f44eb8e1f44efc239ace1075
4
- data.tar.gz: 7defcc2212776fa9a45a45c206f8d7b5461a88c0
3
+ metadata.gz: f45d1e2c61994223e2835c0d8fdbefea6a9c0b36
4
+ data.tar.gz: 74227dbc52f80e9b1cb150f601a8ec9705ac56b3
5
5
  SHA512:
6
- metadata.gz: 826e25c291d209bb7abd7b5f07ba99576136c6fccdfd60c6ddb075df038779b219958ac31edc50fbd316179a1ef45fc6a98ac284b31d804d7bebae60c07e1128
7
- data.tar.gz: '0885bb30490a0003edabc433638c6de367ae312c603ed35178da12e1ac8fa497ef4e6d46c19c9c8640b07bdf062a1f755fe297166d7c4d3395c6fca8d8eb94e9'
6
+ metadata.gz: f96125273c33eb6b632894c927118c2e308a62829df66128ca3a917dd578eafae7dd77308fd832142ddb54d61e748d753ee06da83f5393d3417d447223a208cd
7
+ data.tar.gz: 777fc575408694f43f3a8de1273341c5be02e3288a63424e5150a13c5c4403fbc547c84b5172cdb23dc9720dfc3b50b8acf3d574dad399a569c43035df435984
data/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ ## 2.1.0
2
+
3
+ - Fixed issue with sequences in different schema than table
4
+ - No longer throw errors for unreadable sequences
5
+ - Fixed replication lag for Amazon Aurora
6
+ - Added `vacuum_progress` method
7
+
1
8
  ## 2.0.8
2
9
 
3
10
  - Added support for Postgres 10 replicas
@@ -11,11 +11,13 @@ module PgHero
11
11
  before_action :set_database
12
12
  before_action :set_query_stats_enabled
13
13
  before_action :set_show_details, only: [:index, :queries, :show_query]
14
+ before_action :ensure_query_stats, only: [:queries]
14
15
  else
15
16
  # no need to check API in earlier versions
16
17
  before_filter :set_database
17
18
  before_filter :set_query_stats_enabled
18
19
  before_filter :set_show_details, only: [:index, :queries, :show_query]
20
+ before_filter :ensure_query_stats, only: [:queries]
19
21
  end
20
22
 
21
23
  def index
@@ -24,7 +26,7 @@ module PgHero
24
26
 
25
27
  if @replica
26
28
  @replication_lag = @database.replication_lag
27
- @good_replication_lag = @replication_lag < 5
29
+ @good_replication_lag = @replication_lag ? @replication_lag < 5 : true
28
30
  else
29
31
  @inactive_replication_slots = @database.replication_slots.select { |r| !r[:active] }
30
32
  end
@@ -36,15 +38,9 @@ module PgHero
36
38
 
37
39
  @transaction_id_danger = @database.transaction_id_danger(threshold: 1500000000)
38
40
 
39
- begin
40
- @sequence_danger = @database.sequence_danger(threshold: (params[:sequence_threshold] || 0.9).to_f)
41
- rescue ActiveRecord::StatementInvalid => e
42
- if (m = /permission denied for [^:]+/.match(e.message))
43
- @sequence_danger_error = m[0]
44
- else
45
- raise
46
- end
47
- end
41
+ @readable_sequences, @unreadable_sequences = @database.sequences.partition { |s| s[:readable] }
42
+
43
+ @sequence_danger = @database.sequence_danger(threshold: (params[:sequence_threshold] || 0.9).to_f, sequences: @readable_sequences)
48
44
 
49
45
  @indexes = @database.indexes
50
46
  @invalid_indexes = @indexes.select { |i| !i[:valid] }
@@ -112,6 +108,7 @@ module PgHero
112
108
  def live_queries
113
109
  @title = "Live Queries"
114
110
  @running_queries = @database.running_queries(all: true)
111
+ @vacuum_progress = @database.vacuum_progress.index_by { |q| q[:pid] }
115
112
  end
116
113
 
117
114
  def queries
@@ -385,5 +382,11 @@ module PgHero
385
382
  render text: message
386
383
  end
387
384
  end
385
+
386
+ def ensure_query_stats
387
+ unless @query_stats_enabled
388
+ redirect_to root_path, alert: "Query stats not enabled"
389
+ end
390
+ end
388
391
  end
389
392
  end
@@ -12,7 +12,13 @@
12
12
  <tr>
13
13
  <td><%= query[:pid] %></td>
14
14
  <td><%= number_with_delimiter(query[:duration_ms].round) %> ms</td>
15
- <td><%= query[:state] %></td>
15
+ <td>
16
+ <%= query[:state] %>
17
+ <% if vacuum_progress[query[:pid]] %>
18
+ <br />
19
+ <strong><%= vacuum_progress[query[:pid]][:phase] %></strong>
20
+ <% end %>
21
+ </td>
16
22
  <td class="text-right">
17
23
  <% button_path, button_options = Rails.version >= "4.1" ? [explain_path, {params: {query: query[:query]}}] : [explain_path(query: query[:query]), {}] %>
18
24
  <%= button_to "Explain", button_path, button_options.merge(form: {target: "_blank"}, class: "btn btn-info") %>
@@ -1,12 +1,16 @@
1
1
  <div id="status">
2
2
  <% if @replica %>
3
3
  <div class="alert alert-<%= @good_replication_lag ? "success" : "warning" %>">
4
- <% if @good_replication_lag %>
5
- Healthy replication lag
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>
6
11
  <% else %>
7
- High replication lag
12
+ Replication lag not supported
8
13
  <% end %>
9
- <span class="tiny"><%= number_with_delimiter((@replication_lag * 1000).round) %> ms</span>
10
14
  </div>
11
15
  <% elsif @inactive_replication_slots.any? %>
12
16
  <div class="alert alert-warning">
@@ -48,13 +52,14 @@
48
52
  <% end %>
49
53
  </div>
50
54
  <div class="alert alert-<%= @sequence_danger && @sequence_danger.empty? ? "success" : "warning" %>">
51
- <% if !@sequence_danger %>
52
- Error checking for columns near integer overflow: <%= @sequence_danger_error %>
53
- <% elsif @sequence_danger.any? %>
54
- <%= pluralize(@sequence_danger.size, "columns") %> approaching overflow
55
+ <% if @sequence_danger.any? %>
56
+ <%= pluralize(@sequence_danger.size, "column") %> approaching overflow
55
57
  <% else %>
56
58
  No columns near integer overflow
57
59
  <% end %>
60
+ <% if @unreadable_sequences.any? %>
61
+ (<%= link_to pluralize(@unreadable_sequences.size, "unreadable sequence", "unreadable sequences"), {unreadable: "t"} %>)
62
+ <% end %>
58
63
  </div>
59
64
  <div class="alert alert-<%= @invalid_indexes.empty? ? "success" : "warning" %>">
60
65
  <% if @invalid_indexes.any? %>
@@ -101,6 +106,41 @@
101
106
  <% end %>
102
107
  </div>
103
108
 
109
+ <% if params[:unreadable] && @unreadable_sequences.any? %>
110
+ <div class="content">
111
+ <h1>Unreadable Sequences</h1>
112
+
113
+ <p>This is likely due to missing privileges. Make sure your user has the SELECT privilege for each sequence.</p>
114
+
115
+ <table class="table">
116
+ <thead>
117
+ <tr>
118
+ <th style="width: 33%;">Column</th>
119
+ <th>Sequence</th>
120
+ </tr>
121
+ </thead>
122
+ <tbody>
123
+ <% @unreadable_sequences.each do |sequence| %>
124
+ <tr>
125
+ <td>
126
+ <%= sequence[:table] %>.<%= sequence[:column] %>
127
+ <% if sequence[:table_schema] != "public" %>
128
+ <span class="text-muted"><%= sequence[:table_schema] %></span>
129
+ <% end %>
130
+ </td>
131
+ <td>
132
+ <%= sequence[:sequence] %>
133
+ <% if sequence[:schema] && sequence[:schema] != "public" %>
134
+ <span class="text-muted"><%= sequence[:schema] %></span>
135
+ <% end %>
136
+ </td>
137
+ </tr>
138
+ <% end %>
139
+ </tbody>
140
+ </table>
141
+ </div>
142
+ <% end %>
143
+
104
144
  <% if @replica && !@good_replication_lag %>
105
145
  <div class="content">
106
146
  <h1>High Replication Lag</h1>
@@ -141,7 +181,7 @@
141
181
 
142
182
  <pre><code>ALTER ROLE &lt;user&gt; SET statement_timeout TO '60s';</code></pre>
143
183
 
144
- <%= render partial: "live_queries_table", locals: {queries: @long_running_queries} %>
184
+ <%= render partial: "live_queries_table", locals: {queries: @long_running_queries, vacuum_progress: {}} %>
145
185
  </div>
146
186
  <% end %>
147
187
 
@@ -224,6 +264,9 @@
224
264
  <tr>
225
265
  <td>
226
266
  <%= query[:table] %>.<%= query[:column] %>
267
+ <% if query[:table_schema] != "public" %>
268
+ <span class="text-muted"><%= query[:table_schema] %></span>
269
+ <% end %>
227
270
  </td>
228
271
  <td>
229
272
  <%= query[:column_type] %>
@@ -3,7 +3,7 @@
3
3
 
4
4
  <p><%= pluralize(@running_queries.size, "query") %></p>
5
5
 
6
- <%= render partial: "live_queries_table", locals: {queries: @running_queries} %>
6
+ <%= render partial: "live_queries_table", locals: {queries: @running_queries, vacuum_progress: @vacuum_progress} %>
7
7
 
8
8
  <p><%= button_to "Kill all connections", kill_all_path, class: "btn btn-danger" %></p>
9
9
 
@@ -35,9 +35,21 @@ module PgHero
35
35
  def select_all(sql, conn = nil)
36
36
  conn ||= connection
37
37
  # squish for logs
38
- result = conn.select_all(squish(sql))
39
- cast_method = ActiveRecord::VERSION::MAJOR < 5 ? :type_cast : :cast_value
40
- result.map { |row| Hash[row.map { |col, val| [col.to_sym, result.column_types[col].send(cast_method, val)] }] }
38
+ retries = 0
39
+ begin
40
+ result = conn.select_all(squish(sql))
41
+ cast_method = ActiveRecord::VERSION::MAJOR < 5 ? :type_cast : :cast_value
42
+ result.map { |row| Hash[row.map { |col, val| [col.to_sym, result.column_types[col].send(cast_method, val)] }] }
43
+ rescue ActiveRecord::StatementInvalid => e
44
+ # fix for random internal errors
45
+ if e.message.include?("PG::InternalError") && retries < 2
46
+ retries += 1
47
+ sleep(0.1)
48
+ retry
49
+ else
50
+ raise e
51
+ end
52
+ end
41
53
  end
42
54
 
43
55
  def select_all_stats(sql)
@@ -33,6 +33,22 @@ module PgHero
33
33
  transaction_id_danger(threshold: 2000000, max_value: max_value)
34
34
  end
35
35
 
36
+ def vacuum_progress
37
+ if server_version_num >= 90600
38
+ select_all <<-SQL
39
+ SELECT
40
+ pid,
41
+ phase
42
+ FROM
43
+ pg_stat_progress_vacuum
44
+ WHERE
45
+ datname = current_database()
46
+ SQL
47
+ else
48
+ []
49
+ end
50
+ end
51
+
36
52
  def maintenance_info
37
53
  select_all <<-SQL
38
54
  SELECT
@@ -9,22 +9,24 @@ module PgHero
9
9
  end
10
10
 
11
11
  # http://www.postgresql.org/message-id/CADKbJJWz9M0swPT3oqe8f9+tfD4-F54uE6Xtkh4nERpVsQnjnw@mail.gmail.com
12
- def replication_lag
13
- lag_condition =
14
- if server_version_num >= 100000
15
- "pg_last_wal_receive_lsn() = pg_last_wal_replay_lsn()"
16
- else
17
- "pg_last_xlog_receive_location() = pg_last_xlog_replay_location()"
18
- end
12
+ def replication_lag
13
+ with_feature_support do
14
+ lag_condition =
15
+ if server_version_num >= 100000
16
+ "pg_last_wal_receive_lsn() = pg_last_wal_replay_lsn()"
17
+ else
18
+ "pg_last_xlog_receive_location() = pg_last_xlog_replay_location()"
19
+ end
19
20
 
20
- select_one <<-SQL
21
- SELECT
22
- CASE
23
- WHEN NOT pg_is_in_recovery() OR #{lag_condition} THEN 0
24
- ELSE EXTRACT (EPOCH FROM NOW() - pg_last_xact_replay_timestamp())
25
- END
26
- AS replication_lag
27
- SQL
21
+ select_one <<-SQL
22
+ SELECT
23
+ CASE
24
+ WHEN NOT pg_is_in_recovery() OR #{lag_condition} THEN 0
25
+ ELSE EXTRACT (EPOCH FROM NOW() - pg_last_xact_replay_timestamp())
26
+ END
27
+ AS replication_lag
28
+ SQL
29
+ end
28
30
  end
29
31
 
30
32
  def replication_slots
@@ -51,7 +53,7 @@ module PgHero
51
53
 
52
54
  private
53
55
 
54
- def with_feature_support(default)
56
+ def with_feature_support(default = nil)
55
57
  begin
56
58
  yield
57
59
  rescue ActiveRecord::StatementInvalid => e
@@ -29,31 +29,79 @@ module PgHero
29
29
  # parse out sequence
30
30
  sequences.each do |column|
31
31
  m = /^nextval\('(.+)'\:\:regclass\)$/.match(column.delete(:default_value))
32
- column[:schema], column[:sequence] = unquote_ident(m[1], column[:table_schema])
32
+ column[:schema], column[:sequence] = unquote_ident(m[1])
33
33
  column[:max_value] = column[:column_type] == 'integer' ? 2147483647 : 9223372036854775807
34
34
  end
35
35
 
36
- select_all(sequences.map { |s| "SELECT last_value FROM #{quote_ident(s[:schema])}.#{quote_ident(s[:sequence])}" }.join(" UNION ALL ")).each_with_index do |row, i|
36
+ add_sequence_attributes(sequences)
37
+
38
+ select_all(sequences.select { |s| s[:readable] }.map { |s| "SELECT last_value FROM #{quote_ident(s[:schema])}.#{quote_ident(s[:sequence])}" }.join(" UNION ALL ")).each_with_index do |row, i|
37
39
  sequences[i][:last_value] = row[:last_value]
38
40
  end
39
41
 
40
42
  sequences.sort_by { |s| s[:sequence] }
41
43
  end
42
44
 
43
- def sequence_danger(threshold: 0.9)
44
- sequences.select { |s| s[:last_value] / s[:max_value].to_f > threshold }.sort_by { |s| s[:max_value] - s[:last_value] }
45
+ def sequence_danger(threshold: 0.9, sequences: nil)
46
+ sequences ||= self.sequences
47
+ sequences.select { |s| s[:last_value] && s[:last_value] / s[:max_value].to_f > threshold }.sort_by { |s| s[:max_value] - s[:last_value] }
45
48
  end
46
49
 
47
50
  private
48
51
 
49
- def unquote_ident(value, default_schema)
52
+ def unquote_ident(value)
50
53
  schema, seq = value.split(".")
51
54
  unless seq
52
55
  seq = schema
53
- schema = default_schema
56
+ schema = nil
54
57
  end
55
58
  [unquote(schema), unquote(seq)]
56
59
  end
60
+
61
+ # adds readable attribute to all sequences
62
+ # also adds schema if missing
63
+ def add_sequence_attributes(sequences)
64
+ # fetch data
65
+ sequence_attributes = select_all <<-SQL
66
+ SELECT
67
+ n.nspname AS schema,
68
+ c.relname AS sequence,
69
+ has_sequence_privilege(c.oid, 'SELECT') AS readable
70
+ FROM
71
+ pg_class c
72
+ INNER JOIN
73
+ pg_catalog.pg_namespace n ON n.oid = c.relnamespace
74
+ WHERE
75
+ c.relkind = 'S'
76
+ AND n.nspname NOT IN ('pg_catalog', 'information_schema')
77
+ SQL
78
+
79
+ # first populate missing schemas
80
+ missing_schema = sequences.select { |s| s[:schema].nil? }
81
+ if missing_schema.any?
82
+ sequence_schemas = sequence_attributes.group_by { |s| s[:sequence] }
83
+
84
+ missing_schema.each do |sequence|
85
+ schemas = sequence_schemas[sequence[:sequence]] || []
86
+
87
+ case schemas.size
88
+ when 0
89
+ # do nothing, will be marked as unreadable
90
+ when 1
91
+ # bingo
92
+ sequence[:schema] = schemas[0][:schema]
93
+ else
94
+ raise PgHero::Error, "Same sequence name in multiple schemas: #{sequence[:sequence]}"
95
+ end
96
+ end
97
+ end
98
+
99
+ # then populate attributes
100
+ readable = Hash[sequence_attributes.map { |s| [[s[:schema], s[:sequence]], s[:readable]] }]
101
+ sequences.each do |sequence|
102
+ sequence[:readable] = readable[[sequence[:schema], sequence[:sequence]]] || false
103
+ end
104
+ end
57
105
  end
58
106
  end
59
107
  end
@@ -1,3 +1,3 @@
1
1
  module PgHero
2
- VERSION = "2.0.8"
2
+ VERSION = "2.1.0"
3
3
  end
data/lib/pghero.rb CHANGED
@@ -109,6 +109,7 @@ module PgHero
109
109
 
110
110
  def capture_query_stats(verbose: false)
111
111
  each_database do |database|
112
+ next unless database.capture_query_stats?
112
113
  puts "Capturing query stats for #{database.id}..." if verbose
113
114
  database.capture_query_stats(raise_errors: true)
114
115
  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: 2.0.8
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kane
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-11-13 00:00:00.000000000 Z
11
+ date: 2017-12-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord