pghero 2.8.3 → 3.3.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +47 -0
- data/LICENSE.txt +1 -1
- data/app/assets/javascripts/pghero/Chart.bundle.js +23379 -19766
- data/app/assets/javascripts/pghero/application.js +13 -12
- data/app/assets/javascripts/pghero/chartkick.js +834 -764
- data/app/assets/javascripts/pghero/highlight.min.js +440 -0
- data/app/assets/javascripts/pghero/jquery.js +318 -197
- data/app/assets/javascripts/pghero/nouislider.js +676 -1066
- data/app/assets/stylesheets/pghero/application.css +8 -2
- data/app/assets/stylesheets/pghero/nouislider.css +4 -10
- data/app/controllers/pg_hero/home_controller.rb +103 -37
- data/app/helpers/pg_hero/home_helper.rb +2 -2
- data/app/views/layouts/pg_hero/application.html.erb +4 -2
- data/app/views/pg_hero/home/_query_stats_slider.html.erb +6 -6
- data/app/views/pg_hero/home/connections.html.erb +6 -6
- data/app/views/pg_hero/home/explain.html.erb +4 -2
- data/app/views/pg_hero/home/index.html.erb +3 -1
- data/app/views/pg_hero/home/queries.html.erb +4 -2
- data/app/views/pg_hero/home/relation_space.html.erb +1 -1
- data/app/views/pg_hero/home/show_query.html.erb +17 -13
- data/app/views/pg_hero/home/space.html.erb +44 -40
- data/app/views/pg_hero/home/system.html.erb +6 -6
- data/lib/generators/pghero/query_stats_generator.rb +1 -0
- data/lib/generators/pghero/space_stats_generator.rb +1 -0
- data/lib/generators/pghero/templates/config.yml.tt +6 -0
- data/lib/pghero/database.rb +0 -7
- data/lib/pghero/engine.rb +1 -1
- data/lib/pghero/methods/basic.rb +6 -9
- data/lib/pghero/methods/connections.rb +5 -5
- data/lib/pghero/methods/constraints.rb +1 -1
- data/lib/pghero/methods/explain.rb +34 -0
- data/lib/pghero/methods/indexes.rb +8 -8
- data/lib/pghero/methods/kill.rb +1 -1
- data/lib/pghero/methods/maintenance.rb +4 -4
- data/lib/pghero/methods/queries.rb +2 -2
- data/lib/pghero/methods/query_stats.rb +28 -29
- data/lib/pghero/methods/replication.rb +2 -2
- data/lib/pghero/methods/sequences.rb +3 -3
- data/lib/pghero/methods/settings.rb +1 -1
- data/lib/pghero/methods/space.rb +20 -14
- data/lib/pghero/methods/suggested_indexes.rb +40 -110
- data/lib/pghero/methods/system.rb +16 -16
- data/lib/pghero/methods/tables.rb +4 -5
- data/lib/pghero/methods/users.rb +12 -4
- data/lib/pghero/version.rb +1 -1
- data/lib/pghero.rb +90 -65
- data/lib/tasks/pghero.rake +11 -1
- data/licenses/LICENSE-chart.js.txt +1 -1
- data/licenses/LICENSE-date-fns.txt +21 -20
- data/licenses/LICENSE-kurkle-color.txt +9 -0
- metadata +9 -8
- data/app/assets/javascripts/pghero/highlight.pack.js +0 -2
@@ -1,34 +1,34 @@
|
|
1
1
|
<div class="content">
|
2
2
|
<p id="periods">
|
3
3
|
<% @periods.each do |name, options| %>
|
4
|
-
<%= link_to name, system_path(options) %>
|
4
|
+
<%= link_to name, system_path(params: options) %>
|
5
5
|
<% end %>
|
6
6
|
</p>
|
7
|
-
<% path_options = {duration: @duration, period: @period} %>
|
7
|
+
<% path_options = {params: {duration: @duration, period: @period}} %>
|
8
8
|
|
9
9
|
<h1>CPU</h1>
|
10
10
|
<div id="chart-1" class="chart" style="margin-bottom: 20px;">Loading...</div>
|
11
11
|
<script>
|
12
|
-
new Chartkick.LineChart("chart-1", <%=
|
12
|
+
new Chartkick.LineChart("chart-1", <%= pghero_js_value(cpu_usage_path(path_options)) %>, {max: 100, colors: ["#5bc0de"], suffix: "%", library: {plugins: {tooltip: {intersect: false, mode: "index"}}}})
|
13
13
|
</script>
|
14
14
|
|
15
15
|
<h1>Load</h1>
|
16
16
|
<div id="chart-2" class="chart" style="margin-bottom: 20px;">Loading...</div>
|
17
17
|
<script>
|
18
|
-
new Chartkick.LineChart("chart-2", <%=
|
18
|
+
new Chartkick.LineChart("chart-2", <%= pghero_js_value(load_stats_path(path_options)) %>, {colors: ["#5bc0de", "#d9534f"], library: {plugins: {tooltip: {intersect: false, mode: "nearest"}}}})
|
19
19
|
</script>
|
20
20
|
|
21
21
|
<h1>Connections</h1>
|
22
22
|
<div id="chart-3" class="chart" style="margin-bottom: 20px;">Loading...</div>
|
23
23
|
<script>
|
24
|
-
new Chartkick.LineChart("chart-3", <%=
|
24
|
+
new Chartkick.LineChart("chart-3", <%= pghero_js_value(connection_stats_path(path_options)) %>, {colors: ["#5bc0de"], library: {plugins: {tooltip: {intersect: false, mode: "index"}}}})
|
25
25
|
</script>
|
26
26
|
|
27
27
|
<% if @database.replica? %>
|
28
28
|
<h1>Replication Lag</h1>
|
29
29
|
<div id="chart-4" class="chart" style="margin-bottom: 20px;">Loading...</div>
|
30
30
|
<script>
|
31
|
-
new Chartkick.LineChart("chart-4", <%=
|
31
|
+
new Chartkick.LineChart("chart-4", <%= pghero_js_value(replication_lag_stats_path(path_options)) %>, {colors: ["#5bc0de"], library: {plugins: {tooltip: {intersect: false, mode: "index"}}}})
|
32
32
|
</script>
|
33
33
|
<% end %>
|
34
34
|
</div>
|
@@ -24,9 +24,15 @@ databases:
|
|
24
24
|
# Minimum connections for high connections warning
|
25
25
|
# total_connections_threshold: 500
|
26
26
|
|
27
|
+
# Explain functionality
|
28
|
+
# explain: true / false / analyze
|
29
|
+
|
27
30
|
# Statement timeout for explain
|
28
31
|
# explain_timeout_sec: 10
|
29
32
|
|
33
|
+
# Visualize URL for explain
|
34
|
+
# visualize_url: https://...
|
35
|
+
|
30
36
|
# Time zone (defaults to app time zone)
|
31
37
|
# time_zone: "Pacific Time (US & Canada)"
|
32
38
|
|
data/lib/pghero/database.rb
CHANGED
@@ -118,12 +118,6 @@ module PgHero
|
|
118
118
|
@filter_data
|
119
119
|
end
|
120
120
|
|
121
|
-
# TODO remove in next major version
|
122
|
-
alias_method :access_key_id, :aws_access_key_id
|
123
|
-
alias_method :secret_access_key, :aws_secret_access_key
|
124
|
-
alias_method :region, :aws_region
|
125
|
-
alias_method :db_instance_identifier, :aws_db_instance_identifier
|
126
|
-
|
127
121
|
private
|
128
122
|
|
129
123
|
# check adapter lazily
|
@@ -148,7 +142,6 @@ module PgHero
|
|
148
142
|
|
149
143
|
# resolve spec
|
150
144
|
if !url && config["spec"]
|
151
|
-
raise Error, "Spec requires Rails 6+" unless PgHero.spec_supported?
|
152
145
|
config_options = {env_name: PgHero.env, PgHero.spec_name_key => config["spec"], PgHero.include_replicas_key => true}
|
153
146
|
resolved = ActiveRecord::Base.configurations.configs_for(**config_options)
|
154
147
|
raise Error, "Spec not found: #{config["spec"]}" unless resolved
|
data/lib/pghero/engine.rb
CHANGED
@@ -5,7 +5,7 @@ module PgHero
|
|
5
5
|
initializer "pghero", group: :all do |app|
|
6
6
|
# check if Rails api mode
|
7
7
|
if app.config.respond_to?(:assets)
|
8
|
-
if defined?(Sprockets) && Sprockets::VERSION >=
|
8
|
+
if defined?(Sprockets) && Sprockets::VERSION.to_i >= 4
|
9
9
|
app.config.assets.precompile << "pghero/application.js"
|
10
10
|
app.config.assets.precompile << "pghero/application.css"
|
11
11
|
app.config.assets.precompile << "pghero/favicon.png"
|
data/lib/pghero/methods/basic.rb
CHANGED
@@ -45,7 +45,7 @@ module PgHero
|
|
45
45
|
if ActiveRecord::VERSION::STRING.to_f >= 6.1
|
46
46
|
result = result.map(&:symbolize_keys)
|
47
47
|
else
|
48
|
-
result = result.map { |row|
|
48
|
+
result = result.map { |row| row.to_h { |col, val| [col.to_sym, result.column_types[col].send(:cast_value, val)] } }
|
49
49
|
end
|
50
50
|
if filter_data
|
51
51
|
query_columns.each do |column|
|
@@ -112,15 +112,8 @@ module PgHero
|
|
112
112
|
::PgHero::Stats.connection
|
113
113
|
end
|
114
114
|
|
115
|
-
def insert_stats(table, columns, values)
|
116
|
-
values = values.map { |v| "(#{v.map { |v2| quote(v2) }.join(",")})" }.join(",")
|
117
|
-
columns = columns.map { |v| quote_table_name(v) }.join(",")
|
118
|
-
stats_connection.execute("INSERT INTO #{quote_table_name(table)} (#{columns}) VALUES #{values}")
|
119
|
-
end
|
120
|
-
|
121
|
-
# from ActiveSupport
|
122
115
|
def squish(str)
|
123
|
-
str.to_s.
|
116
|
+
str.to_s.squish
|
124
117
|
end
|
125
118
|
|
126
119
|
def add_source(sql)
|
@@ -135,6 +128,10 @@ module PgHero
|
|
135
128
|
connection.quote_table_name(value)
|
136
129
|
end
|
137
130
|
|
131
|
+
def quote_column_name(value)
|
132
|
+
connection.quote_column_name(value)
|
133
|
+
end
|
134
|
+
|
138
135
|
def unquote(part)
|
139
136
|
if part && part.start_with?('"')
|
140
137
|
part[1..-2]
|
@@ -3,7 +3,7 @@ module PgHero
|
|
3
3
|
module Connections
|
4
4
|
def connections
|
5
5
|
if server_version_num >= 90500
|
6
|
-
select_all
|
6
|
+
select_all <<~SQL
|
7
7
|
SELECT
|
8
8
|
pg_stat_activity.pid,
|
9
9
|
datname AS database,
|
@@ -20,7 +20,7 @@ module PgHero
|
|
20
20
|
pg_stat_activity.pid
|
21
21
|
SQL
|
22
22
|
else
|
23
|
-
select_all
|
23
|
+
select_all <<~SQL
|
24
24
|
SELECT
|
25
25
|
pid,
|
26
26
|
datname AS database,
|
@@ -41,7 +41,7 @@ module PgHero
|
|
41
41
|
end
|
42
42
|
|
43
43
|
def connection_states
|
44
|
-
states = select_all
|
44
|
+
states = select_all <<~SQL
|
45
45
|
SELECT
|
46
46
|
state,
|
47
47
|
COUNT(*) AS connections
|
@@ -53,11 +53,11 @@ module PgHero
|
|
53
53
|
2 DESC, 1
|
54
54
|
SQL
|
55
55
|
|
56
|
-
|
56
|
+
states.to_h { |s| [s[:state], s[:connections]] }
|
57
57
|
end
|
58
58
|
|
59
59
|
def connection_sources
|
60
|
-
select_all
|
60
|
+
select_all <<~SQL
|
61
61
|
SELECT
|
62
62
|
datname AS database,
|
63
63
|
usename AS user,
|
@@ -1,6 +1,8 @@
|
|
1
1
|
module PgHero
|
2
2
|
module Methods
|
3
3
|
module Explain
|
4
|
+
# TODO remove in 4.0
|
5
|
+
# note: this method is not affected by the explain option
|
4
6
|
def explain(sql)
|
5
7
|
sql = squish(sql)
|
6
8
|
explanation = nil
|
@@ -16,6 +18,23 @@ module PgHero
|
|
16
18
|
explanation
|
17
19
|
end
|
18
20
|
|
21
|
+
# TODO rename to explain in 4.0
|
22
|
+
# note: this method is not affected by the explain option
|
23
|
+
def explain_v2(sql, analyze: nil, verbose: nil, costs: nil, settings: nil, buffers: nil, wal: nil, timing: nil, summary: nil, format: "text")
|
24
|
+
options = []
|
25
|
+
add_explain_option(options, "ANALYZE", analyze)
|
26
|
+
add_explain_option(options, "VERBOSE", verbose)
|
27
|
+
add_explain_option(options, "SETTINGS", settings)
|
28
|
+
add_explain_option(options, "COSTS", costs)
|
29
|
+
add_explain_option(options, "BUFFERS", buffers)
|
30
|
+
add_explain_option(options, "WAL", wal)
|
31
|
+
add_explain_option(options, "TIMING", timing)
|
32
|
+
add_explain_option(options, "SUMMARY", summary)
|
33
|
+
options << "FORMAT #{explain_format(format)}"
|
34
|
+
|
35
|
+
explain("(#{options.join(", ")}) #{sql}")
|
36
|
+
end
|
37
|
+
|
19
38
|
private
|
20
39
|
|
21
40
|
def explain_safe?
|
@@ -24,6 +43,21 @@ module PgHero
|
|
24
43
|
rescue ActiveRecord::StatementInvalid
|
25
44
|
true
|
26
45
|
end
|
46
|
+
|
47
|
+
def add_explain_option(options, name, value)
|
48
|
+
unless value.nil?
|
49
|
+
options << "#{name}#{value ? "" : " FALSE"}"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# important! validate format to prevent injection
|
54
|
+
def explain_format(format)
|
55
|
+
if ["text", "xml", "json", "yaml"].include?(format)
|
56
|
+
format.upcase
|
57
|
+
else
|
58
|
+
raise ArgumentError, "Unknown format"
|
59
|
+
end
|
60
|
+
end
|
27
61
|
end
|
28
62
|
end
|
29
63
|
end
|
@@ -2,7 +2,7 @@ module PgHero
|
|
2
2
|
module Methods
|
3
3
|
module Indexes
|
4
4
|
def index_hit_rate
|
5
|
-
select_one
|
5
|
+
select_one <<~SQL
|
6
6
|
SELECT
|
7
7
|
(sum(idx_blks_hit)) / nullif(sum(idx_blks_hit + idx_blks_read), 0) AS rate
|
8
8
|
FROM
|
@@ -11,7 +11,7 @@ module PgHero
|
|
11
11
|
end
|
12
12
|
|
13
13
|
def index_caching
|
14
|
-
select_all
|
14
|
+
select_all <<~SQL
|
15
15
|
SELECT
|
16
16
|
schemaname AS schema,
|
17
17
|
relname AS table,
|
@@ -29,7 +29,7 @@ module PgHero
|
|
29
29
|
end
|
30
30
|
|
31
31
|
def index_usage
|
32
|
-
select_all
|
32
|
+
select_all <<~SQL
|
33
33
|
SELECT
|
34
34
|
schemaname AS schema,
|
35
35
|
relname AS table,
|
@@ -47,7 +47,7 @@ module PgHero
|
|
47
47
|
end
|
48
48
|
|
49
49
|
def missing_indexes
|
50
|
-
select_all
|
50
|
+
select_all <<~SQL
|
51
51
|
SELECT
|
52
52
|
schemaname AS schema,
|
53
53
|
relname AS table,
|
@@ -69,7 +69,7 @@ module PgHero
|
|
69
69
|
end
|
70
70
|
|
71
71
|
def unused_indexes(max_scans: 50, across: [])
|
72
|
-
result = select_all_size
|
72
|
+
result = select_all_size <<~SQL
|
73
73
|
SELECT
|
74
74
|
schemaname AS schema,
|
75
75
|
relname AS table,
|
@@ -104,7 +104,7 @@ module PgHero
|
|
104
104
|
end
|
105
105
|
|
106
106
|
def last_stats_reset_time
|
107
|
-
select_one
|
107
|
+
select_one <<~SQL
|
108
108
|
SELECT
|
109
109
|
pg_stat_get_db_stat_reset_time(oid) AS reset_time
|
110
110
|
FROM
|
@@ -126,7 +126,7 @@ module PgHero
|
|
126
126
|
# TODO parse array properly
|
127
127
|
# https://stackoverflow.com/questions/2204058/list-columns-with-indexes-in-postgresql
|
128
128
|
def indexes
|
129
|
-
indexes = select_all(
|
129
|
+
indexes = select_all(<<~SQL
|
130
130
|
SELECT
|
131
131
|
schemaname AS schema,
|
132
132
|
t.relname AS table,
|
@@ -186,7 +186,7 @@ module PgHero
|
|
186
186
|
# thanks @jberkus and @mbanck
|
187
187
|
def index_bloat(min_size: nil)
|
188
188
|
min_size ||= index_bloat_bytes
|
189
|
-
select_all
|
189
|
+
select_all <<~SQL
|
190
190
|
WITH btree_index_atts AS (
|
191
191
|
SELECT
|
192
192
|
nspname, relname, reltuples, relpages, indrelid, relam,
|
data/lib/pghero/methods/kill.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
module PgHero
|
2
2
|
module Methods
|
3
3
|
module Maintenance
|
4
|
-
# https://www.postgresql.org/docs/
|
4
|
+
# https://www.postgresql.org/docs/current/routine-vacuuming.html#VACUUM-FOR-WRAPAROUND
|
5
5
|
# "the system will shut down and refuse to start any new transactions
|
6
6
|
# once there are fewer than 1 million transactions left until wraparound"
|
7
7
|
# warn when 10,000,000 transactions left
|
@@ -9,7 +9,7 @@ module PgHero
|
|
9
9
|
max_value = max_value.to_i
|
10
10
|
threshold = threshold.to_i
|
11
11
|
|
12
|
-
select_all
|
12
|
+
select_all <<~SQL
|
13
13
|
SELECT
|
14
14
|
n.nspname AS schema,
|
15
15
|
c.relname AS table,
|
@@ -35,7 +35,7 @@ module PgHero
|
|
35
35
|
|
36
36
|
def vacuum_progress
|
37
37
|
if server_version_num >= 90600
|
38
|
-
select_all
|
38
|
+
select_all <<~SQL
|
39
39
|
SELECT
|
40
40
|
pid,
|
41
41
|
phase
|
@@ -50,7 +50,7 @@ module PgHero
|
|
50
50
|
end
|
51
51
|
|
52
52
|
def maintenance_info
|
53
|
-
select_all
|
53
|
+
select_all <<~SQL
|
54
54
|
SELECT
|
55
55
|
schemaname AS schema,
|
56
56
|
relname AS table,
|
@@ -2,7 +2,7 @@ module PgHero
|
|
2
2
|
module Methods
|
3
3
|
module Queries
|
4
4
|
def running_queries(min_duration: nil, all: false)
|
5
|
-
query =
|
5
|
+
query = <<~SQL
|
6
6
|
SELECT
|
7
7
|
pid,
|
8
8
|
state,
|
@@ -36,7 +36,7 @@ module PgHero
|
|
36
36
|
# from https://wiki.postgresql.org/wiki/Lock_Monitoring
|
37
37
|
# and https://big-elephants.com/2013-09/exploring-query-locks-in-postgres/
|
38
38
|
def blocked_queries
|
39
|
-
query =
|
39
|
+
query = <<~SQL
|
40
40
|
SELECT
|
41
41
|
COALESCE(blockingl.relation::regclass::text,blockingl.locktype) as locked_item,
|
42
42
|
blockeda.pid AS blocked_pid,
|
@@ -56,10 +56,9 @@ module PgHero
|
|
56
56
|
true
|
57
57
|
end
|
58
58
|
|
59
|
-
# TODO scope by database in PgHero 3.0
|
60
|
-
# (add database: database_name to options)
|
61
59
|
def reset_query_stats(**options)
|
62
|
-
reset_instance_query_stats(
|
60
|
+
raise PgHero::Error, "Use reset_instance_query_stats to pass database" if options.delete(:database)
|
61
|
+
reset_instance_query_stats(**options, database: database_name)
|
63
62
|
end
|
64
63
|
|
65
64
|
# resets query stats for the entire instance
|
@@ -121,7 +120,7 @@ module PgHero
|
|
121
120
|
server_version_num >= 90400
|
122
121
|
end
|
123
122
|
|
124
|
-
# resetting query stats will reset across the entire Postgres instance
|
123
|
+
# resetting query stats will reset across the entire Postgres instance in Postgres < 12
|
125
124
|
# this is problematic if multiple PgHero databases use the same Postgres instance
|
126
125
|
#
|
127
126
|
# to get around this, we capture queries for every Postgres database before we
|
@@ -147,16 +146,15 @@ module PgHero
|
|
147
146
|
# nothing to do
|
148
147
|
return if query_stats.empty?
|
149
148
|
|
150
|
-
#
|
151
|
-
|
152
|
-
if false # mapping.size == 1 && server_version_num >= 120000
|
149
|
+
# reset individual databases for Postgres 12+ instance
|
150
|
+
if server_version_num >= 120000
|
153
151
|
query_stats.each do |db_id, db_query_stats|
|
154
|
-
if
|
152
|
+
if reset_instance_query_stats(database: mapping[db_id], raise_errors: raise_errors)
|
155
153
|
insert_query_stats(db_id, db_query_stats, now)
|
156
154
|
end
|
157
155
|
end
|
158
156
|
else
|
159
|
-
if
|
157
|
+
if reset_instance_query_stats(raise_errors: raise_errors)
|
160
158
|
query_stats.each do |db_id, db_query_stats|
|
161
159
|
insert_query_stats(db_id, db_query_stats, now)
|
162
160
|
end
|
@@ -164,8 +162,9 @@ module PgHero
|
|
164
162
|
end
|
165
163
|
end
|
166
164
|
|
167
|
-
def clean_query_stats
|
168
|
-
|
165
|
+
def clean_query_stats(before: nil)
|
166
|
+
before ||= 14.days.ago
|
167
|
+
PgHero::QueryStats.where(database: id).where("captured_at < ?", before).delete_all
|
169
168
|
end
|
170
169
|
|
171
170
|
def slow_queries(query_stats: nil, **options)
|
@@ -173,10 +172,11 @@ module PgHero
|
|
173
172
|
query_stats.select { |q| q[:calls].to_i >= slow_query_calls.to_i && q[:average_time].to_f >= slow_query_ms.to_f }
|
174
173
|
end
|
175
174
|
|
175
|
+
# TODO option to include current period
|
176
176
|
def query_hash_stats(query_hash, user: nil)
|
177
177
|
if historical_query_stats_enabled? && supports_query_hash?
|
178
178
|
start_at = 24.hours.ago
|
179
|
-
select_all_stats
|
179
|
+
select_all_stats <<~SQL
|
180
180
|
SELECT
|
181
181
|
captured_at,
|
182
182
|
total_time / 1000 / 60 AS total_minutes,
|
@@ -206,7 +206,7 @@ module PgHero
|
|
206
206
|
limit ||= 100
|
207
207
|
sort ||= "total_minutes"
|
208
208
|
total_time = server_version_num >= 130000 ? "(total_plan_time + total_exec_time)" : "total_time"
|
209
|
-
query =
|
209
|
+
query = <<~SQL
|
210
210
|
WITH query_stats AS (
|
211
211
|
SELECT
|
212
212
|
LEFT(query, 10000) AS query,
|
@@ -228,6 +228,7 @@ module PgHero
|
|
228
228
|
)
|
229
229
|
SELECT
|
230
230
|
query,
|
231
|
+
query AS explainable_query,
|
231
232
|
query_hash,
|
232
233
|
query_stats.user,
|
233
234
|
total_minutes,
|
@@ -238,14 +239,14 @@ module PgHero
|
|
238
239
|
FROM
|
239
240
|
query_stats
|
240
241
|
ORDER BY
|
241
|
-
#{
|
242
|
+
#{quote_column_name(sort)} DESC
|
242
243
|
LIMIT #{limit.to_i}
|
243
244
|
SQL
|
244
245
|
|
245
246
|
# we may be able to skip query_columns
|
246
247
|
# in more recent versions of Postgres
|
247
248
|
# as pg_stat_statements should be already normalized
|
248
|
-
select_all(query, query_columns: [:query])
|
249
|
+
select_all(query, query_columns: [:query, :explainable_query])
|
249
250
|
else
|
250
251
|
raise NotEnabled, "Query stats not enabled"
|
251
252
|
end
|
@@ -254,7 +255,7 @@ module PgHero
|
|
254
255
|
def historical_query_stats(sort: nil, start_at: nil, end_at: nil, query_hash: nil)
|
255
256
|
if historical_query_stats_enabled?
|
256
257
|
sort ||= "total_minutes"
|
257
|
-
query =
|
258
|
+
query = <<~SQL
|
258
259
|
WITH query_stats AS (
|
259
260
|
SELECT
|
260
261
|
#{supports_query_hash? ? "query_hash" : "md5(query)"} AS query_hash,
|
@@ -287,7 +288,7 @@ module PgHero
|
|
287
288
|
FROM
|
288
289
|
query_stats
|
289
290
|
ORDER BY
|
290
|
-
#{
|
291
|
+
#{quote_column_name(sort)} DESC
|
291
292
|
LIMIT 100
|
292
293
|
SQL
|
293
294
|
|
@@ -330,19 +331,17 @@ module PgHero
|
|
330
331
|
def insert_query_stats(db_id, db_query_stats, now)
|
331
332
|
values =
|
332
333
|
db_query_stats.map do |qs|
|
333
|
-
|
334
|
-
db_id,
|
335
|
-
qs[:query],
|
336
|
-
qs[:total_minutes] * 60 * 1000,
|
337
|
-
qs[:calls],
|
338
|
-
now,
|
339
|
-
supports_query_hash? ? qs[:query_hash] : nil,
|
340
|
-
qs[:user]
|
341
|
-
|
334
|
+
{
|
335
|
+
database: db_id,
|
336
|
+
query: qs[:query],
|
337
|
+
total_time: qs[:total_minutes] * 60 * 1000,
|
338
|
+
calls: qs[:calls],
|
339
|
+
captured_at: now,
|
340
|
+
query_hash: supports_query_hash? ? qs[:query_hash] : nil,
|
341
|
+
user: qs[:user]
|
342
|
+
}
|
342
343
|
end
|
343
|
-
|
344
|
-
columns = %w[database query total_time calls captured_at query_hash user]
|
345
|
-
insert_stats("pghero_query_stats", columns, values)
|
344
|
+
PgHero::QueryStats.insert_all!(values)
|
346
345
|
end
|
347
346
|
end
|
348
347
|
end
|
@@ -18,7 +18,7 @@ module PgHero
|
|
18
18
|
"pg_last_xlog_receive_location() = pg_last_xlog_replay_location()"
|
19
19
|
end
|
20
20
|
|
21
|
-
select_one
|
21
|
+
select_one <<~SQL
|
22
22
|
SELECT
|
23
23
|
CASE
|
24
24
|
WHEN NOT pg_is_in_recovery() OR #{lag_condition} THEN 0
|
@@ -32,7 +32,7 @@ module PgHero
|
|
32
32
|
def replication_slots
|
33
33
|
if server_version_num >= 90400
|
34
34
|
with_feature_support(:replication_slots, []) do
|
35
|
-
select_all
|
35
|
+
select_all <<~SQL
|
36
36
|
SELECT
|
37
37
|
slot_name,
|
38
38
|
database,
|
@@ -7,7 +7,7 @@ module PgHero
|
|
7
7
|
# it's what information_schema.columns uses
|
8
8
|
# also, exclude temporary tables to prevent error
|
9
9
|
# when accessing across sessions
|
10
|
-
sequences = select_all
|
10
|
+
sequences = select_all <<~SQL
|
11
11
|
SELECT
|
12
12
|
n.nspname AS table_schema,
|
13
13
|
c.relname AS table,
|
@@ -84,7 +84,7 @@ module PgHero
|
|
84
84
|
# also adds schema if missing
|
85
85
|
def add_sequence_attributes(sequences)
|
86
86
|
# fetch data
|
87
|
-
sequence_attributes = select_all
|
87
|
+
sequence_attributes = select_all <<~SQL
|
88
88
|
SELECT
|
89
89
|
n.nspname AS schema,
|
90
90
|
c.relname AS sequence,
|
@@ -115,7 +115,7 @@ module PgHero
|
|
115
115
|
end
|
116
116
|
|
117
117
|
# then populate attributes
|
118
|
-
readable =
|
118
|
+
readable = sequence_attributes.to_h { |s| [[s[:schema], s[:sequence]], s[:readable]] }
|
119
119
|
sequences.each do |sequence|
|
120
120
|
sequence[:readable] = readable[[sequence[:schema], sequence[:sequence]]] || false
|
121
121
|
end
|
data/lib/pghero/methods/space.rb
CHANGED
@@ -6,7 +6,7 @@ module PgHero
|
|
6
6
|
end
|
7
7
|
|
8
8
|
def relation_sizes
|
9
|
-
select_all_size
|
9
|
+
select_all_size <<~SQL
|
10
10
|
SELECT
|
11
11
|
n.nspname AS schema,
|
12
12
|
c.relname AS relation,
|
@@ -27,7 +27,7 @@ module PgHero
|
|
27
27
|
end
|
28
28
|
|
29
29
|
def table_sizes
|
30
|
-
select_all_size
|
30
|
+
select_all_size <<~SQL
|
31
31
|
SELECT
|
32
32
|
n.nspname AS schema,
|
33
33
|
c.relname AS table,
|
@@ -49,10 +49,10 @@ module PgHero
|
|
49
49
|
def space_growth(days: 7, relation_sizes: nil)
|
50
50
|
if space_stats_enabled?
|
51
51
|
relation_sizes ||= self.relation_sizes
|
52
|
-
sizes =
|
52
|
+
sizes = relation_sizes.to_h { |r| [[r[:schema], r[:relation]], r[:size_bytes]] }
|
53
53
|
start_at = days.days.ago
|
54
54
|
|
55
|
-
stats = select_all_stats
|
55
|
+
stats = select_all_stats <<~SQL
|
56
56
|
WITH t AS (
|
57
57
|
SELECT
|
58
58
|
schema,
|
@@ -92,10 +92,10 @@ module PgHero
|
|
92
92
|
def relation_space_stats(relation, schema: "public")
|
93
93
|
if space_stats_enabled?
|
94
94
|
relation_sizes ||= self.relation_sizes
|
95
|
-
sizes =
|
95
|
+
sizes = relation_sizes.map { |r| [[r[:schema], r[:relation]], r[:size_bytes]] }.to_h
|
96
96
|
start_at = 30.days.ago
|
97
97
|
|
98
|
-
stats = select_all_stats
|
98
|
+
stats = select_all_stats <<~SQL
|
99
99
|
SELECT
|
100
100
|
captured_at,
|
101
101
|
size AS size_bytes
|
@@ -121,16 +121,22 @@ module PgHero
|
|
121
121
|
|
122
122
|
def capture_space_stats
|
123
123
|
now = Time.now
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
124
|
+
values =
|
125
|
+
relation_sizes.map do |rs|
|
126
|
+
{
|
127
|
+
database: id,
|
128
|
+
schema: rs[:schema],
|
129
|
+
relation: rs[:relation],
|
130
|
+
size: rs[:size_bytes].to_i,
|
131
|
+
captured_at: now
|
132
|
+
}
|
133
|
+
end
|
134
|
+
PgHero::SpaceStats.insert_all!(values) if values.any?
|
130
135
|
end
|
131
136
|
|
132
|
-
def clean_space_stats
|
133
|
-
|
137
|
+
def clean_space_stats(before: nil)
|
138
|
+
before ||= 90.days.ago
|
139
|
+
PgHero::SpaceStats.where(database: id).where("captured_at < ?", before).delete_all
|
134
140
|
end
|
135
141
|
|
136
142
|
def space_stats_enabled?
|