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.

Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +11 -0
  3. data/CHANGELOG.md +6 -0
  4. data/README.md +1 -1
  5. data/app/controllers/pg_hero/home_controller.rb +15 -4
  6. data/app/views/layouts/pg_hero/application.html.erb +4 -4
  7. data/app/views/pg_hero/home/explain.html.erb +1 -1
  8. data/app/views/pg_hero/home/index_usage.html.erb +6 -1
  9. data/app/views/pg_hero/home/maintenance.html.erb +6 -1
  10. data/app/views/pg_hero/home/space.html.erb +5 -3
  11. data/lib/pghero.rb +35 -1243
  12. data/lib/pghero/connection.rb +5 -0
  13. data/lib/pghero/database.rb +12 -3
  14. data/lib/pghero/methods/basic.rb +104 -0
  15. data/lib/pghero/methods/connections.rb +49 -0
  16. data/lib/pghero/methods/databases.rb +39 -0
  17. data/lib/pghero/methods/explain.rb +29 -0
  18. data/lib/pghero/methods/indexes.rb +154 -0
  19. data/lib/pghero/methods/kill.rb +27 -0
  20. data/lib/pghero/methods/maintenance.rb +61 -0
  21. data/lib/pghero/methods/queries.rb +73 -0
  22. data/lib/pghero/methods/query_stats.rb +188 -0
  23. data/lib/pghero/methods/replica.rb +22 -0
  24. data/lib/pghero/methods/space.rb +30 -0
  25. data/lib/pghero/methods/suggested_indexes.rb +322 -0
  26. data/lib/pghero/methods/system.rb +70 -0
  27. data/lib/pghero/methods/tables.rb +68 -0
  28. data/lib/pghero/methods/users.rb +85 -0
  29. data/lib/pghero/query_stats.rb +7 -0
  30. data/lib/pghero/version.rb +1 -1
  31. data/lib/{pghero/tasks.rb → tasks/pghero.rake} +0 -2
  32. data/test/suggested_indexes_test.rb +3 -2
  33. data/test/test_helper.rb +1 -1
  34. metadata +22 -10
  35. data/test/gemfiles/activerecord31.gemfile +0 -6
  36. data/test/gemfiles/activerecord32.gemfile +0 -6
  37. data/test/gemfiles/activerecord40.gemfile +0 -6
@@ -0,0 +1,5 @@
1
+ module PgHero
2
+ class Connection < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -5,17 +5,26 @@ module PgHero
5
5
  def initialize(id, config)
6
6
  @id = id
7
7
  @config = config
8
- @connection_model =
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(config["url"]) if config["url"]
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