pghero 2.8.3 → 3.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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?
|