awesome_explain 0.3.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.github/workflows/mongodb.yml +53 -0
- data/.github/workflows/postgres.yml +56 -0
- data/.gitignore +11 -0
- data/Appraisals +11 -0
- data/Gemfile.lock +209 -49
- data/LICENSE.txt +4 -20
- data/README.md +155 -7
- data/Rakefile +35 -1
- data/app/models/awesome_explain/application_record.rb +5 -0
- data/app/models/awesome_explain/controller.rb +20 -0
- data/app/models/awesome_explain/delayed_job.rb +7 -0
- data/app/models/awesome_explain/explain.rb +23 -0
- data/app/models/awesome_explain/log.rb +7 -0
- data/app/models/awesome_explain/pg_dml_stat.rb +4 -0
- data/app/models/awesome_explain/pg_seq_scan.rb +4 -0
- data/app/models/awesome_explain/plan_node.rb +52 -0
- data/app/models/awesome_explain/plan_tree.rb +66 -0
- data/app/models/awesome_explain/sidekiq_worker.rb +7 -0
- data/app/models/awesome_explain/sql_explain.rb +14 -0
- data/app/models/awesome_explain/sql_plan_node.rb +73 -0
- data/app/models/awesome_explain/sql_plan_stats.rb +34 -0
- data/app/models/awesome_explain/sql_plan_tree.rb +133 -0
- data/app/models/awesome_explain/sql_query.rb +7 -0
- data/app/models/awesome_explain/stacktrace.rb +11 -0
- data/awesome_explain.gemspec +16 -5
- data/bin/rails +14 -0
- data/data/mongodb/customers.bson +0 -0
- data/data/mongodb/customers.metadata.json +1 -0
- data/data/mongodb/line_items.bson +0 -0
- data/data/mongodb/line_items.metadata.json +1 -0
- data/data/mongodb/orders.bson +0 -0
- data/data/mongodb/orders.metadata.json +1 -0
- data/data/mongodb/products.bson +0 -0
- data/data/mongodb/products.metadata.json +1 -0
- data/data/postgresql/dvdrental.tar +0 -0
- data/db/migrate/20200507214801_stacktraces.rb +12 -0
- data/db/migrate/20200507214949_controllers.rb +16 -0
- data/db/migrate/20200507215205_logs.rb +22 -0
- data/db/migrate/20200507215243_explains.rb +27 -0
- data/gemfiles/rails_4.gemfile +7 -0
- data/gemfiles/rails_4.gemfile.lock +208 -0
- data/gemfiles/rails_5.gemfile +7 -0
- data/gemfiles/rails_5.gemfile.lock +209 -0
- data/gemfiles/rails_6.gemfile +7 -0
- data/gemfiles/rails_6.gemfile.lock +233 -0
- data/images/universe.png +0 -0
- data/lib/awesome_explain.rb +79 -2
- data/lib/awesome_explain/config.rb +196 -0
- data/lib/awesome_explain/engine.rb +5 -0
- data/lib/awesome_explain/insights/active_record_insights.rb +137 -0
- data/lib/awesome_explain/insights/base.rb +18 -0
- data/lib/awesome_explain/insights/mongoid_insights.rb +44 -0
- data/lib/awesome_explain/insights/sql_plans_insights.rb +64 -0
- data/lib/awesome_explain/kernel.rb +17 -0
- data/lib/awesome_explain/mongodb/base.rb +4 -0
- data/lib/awesome_explain/mongodb/command_start.rb +84 -0
- data/lib/awesome_explain/mongodb/command_success.rb +58 -0
- data/lib/awesome_explain/mongodb/formatter.rb +62 -0
- data/lib/awesome_explain/mongodb/helpers.rb +71 -0
- data/lib/awesome_explain/queue/command.rb +17 -0
- data/lib/awesome_explain/queue/simple_queue.rb +88 -0
- data/lib/awesome_explain/renderers/active_record.rb +114 -0
- data/lib/awesome_explain/renderers/base.rb +2 -0
- data/lib/awesome_explain/renderers/mongoid.rb +20 -33
- data/lib/awesome_explain/sidekiq_middleware.rb +17 -0
- data/lib/awesome_explain/stats/postgresql.rb +54 -0
- data/lib/awesome_explain/subscribers/active_record_passive_subscriber.rb +82 -0
- data/lib/awesome_explain/subscribers/active_record_subscriber.rb +187 -0
- data/lib/awesome_explain/subscribers/base.rb +3 -0
- data/lib/awesome_explain/subscribers/command_subscriber.rb +53 -0
- data/lib/awesome_explain/tasks/db.rb +325 -0
- data/lib/awesome_explain/utils/color.rb +16 -0
- data/lib/awesome_explain/version.rb +1 -1
- data/lib/tasks/ae.rake +28 -0
- data/lib/tasks/awesome_explain_tasks.rake +4 -0
- metadata +242 -25
- data/.travis.yml +0 -19
data/images/universe.png
ADDED
Binary file
|
data/lib/awesome_explain.rb
CHANGED
@@ -1,7 +1,84 @@
|
|
1
|
-
require '
|
1
|
+
require 'awesome_print'
|
2
|
+
require 'sqlite3'
|
3
|
+
require 'active_record'
|
4
|
+
require 'kaminari'
|
5
|
+
require 'awesome_print'
|
2
6
|
require 'terminal-table'
|
3
|
-
require '
|
7
|
+
require 'niceql'
|
8
|
+
|
9
|
+
require 'awesome_explain/version'
|
10
|
+
require 'awesome_explain/utils/color'
|
11
|
+
|
12
|
+
require 'awesome_explain/mongodb/base'
|
13
|
+
require 'awesome_explain/renderers/base'
|
14
|
+
require 'awesome_explain/subscribers/base'
|
15
|
+
require 'awesome_explain/insights/base'
|
16
|
+
|
17
|
+
require 'awesome_explain/config'
|
18
|
+
require 'awesome_explain/engine'
|
19
|
+
|
20
|
+
require 'awesome_explain/queue/simple_queue'
|
21
|
+
require 'awesome_explain/queue/command'
|
22
|
+
require 'awesome_explain/sidekiq_middleware'
|
23
|
+
require 'awesome_explain/stats/postgresql'
|
4
24
|
require 'awesome_explain/kernel'
|
5
25
|
|
26
|
+
DEFAULT_SOURCE_NAME = :server
|
27
|
+
|
28
|
+
COMMAND_NAMES_BLACKLIST = [
|
29
|
+
'createIndexes',
|
30
|
+
'explain',
|
31
|
+
'saslStart',
|
32
|
+
'saslContinue',
|
33
|
+
'listCollections',
|
34
|
+
'listIndexes',
|
35
|
+
'endSessions',
|
36
|
+
'killCursors',
|
37
|
+
'create',
|
38
|
+
'drop'
|
39
|
+
]
|
40
|
+
QUERIES = [
|
41
|
+
:aggregate,
|
42
|
+
:count,
|
43
|
+
:delete,
|
44
|
+
:distinct,
|
45
|
+
:find,
|
46
|
+
:getMore,
|
47
|
+
:insert,
|
48
|
+
:update
|
49
|
+
].freeze
|
50
|
+
|
51
|
+
DML_COMMANDS = [
|
52
|
+
:insert,
|
53
|
+
:update,
|
54
|
+
:delete
|
55
|
+
].freeze
|
56
|
+
|
57
|
+
COMMAND_ALLOWED_KEYS = ([
|
58
|
+
'filter',
|
59
|
+
'sort',
|
60
|
+
'limit',
|
61
|
+
'key',
|
62
|
+
'query'
|
63
|
+
] + (QUERIES.map {|q| q.to_s})).freeze
|
64
|
+
|
65
|
+
require 'thread'
|
66
|
+
|
6
67
|
module AwesomeExplain
|
68
|
+
def self.clean
|
69
|
+
AwesomeExplain::Log.delete_all
|
70
|
+
AwesomeExplain::SqlQuery.delete_all
|
71
|
+
AwesomeExplain::Explain.delete_all
|
72
|
+
AwesomeExplain::SqlExplain.delete_all
|
73
|
+
AwesomeExplain::Stacktrace.delete_all
|
74
|
+
AwesomeExplain::Controller.delete_all
|
75
|
+
end
|
76
|
+
|
77
|
+
def self.configure(&block)
|
78
|
+
raise NoBlockGivenException unless block_given?
|
79
|
+
|
80
|
+
Config.configure(&block)
|
81
|
+
end
|
82
|
+
|
83
|
+
class NoBlockGivenException < RuntimeError; end
|
7
84
|
end
|
@@ -0,0 +1,196 @@
|
|
1
|
+
module AwesomeExplain
|
2
|
+
class Config
|
3
|
+
include Singleton
|
4
|
+
attr_reader :db_name,
|
5
|
+
:db_path,
|
6
|
+
:rails_path,
|
7
|
+
:enabled,
|
8
|
+
:active_record_enabled,
|
9
|
+
:include_full_plan,
|
10
|
+
:max_limit,
|
11
|
+
:app_name,
|
12
|
+
:logger,
|
13
|
+
:queue,
|
14
|
+
:logs,
|
15
|
+
:adapter
|
16
|
+
|
17
|
+
DEFAULT_DB_NAME = :awesome_explain
|
18
|
+
POSTGRES_DEV_DBNAME = 'awesome_explain_development'
|
19
|
+
POSTGRES_DEFAULT_HOST = 'localhost'
|
20
|
+
POSTGRES_DEFAULT_USERNAME = 'postgres'
|
21
|
+
POSTGRES_DEFAULT_PASSWORD = 'postgres'
|
22
|
+
DEFAULT_DB_PATH = './log'
|
23
|
+
|
24
|
+
alias :enabled? :enabled
|
25
|
+
alias :active_record_enabled? :active_record_enabled
|
26
|
+
|
27
|
+
def self.configure(&block)
|
28
|
+
raise NoBlockGivenException unless block_given?
|
29
|
+
|
30
|
+
instance = Config.instance
|
31
|
+
instance.instance_eval(&block)
|
32
|
+
instance.init
|
33
|
+
|
34
|
+
instance
|
35
|
+
end
|
36
|
+
|
37
|
+
def init
|
38
|
+
return unless enabled
|
39
|
+
@logs = []
|
40
|
+
|
41
|
+
if Rails.env.development?
|
42
|
+
# Misc
|
43
|
+
ActiveSupport::Notifications.subscribe 'start_processing.action_controller' do |*args|
|
44
|
+
data = args.extract_options!
|
45
|
+
unless data[:controller] =~ /AwesomeExplain/ || data[:controller] =~ /ErrorsController/ || data[:path] =~ /awesome_explain/
|
46
|
+
Thread.current[:ae_controller_data] = data
|
47
|
+
end
|
48
|
+
Thread.current[:ae_session_id] = SecureRandom.uuid
|
49
|
+
end
|
50
|
+
|
51
|
+
ActiveSupport::Notifications.subscribe 'process_action.action_controller' do |*args|
|
52
|
+
Thread.current[:ae_session_id] = nil
|
53
|
+
end
|
54
|
+
|
55
|
+
# Mongoid
|
56
|
+
if Rails.const_defined?('Mongo') && Rails.const_defined?('Mongoid')
|
57
|
+
command_subscribers = Mongo::Monitoring::Global.subscribers.dig('Command')
|
58
|
+
if command_subscribers.nil? || !command_subscribers.collect(&:class).include?(AwesomeExplain::Subscribers::CommandSubscriber)
|
59
|
+
command_subscriber = AwesomeExplain::Subscribers::CommandSubscriber.new
|
60
|
+
begin
|
61
|
+
Mongoid.default_client.subscribe(Mongo::Monitoring::COMMAND, command_subscriber)
|
62
|
+
rescue => exception
|
63
|
+
Mongo::Monitoring::Global.subscribe(Mongo::Monitoring::COMMAND, command_subscriber)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# ActiveRecord
|
69
|
+
if active_record_enabled
|
70
|
+
::AwesomeExplain::Subscribers::ActiveRecordSubscriber.attach_to :active_record
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def connection
|
76
|
+
# TODO: Improve this condition
|
77
|
+
if AwesomeExplain::Config.rails4?
|
78
|
+
connection = ::ActiveRecord::Base.connection_handler.connection_pools.first.last.connection
|
79
|
+
else
|
80
|
+
connection = ::ActiveRecord::Base.connection_handler.connection_pools.select do |cp|
|
81
|
+
!cp.connection.current_database.match(/awesome_explain/)
|
82
|
+
end.first.connection
|
83
|
+
end
|
84
|
+
|
85
|
+
connection
|
86
|
+
end
|
87
|
+
|
88
|
+
def db_config
|
89
|
+
adapter == :postgres ? postgres_config : sqlite3_config
|
90
|
+
end
|
91
|
+
|
92
|
+
def postgres_config
|
93
|
+
{
|
94
|
+
ae_development: {
|
95
|
+
adapter: 'postgresql',
|
96
|
+
encoding: 'utf8',
|
97
|
+
host: postgres_host || POSTGRES_DEFAULT_HOST,
|
98
|
+
database: POSTGRES_DEV_DBNAME,
|
99
|
+
username: postgres_username || POSTGRES_DEFAULT_USERNAME,
|
100
|
+
password: postgres_password || POSTGRES_DEFAULT_PASSWORD,
|
101
|
+
pool: 5,
|
102
|
+
timeout: 5000,
|
103
|
+
},
|
104
|
+
ae_staging: {
|
105
|
+
adapter: 'postgresql',
|
106
|
+
encoding: 'utf8',
|
107
|
+
host: postgres_host || POSTGRES_DEFAULT_HOST,
|
108
|
+
database: POSTGRES_DEV_DBNAME,
|
109
|
+
username: postgres_username || POSTGRES_DEFAULT_USERNAME,
|
110
|
+
password: postgres_password || POSTGRES_DEFAULT_PASSWORD,
|
111
|
+
pool: 5,
|
112
|
+
timeout: 5000,
|
113
|
+
}
|
114
|
+
}.with_indifferent_access["ae_#{Rails.env}"]
|
115
|
+
end
|
116
|
+
|
117
|
+
def sqlite3_config
|
118
|
+
{
|
119
|
+
ae_development: {
|
120
|
+
adapter: 'sqlite3',
|
121
|
+
database: "#{db_path || './log'}/ae.db",
|
122
|
+
pool: 5,
|
123
|
+
timeout: 5000,
|
124
|
+
}
|
125
|
+
}.with_indifferent_access["ae_#{Rails.env}"]
|
126
|
+
end
|
127
|
+
|
128
|
+
def self.rails4?
|
129
|
+
Rails.version.start_with? '4'
|
130
|
+
end
|
131
|
+
|
132
|
+
def db_name=(value = DEFAULT_DB_NAME)
|
133
|
+
@db_name = "#{value.to_s}.db"
|
134
|
+
end
|
135
|
+
|
136
|
+
def db_path=(value = DEFAULT_DB_PATH)
|
137
|
+
@db_path = value
|
138
|
+
end
|
139
|
+
|
140
|
+
def rails_path=(value)
|
141
|
+
@rails_path = value
|
142
|
+
end
|
143
|
+
|
144
|
+
def enabled=(value = false)
|
145
|
+
@enabled = value
|
146
|
+
end
|
147
|
+
|
148
|
+
def active_record_enabled=(value = false)
|
149
|
+
@active_record_enabled = value
|
150
|
+
end
|
151
|
+
|
152
|
+
def include_full_plan=(value = false)
|
153
|
+
@include_full_plan = value
|
154
|
+
end
|
155
|
+
|
156
|
+
def max_limit=(value = :unlimited)
|
157
|
+
@max_limit = value
|
158
|
+
end
|
159
|
+
|
160
|
+
def app_name=(value = :rails)
|
161
|
+
@app_name = value
|
162
|
+
end
|
163
|
+
|
164
|
+
def logger=(value = nil)
|
165
|
+
@logger = value.nil? ? Logger.new(STDOUT) : value
|
166
|
+
end
|
167
|
+
|
168
|
+
def adapter=(value = :sqlite)
|
169
|
+
@adapter = value
|
170
|
+
end
|
171
|
+
|
172
|
+
def postgres_host=(value = 'localhost')
|
173
|
+
@postgres_host = value
|
174
|
+
end
|
175
|
+
|
176
|
+
def postgres_host
|
177
|
+
@postgres_host || 'localhost'
|
178
|
+
end
|
179
|
+
|
180
|
+
def postgres_username=(value = 'postgres')
|
181
|
+
@postgres_username = value
|
182
|
+
end
|
183
|
+
|
184
|
+
def postgres_username
|
185
|
+
@postgres_username || 'postgres'
|
186
|
+
end
|
187
|
+
|
188
|
+
def postgres_password=(value = 'postgres')
|
189
|
+
@postgres_password = value
|
190
|
+
end
|
191
|
+
|
192
|
+
def postgres_password
|
193
|
+
@postgres_password || 'postgres'
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
module AwesomeExplain::Insights
|
2
|
+
class ActiveRecordInsights
|
3
|
+
attr_accessor :options, :active_record_subscriber
|
4
|
+
|
5
|
+
def self.analyze(options, &block)
|
6
|
+
instance = new
|
7
|
+
instance.init
|
8
|
+
instance.options = options
|
9
|
+
block_result = instance.instance_eval(&block)
|
10
|
+
instance.tear_down
|
11
|
+
block_result
|
12
|
+
end
|
13
|
+
|
14
|
+
def init
|
15
|
+
subscribed = ::ActiveRecord::LogSubscriber.log_subscribers.select do |s|
|
16
|
+
s.is_a?(::AwesomeExplain::Subscribers::ActiveRecordPassiveSubscriber)
|
17
|
+
end.size.positive?
|
18
|
+
|
19
|
+
unless subscribed
|
20
|
+
SqlPlansInsights.clear
|
21
|
+
::AwesomeExplain::Subscribers::ActiveRecordPassiveSubscriber.attach_to(
|
22
|
+
:active_record
|
23
|
+
)
|
24
|
+
end
|
25
|
+
|
26
|
+
Thread.current['ae_analyze'] = true
|
27
|
+
Thread.current['ae_source'] = 'console'
|
28
|
+
end
|
29
|
+
|
30
|
+
def print_sql?
|
31
|
+
options.dig(:print) == true
|
32
|
+
end
|
33
|
+
|
34
|
+
def tear_down
|
35
|
+
if SqlPlansInsights.plans_stats.size.positive?
|
36
|
+
SqlPlansInsights.queries.each do |query|
|
37
|
+
puts query
|
38
|
+
end if print_sql?
|
39
|
+
|
40
|
+
table = Terminal::Table.new do |t|
|
41
|
+
t << ['Time (sec)', total_time]
|
42
|
+
t << :separator
|
43
|
+
t << ['Total Rows Planned', total_rows_planned]
|
44
|
+
t << :separator
|
45
|
+
t << ['Total Rows', total_rows]
|
46
|
+
t << :separator
|
47
|
+
t << total_loops_row
|
48
|
+
t << :separator
|
49
|
+
t << seq_scans_row
|
50
|
+
t << :separator
|
51
|
+
t << ['Tables', tables]
|
52
|
+
t << :separator
|
53
|
+
t << ['Node Types', node_types]
|
54
|
+
t << :separator
|
55
|
+
t << ['Indexes', indexes]
|
56
|
+
end
|
57
|
+
puts table
|
58
|
+
end
|
59
|
+
SqlPlansInsights.clear
|
60
|
+
Thread.current['ae_analyze'] = false
|
61
|
+
Thread.current['ae_source'] = nil
|
62
|
+
end
|
63
|
+
|
64
|
+
def total_time
|
65
|
+
stats = SqlPlansInsights.plans_stats.map do |ps|
|
66
|
+
ps.table_stats if ps.table_stats.size.positive?
|
67
|
+
end.compact
|
68
|
+
|
69
|
+
time = stats.sum do |table_stat|
|
70
|
+
table_stat.values.first.dig(:time)
|
71
|
+
end
|
72
|
+
|
73
|
+
(time / 1000).round(3)
|
74
|
+
end
|
75
|
+
|
76
|
+
def total_rows_planned
|
77
|
+
SqlPlansInsights.plans_stats.sum { |ps| ps.total_rows_planned }
|
78
|
+
end
|
79
|
+
|
80
|
+
def total_rows
|
81
|
+
SqlPlansInsights.plans_stats.sum { |ps| ps.total_rows }
|
82
|
+
end
|
83
|
+
|
84
|
+
def total_loops
|
85
|
+
SqlPlansInsights.plans_stats.sum { |ps| ps.total_loops }
|
86
|
+
end
|
87
|
+
|
88
|
+
def seq_scans
|
89
|
+
SqlPlansInsights.plans_stats.sum { |ps| ps.seq_scans }
|
90
|
+
end
|
91
|
+
|
92
|
+
def tables
|
93
|
+
stats = SqlPlansInsights.plans_stats.map do |ps|
|
94
|
+
ps.table_stats if ps.table_stats.size.positive?
|
95
|
+
end.compact
|
96
|
+
|
97
|
+
stats.inject(Hash.new(0)) do |h, s|
|
98
|
+
h[s.keys.first] += s[s.keys.first].dig(:count)
|
99
|
+
h
|
100
|
+
end&.map {|s| "#{s.first} (#{s.last})"}&.join("\n")
|
101
|
+
end
|
102
|
+
|
103
|
+
def node_types
|
104
|
+
SqlPlansInsights.plans_stats.map do |ps|
|
105
|
+
ps.node_type_stats
|
106
|
+
end.inject(Hash.new(0)) do |h, s|
|
107
|
+
h[s.keys.first] += s[s.keys.first].dig(:count)
|
108
|
+
h
|
109
|
+
end&.map {|s| "#{s.first} (#{s.last})"}&.join("\n")
|
110
|
+
end
|
111
|
+
|
112
|
+
def indexes
|
113
|
+
stats = SqlPlansInsights.plans_stats.map do |ps|
|
114
|
+
ps.index_stats if ps.index_stats.size.positive?
|
115
|
+
end.compact
|
116
|
+
|
117
|
+
stats.inject(Hash.new(0)) do |h, s|
|
118
|
+
h[s.keys.first] += s[s.keys.first].dig(:count)
|
119
|
+
h
|
120
|
+
end&.map {|s| "#{s.first} (#{s.last})"}&.join("\n")
|
121
|
+
end
|
122
|
+
|
123
|
+
def total_loops_row
|
124
|
+
color = total_loops >= 100 ? :red : :green
|
125
|
+
title = AwesomeExplain::Utils::Color.fg_color color, 'Total Loops'
|
126
|
+
value = AwesomeExplain::Utils::Color.fg_color color, total_loops.to_s
|
127
|
+
[title, value]
|
128
|
+
end
|
129
|
+
|
130
|
+
def seq_scans_row
|
131
|
+
color = seq_scans >= 1 ? :cyan : :green
|
132
|
+
title = AwesomeExplain::Utils::Color.fg_color color, 'Seq Scans'
|
133
|
+
value = AwesomeExplain::Utils::Color.fg_color color, seq_scans.to_s
|
134
|
+
[title, value]
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'awesome_explain/insights/sql_plans_insights'
|
2
|
+
require 'awesome_explain/insights/mongoid_insights'
|
3
|
+
require 'awesome_explain/insights/active_record_insights'
|
4
|
+
|
5
|
+
|
6
|
+
module AwesomeExplain::Insights
|
7
|
+
class Base
|
8
|
+
attr_accessor :command_subscriber, :metrics
|
9
|
+
|
10
|
+
def self.analyze_mongoid(metrics = [], &block)
|
11
|
+
MongoidInsights.analyze(metrics, block)
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.analyze_ar(&block)
|
15
|
+
ActiveRecordInsights.analyze(&block)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|