pghero 1.2.2 → 1.2.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.
- checksums.yaml +4 -4
- data/.travis.yml +11 -0
- data/CHANGELOG.md +6 -0
- data/README.md +1 -1
- data/app/controllers/pg_hero/home_controller.rb +15 -4
- data/app/views/layouts/pg_hero/application.html.erb +4 -4
- data/app/views/pg_hero/home/explain.html.erb +1 -1
- data/app/views/pg_hero/home/index_usage.html.erb +6 -1
- data/app/views/pg_hero/home/maintenance.html.erb +6 -1
- data/app/views/pg_hero/home/space.html.erb +5 -3
- data/lib/pghero.rb +35 -1243
- data/lib/pghero/connection.rb +5 -0
- data/lib/pghero/database.rb +12 -3
- data/lib/pghero/methods/basic.rb +104 -0
- data/lib/pghero/methods/connections.rb +49 -0
- data/lib/pghero/methods/databases.rb +39 -0
- data/lib/pghero/methods/explain.rb +29 -0
- data/lib/pghero/methods/indexes.rb +154 -0
- data/lib/pghero/methods/kill.rb +27 -0
- data/lib/pghero/methods/maintenance.rb +61 -0
- data/lib/pghero/methods/queries.rb +73 -0
- data/lib/pghero/methods/query_stats.rb +188 -0
- data/lib/pghero/methods/replica.rb +22 -0
- data/lib/pghero/methods/space.rb +30 -0
- data/lib/pghero/methods/suggested_indexes.rb +322 -0
- data/lib/pghero/methods/system.rb +70 -0
- data/lib/pghero/methods/tables.rb +68 -0
- data/lib/pghero/methods/users.rb +85 -0
- data/lib/pghero/query_stats.rb +7 -0
- data/lib/pghero/version.rb +1 -1
- data/lib/{pghero/tasks.rb → tasks/pghero.rake} +0 -2
- data/test/suggested_indexes_test.rb +3 -2
- data/test/test_helper.rb +1 -1
- metadata +22 -10
- data/test/gemfiles/activerecord31.gemfile +0 -6
- data/test/gemfiles/activerecord32.gemfile +0 -6
- data/test/gemfiles/activerecord40.gemfile +0 -6
data/lib/pghero/database.rb
CHANGED
@@ -5,17 +5,26 @@ module PgHero
|
|
5
5
|
def initialize(id, config)
|
6
6
|
@id = id
|
7
7
|
@config = config
|
8
|
-
|
8
|
+
end
|
9
|
+
|
10
|
+
def connection_model
|
11
|
+
@connection_model ||= begin
|
12
|
+
url = config["url"]
|
9
13
|
Class.new(PgHero::Connection) do
|
10
14
|
def self.name
|
11
15
|
"PgHero::Connection::#{object_id}"
|
12
16
|
end
|
13
|
-
establish_connection(
|
17
|
+
establish_connection(url) if url
|
14
18
|
end
|
19
|
+
end
|
15
20
|
end
|
16
21
|
|
17
22
|
def db_instance_identifier
|
18
|
-
@config["db_instance_identifier"]
|
23
|
+
@db_instance_identifier ||= @config["db_instance_identifier"]
|
24
|
+
end
|
25
|
+
|
26
|
+
def name
|
27
|
+
@name ||= @config["name"] || id.titleize
|
19
28
|
end
|
20
29
|
end
|
21
30
|
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
module PgHero
|
2
|
+
module Methods
|
3
|
+
module Basic
|
4
|
+
def time_zone=(time_zone)
|
5
|
+
@time_zone = time_zone.is_a?(ActiveSupport::TimeZone) ? time_zone : ActiveSupport::TimeZone[time_zone.to_s]
|
6
|
+
end
|
7
|
+
|
8
|
+
def time_zone
|
9
|
+
@time_zone || Time.zone
|
10
|
+
end
|
11
|
+
|
12
|
+
def config
|
13
|
+
Thread.current[:pghero_config] ||= begin
|
14
|
+
path = "config/pghero.yml"
|
15
|
+
|
16
|
+
config =
|
17
|
+
(YAML.load(ERB.new(File.read(path)).result)[env] if File.exist?(path))
|
18
|
+
|
19
|
+
if config
|
20
|
+
config
|
21
|
+
else
|
22
|
+
{
|
23
|
+
"databases" => {
|
24
|
+
"primary" => {
|
25
|
+
"url" => ENV["PGHERO_DATABASE_URL"] || ActiveRecord::Base.connection_config,
|
26
|
+
"db_instance_identifier" => ENV["PGHERO_DB_INSTANCE_IDENTIFIER"]
|
27
|
+
}
|
28
|
+
}
|
29
|
+
}
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def settings
|
35
|
+
names = %w(
|
36
|
+
max_connections shared_buffers effective_cache_size work_mem
|
37
|
+
maintenance_work_mem checkpoint_segments checkpoint_completion_target
|
38
|
+
wal_buffers default_statistics_target
|
39
|
+
)
|
40
|
+
values = Hash[select_all(connection_model.send(:sanitize_sql_array, ["SELECT name, setting, unit FROM pg_settings WHERE name IN (?)", names])).sort_by { |row| names.index(row["name"]) }.map { |row| [row["name"], friendly_value(row["setting"], row["unit"])] }]
|
41
|
+
Hash[names.map { |name| [name, values[name]] }]
|
42
|
+
end
|
43
|
+
|
44
|
+
def ssl_used?
|
45
|
+
ssl_used = nil
|
46
|
+
connection_model.transaction do
|
47
|
+
execute("CREATE EXTENSION IF NOT EXISTS sslinfo")
|
48
|
+
ssl_used = select_all("SELECT ssl_is_used()").first["ssl_is_used"] == "t"
|
49
|
+
raise ActiveRecord::Rollback
|
50
|
+
end
|
51
|
+
ssl_used
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def friendly_value(setting, unit)
|
57
|
+
if %w(kB 8kB).include?(unit)
|
58
|
+
value = setting.to_i
|
59
|
+
value *= 8 if unit == "8kB"
|
60
|
+
|
61
|
+
if value % (1024 * 1024) == 0
|
62
|
+
"#{value / (1024 * 1024)}GB"
|
63
|
+
elsif value % 1024 == 0
|
64
|
+
"#{value / 1024}MB"
|
65
|
+
else
|
66
|
+
"#{value}kB"
|
67
|
+
end
|
68
|
+
else
|
69
|
+
"#{setting}#{unit}".strip
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def select_all(sql)
|
74
|
+
# squish for logs
|
75
|
+
connection.select_all(squish(sql)).to_a
|
76
|
+
end
|
77
|
+
|
78
|
+
def execute(sql)
|
79
|
+
connection.execute(sql)
|
80
|
+
end
|
81
|
+
|
82
|
+
def connection_model
|
83
|
+
databases[current_database].connection_model
|
84
|
+
end
|
85
|
+
|
86
|
+
def connection
|
87
|
+
connection_model.connection
|
88
|
+
end
|
89
|
+
|
90
|
+
# from ActiveSupport
|
91
|
+
def squish(str)
|
92
|
+
str.to_s.gsub(/\A[[:space:]]+/, "").gsub(/[[:space:]]+\z/, "").gsub(/[[:space:]]+/, " ")
|
93
|
+
end
|
94
|
+
|
95
|
+
def quote(value)
|
96
|
+
connection.quote(value)
|
97
|
+
end
|
98
|
+
|
99
|
+
def quote_table_name(value)
|
100
|
+
connection.quote_table_name(value)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module PgHero
|
2
|
+
module Methods
|
3
|
+
module Connections
|
4
|
+
def total_connections
|
5
|
+
select_all("SELECT COUNT(*) FROM pg_stat_activity WHERE pid <> pg_backend_pid()").first["count"].to_i
|
6
|
+
end
|
7
|
+
|
8
|
+
def connection_sources(options = {})
|
9
|
+
if options[:by_database]
|
10
|
+
select_all <<-SQL
|
11
|
+
SELECT
|
12
|
+
application_name AS source,
|
13
|
+
client_addr AS ip,
|
14
|
+
datname AS database,
|
15
|
+
COUNT(*) AS total_connections
|
16
|
+
FROM
|
17
|
+
pg_stat_activity
|
18
|
+
WHERE
|
19
|
+
pid <> pg_backend_pid()
|
20
|
+
GROUP BY
|
21
|
+
1, 2, 3
|
22
|
+
ORDER BY
|
23
|
+
COUNT(*) DESC,
|
24
|
+
application_name ASC,
|
25
|
+
client_addr ASC
|
26
|
+
SQL
|
27
|
+
else
|
28
|
+
select_all <<-SQL
|
29
|
+
SELECT
|
30
|
+
application_name AS source,
|
31
|
+
client_addr AS ip,
|
32
|
+
COUNT(*) AS total_connections
|
33
|
+
FROM
|
34
|
+
pg_stat_activity
|
35
|
+
WHERE
|
36
|
+
pid <> pg_backend_pid()
|
37
|
+
GROUP BY
|
38
|
+
application_name,
|
39
|
+
ip
|
40
|
+
ORDER BY
|
41
|
+
COUNT(*) DESC,
|
42
|
+
application_name ASC,
|
43
|
+
client_addr ASC
|
44
|
+
SQL
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module PgHero
|
2
|
+
module Methods
|
3
|
+
module Databases
|
4
|
+
def databases
|
5
|
+
@databases ||= begin
|
6
|
+
Hash[
|
7
|
+
config["databases"].map do |id, c|
|
8
|
+
[id, PgHero::Database.new(id, c)]
|
9
|
+
end
|
10
|
+
]
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def primary_database
|
15
|
+
databases.keys.first
|
16
|
+
end
|
17
|
+
|
18
|
+
def current_database
|
19
|
+
Thread.current[:pghero_current_database] ||= primary_database
|
20
|
+
end
|
21
|
+
|
22
|
+
def current_database=(database)
|
23
|
+
raise "Database not found" unless databases[database]
|
24
|
+
Thread.current[:pghero_current_database] = database.to_s
|
25
|
+
database
|
26
|
+
end
|
27
|
+
|
28
|
+
def with(database)
|
29
|
+
previous_database = current_database
|
30
|
+
begin
|
31
|
+
self.current_database = database
|
32
|
+
yield
|
33
|
+
ensure
|
34
|
+
self.current_database = previous_database
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module PgHero
|
2
|
+
module Methods
|
3
|
+
module Explain
|
4
|
+
def explain(sql)
|
5
|
+
sql = squish(sql)
|
6
|
+
explanation = nil
|
7
|
+
explain_safe = explain_safe?
|
8
|
+
|
9
|
+
# use transaction for safety
|
10
|
+
connection_model.transaction do
|
11
|
+
if !explain_safe && (sql.sub(/;\z/, "").include?(";") || sql.upcase.include?("COMMIT"))
|
12
|
+
raise ActiveRecord::StatementInvalid, "Unsafe statement"
|
13
|
+
end
|
14
|
+
explanation = select_all("EXPLAIN #{sql}").map { |v| v["QUERY PLAN"] }.join("\n")
|
15
|
+
raise ActiveRecord::Rollback
|
16
|
+
end
|
17
|
+
|
18
|
+
explanation
|
19
|
+
end
|
20
|
+
|
21
|
+
def explain_safe?
|
22
|
+
select_all("SELECT 1; SELECT 1")
|
23
|
+
false
|
24
|
+
rescue ActiveRecord::StatementInvalid
|
25
|
+
true
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,154 @@
|
|
1
|
+
module PgHero
|
2
|
+
module Methods
|
3
|
+
module Indexes
|
4
|
+
def index_hit_rate
|
5
|
+
select_all(<<-SQL
|
6
|
+
SELECT
|
7
|
+
(sum(idx_blks_hit)) / nullif(sum(idx_blks_hit + idx_blks_read), 0) AS rate
|
8
|
+
FROM
|
9
|
+
pg_statio_user_indexes
|
10
|
+
SQL
|
11
|
+
).first["rate"].to_f
|
12
|
+
end
|
13
|
+
|
14
|
+
def index_caching
|
15
|
+
select_all <<-SQL
|
16
|
+
SELECT
|
17
|
+
indexrelname AS index,
|
18
|
+
relname AS table,
|
19
|
+
CASE WHEN idx_blks_hit + idx_blks_read = 0 THEN
|
20
|
+
0
|
21
|
+
ELSE
|
22
|
+
ROUND(1.0 * idx_blks_hit / (idx_blks_hit + idx_blks_read), 2)
|
23
|
+
END AS hit_rate
|
24
|
+
FROM
|
25
|
+
pg_statio_user_indexes
|
26
|
+
ORDER BY
|
27
|
+
3 DESC, 1
|
28
|
+
SQL
|
29
|
+
end
|
30
|
+
|
31
|
+
def index_usage
|
32
|
+
select_all <<-SQL
|
33
|
+
SELECT
|
34
|
+
schemaname AS schema,
|
35
|
+
relname AS table,
|
36
|
+
CASE idx_scan
|
37
|
+
WHEN 0 THEN 'Insufficient data'
|
38
|
+
ELSE (100 * idx_scan / (seq_scan + idx_scan))::text
|
39
|
+
END percent_of_times_index_used,
|
40
|
+
n_live_tup rows_in_table
|
41
|
+
FROM
|
42
|
+
pg_stat_user_tables
|
43
|
+
ORDER BY
|
44
|
+
n_live_tup DESC,
|
45
|
+
relname ASC
|
46
|
+
SQL
|
47
|
+
end
|
48
|
+
|
49
|
+
def missing_indexes
|
50
|
+
select_all <<-SQL
|
51
|
+
SELECT
|
52
|
+
schemaname AS schema,
|
53
|
+
relname AS table,
|
54
|
+
CASE idx_scan
|
55
|
+
WHEN 0 THEN 'Insufficient data'
|
56
|
+
ELSE (100 * idx_scan / (seq_scan + idx_scan))::text
|
57
|
+
END percent_of_times_index_used,
|
58
|
+
n_live_tup rows_in_table
|
59
|
+
FROM
|
60
|
+
pg_stat_user_tables
|
61
|
+
WHERE
|
62
|
+
idx_scan > 0
|
63
|
+
AND (100 * idx_scan / (seq_scan + idx_scan)) < 95
|
64
|
+
AND n_live_tup >= 10000
|
65
|
+
ORDER BY
|
66
|
+
n_live_tup DESC,
|
67
|
+
relname ASC
|
68
|
+
SQL
|
69
|
+
end
|
70
|
+
|
71
|
+
def unused_indexes
|
72
|
+
select_all <<-SQL
|
73
|
+
SELECT
|
74
|
+
schemaname AS schema,
|
75
|
+
relname AS table,
|
76
|
+
indexrelname AS index,
|
77
|
+
pg_size_pretty(pg_relation_size(i.indexrelid)) AS index_size,
|
78
|
+
idx_scan as index_scans
|
79
|
+
FROM
|
80
|
+
pg_stat_user_indexes ui
|
81
|
+
INNER JOIN
|
82
|
+
pg_index i ON ui.indexrelid = i.indexrelid
|
83
|
+
WHERE
|
84
|
+
NOT indisunique
|
85
|
+
AND idx_scan < 50
|
86
|
+
ORDER BY
|
87
|
+
pg_relation_size(i.indexrelid) DESC,
|
88
|
+
relname ASC
|
89
|
+
SQL
|
90
|
+
end
|
91
|
+
|
92
|
+
def invalid_indexes
|
93
|
+
select_all <<-SQL
|
94
|
+
SELECT
|
95
|
+
c.relname AS index
|
96
|
+
FROM
|
97
|
+
pg_catalog.pg_class c,
|
98
|
+
pg_catalog.pg_namespace n,
|
99
|
+
pg_catalog.pg_index i
|
100
|
+
WHERE
|
101
|
+
i.indisvalid = false
|
102
|
+
AND i.indexrelid = c.oid
|
103
|
+
AND c.relnamespace = n.oid
|
104
|
+
AND n.nspname != 'pg_catalog'
|
105
|
+
AND n.nspname != 'information_schema'
|
106
|
+
AND n.nspname != 'pg_toast'
|
107
|
+
ORDER BY
|
108
|
+
c.relname
|
109
|
+
SQL
|
110
|
+
end
|
111
|
+
|
112
|
+
# TODO parse array properly
|
113
|
+
# http://stackoverflow.com/questions/2204058/list-columns-with-indexes-in-postgresql
|
114
|
+
def indexes
|
115
|
+
select_all(<<-SQL
|
116
|
+
SELECT
|
117
|
+
t.relname AS table,
|
118
|
+
ix.relname AS name,
|
119
|
+
regexp_replace(pg_get_indexdef(indexrelid), '^[^\\(]*\\((.*)\\)$', '\\1') AS columns,
|
120
|
+
regexp_replace(pg_get_indexdef(indexrelid), '.* USING ([^ ]*) \\(.*', '\\1') AS using,
|
121
|
+
indisunique AS unique,
|
122
|
+
indisprimary AS primary,
|
123
|
+
indisvalid AS valid,
|
124
|
+
indexprs::text,
|
125
|
+
indpred::text,
|
126
|
+
pg_get_indexdef(indexrelid) AS definition
|
127
|
+
FROM
|
128
|
+
pg_index i
|
129
|
+
INNER JOIN
|
130
|
+
pg_class t ON t.oid = i.indrelid
|
131
|
+
INNER JOIN
|
132
|
+
pg_class ix ON ix.oid = i.indexrelid
|
133
|
+
ORDER BY
|
134
|
+
1, 2
|
135
|
+
SQL
|
136
|
+
).map { |v| v["columns"] = v["columns"].sub(") WHERE (", " WHERE ").split(", "); v }
|
137
|
+
end
|
138
|
+
|
139
|
+
def duplicate_indexes
|
140
|
+
indexes = []
|
141
|
+
|
142
|
+
indexes_by_table = self.indexes.group_by { |i| i["table"] }
|
143
|
+
indexes_by_table.values.flatten.select { |i| i["primary"] == "f" && i["unique"] == "f" && !i["indexprs"] && !i["indpred"] && i["valid"] == "t" }.each do |index|
|
144
|
+
covering_index = indexes_by_table[index["table"]].find { |i| index_covers?(i["columns"], index["columns"]) && i["using"] == index["using"] && i["name"] != index["name"] && i["valid"] == "t" }
|
145
|
+
if covering_index
|
146
|
+
indexes << {"unneeded_index" => index, "covering_index" => covering_index}
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
indexes.sort_by { |i| ui = i["unneeded_index"]; [ui["table"], ui["columns"]] }
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module PgHero
|
2
|
+
module Methods
|
3
|
+
module Kill
|
4
|
+
def kill(pid)
|
5
|
+
execute("SELECT pg_terminate_backend(#{pid.to_i})").first["pg_terminate_backend"] == "t"
|
6
|
+
end
|
7
|
+
|
8
|
+
def kill_long_running_queries
|
9
|
+
long_running_queries.each { |query| kill(query["pid"]) }
|
10
|
+
true
|
11
|
+
end
|
12
|
+
|
13
|
+
def kill_all
|
14
|
+
select_all <<-SQL
|
15
|
+
SELECT
|
16
|
+
pg_terminate_backend(pid)
|
17
|
+
FROM
|
18
|
+
pg_stat_activity
|
19
|
+
WHERE
|
20
|
+
pid <> pg_backend_pid()
|
21
|
+
AND query <> '<insufficient privilege>'
|
22
|
+
SQL
|
23
|
+
true
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|