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 +4 -4
- data/CHANGELOG.md +7 -0
- data/app/controllers/pg_hero/home_controller.rb +13 -10
- data/app/views/pg_hero/home/_live_queries_table.html.erb +7 -1
- data/app/views/pg_hero/home/index.html.erb +52 -9
- data/app/views/pg_hero/home/live_queries.html.erb +1 -1
- data/lib/pghero/methods/basic.rb +15 -3
- data/lib/pghero/methods/maintenance.rb +16 -0
- data/lib/pghero/methods/replication.rb +18 -16
- data/lib/pghero/methods/sequences.rb +54 -6
- data/lib/pghero/version.rb +1 -1
- data/lib/pghero.rb +1 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f45d1e2c61994223e2835c0d8fdbefea6a9c0b36
|
4
|
+
data.tar.gz: 74227dbc52f80e9b1cb150f601a8ec9705ac56b3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
40
|
-
|
41
|
-
|
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
|
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 @
|
5
|
-
|
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
|
-
|
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
|
52
|
-
|
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 <user> 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
|
|
data/lib/pghero/methods/basic.rb
CHANGED
@@ -35,9 +35,21 @@ module PgHero
|
|
35
35
|
def select_all(sql, conn = nil)
|
36
36
|
conn ||= connection
|
37
37
|
# squish for logs
|
38
|
-
|
39
|
-
|
40
|
-
|
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
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
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
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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]
|
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
|
-
|
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
|
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
|
52
|
+
def unquote_ident(value)
|
50
53
|
schema, seq = value.split(".")
|
51
54
|
unless seq
|
52
55
|
seq = schema
|
53
|
-
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
|
data/lib/pghero/version.rb
CHANGED
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
|
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
|
+
date: 2017-12-01 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|