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.
Files changed (78) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/mongodb.yml +53 -0
  3. data/.github/workflows/postgres.yml +56 -0
  4. data/.gitignore +11 -0
  5. data/Appraisals +11 -0
  6. data/Gemfile.lock +209 -49
  7. data/LICENSE.txt +4 -20
  8. data/README.md +155 -7
  9. data/Rakefile +35 -1
  10. data/app/models/awesome_explain/application_record.rb +5 -0
  11. data/app/models/awesome_explain/controller.rb +20 -0
  12. data/app/models/awesome_explain/delayed_job.rb +7 -0
  13. data/app/models/awesome_explain/explain.rb +23 -0
  14. data/app/models/awesome_explain/log.rb +7 -0
  15. data/app/models/awesome_explain/pg_dml_stat.rb +4 -0
  16. data/app/models/awesome_explain/pg_seq_scan.rb +4 -0
  17. data/app/models/awesome_explain/plan_node.rb +52 -0
  18. data/app/models/awesome_explain/plan_tree.rb +66 -0
  19. data/app/models/awesome_explain/sidekiq_worker.rb +7 -0
  20. data/app/models/awesome_explain/sql_explain.rb +14 -0
  21. data/app/models/awesome_explain/sql_plan_node.rb +73 -0
  22. data/app/models/awesome_explain/sql_plan_stats.rb +34 -0
  23. data/app/models/awesome_explain/sql_plan_tree.rb +133 -0
  24. data/app/models/awesome_explain/sql_query.rb +7 -0
  25. data/app/models/awesome_explain/stacktrace.rb +11 -0
  26. data/awesome_explain.gemspec +16 -5
  27. data/bin/rails +14 -0
  28. data/data/mongodb/customers.bson +0 -0
  29. data/data/mongodb/customers.metadata.json +1 -0
  30. data/data/mongodb/line_items.bson +0 -0
  31. data/data/mongodb/line_items.metadata.json +1 -0
  32. data/data/mongodb/orders.bson +0 -0
  33. data/data/mongodb/orders.metadata.json +1 -0
  34. data/data/mongodb/products.bson +0 -0
  35. data/data/mongodb/products.metadata.json +1 -0
  36. data/data/postgresql/dvdrental.tar +0 -0
  37. data/db/migrate/20200507214801_stacktraces.rb +12 -0
  38. data/db/migrate/20200507214949_controllers.rb +16 -0
  39. data/db/migrate/20200507215205_logs.rb +22 -0
  40. data/db/migrate/20200507215243_explains.rb +27 -0
  41. data/gemfiles/rails_4.gemfile +7 -0
  42. data/gemfiles/rails_4.gemfile.lock +208 -0
  43. data/gemfiles/rails_5.gemfile +7 -0
  44. data/gemfiles/rails_5.gemfile.lock +209 -0
  45. data/gemfiles/rails_6.gemfile +7 -0
  46. data/gemfiles/rails_6.gemfile.lock +233 -0
  47. data/images/universe.png +0 -0
  48. data/lib/awesome_explain.rb +79 -2
  49. data/lib/awesome_explain/config.rb +196 -0
  50. data/lib/awesome_explain/engine.rb +5 -0
  51. data/lib/awesome_explain/insights/active_record_insights.rb +137 -0
  52. data/lib/awesome_explain/insights/base.rb +18 -0
  53. data/lib/awesome_explain/insights/mongoid_insights.rb +44 -0
  54. data/lib/awesome_explain/insights/sql_plans_insights.rb +64 -0
  55. data/lib/awesome_explain/kernel.rb +17 -0
  56. data/lib/awesome_explain/mongodb/base.rb +4 -0
  57. data/lib/awesome_explain/mongodb/command_start.rb +84 -0
  58. data/lib/awesome_explain/mongodb/command_success.rb +58 -0
  59. data/lib/awesome_explain/mongodb/formatter.rb +62 -0
  60. data/lib/awesome_explain/mongodb/helpers.rb +71 -0
  61. data/lib/awesome_explain/queue/command.rb +17 -0
  62. data/lib/awesome_explain/queue/simple_queue.rb +88 -0
  63. data/lib/awesome_explain/renderers/active_record.rb +114 -0
  64. data/lib/awesome_explain/renderers/base.rb +2 -0
  65. data/lib/awesome_explain/renderers/mongoid.rb +20 -33
  66. data/lib/awesome_explain/sidekiq_middleware.rb +17 -0
  67. data/lib/awesome_explain/stats/postgresql.rb +54 -0
  68. data/lib/awesome_explain/subscribers/active_record_passive_subscriber.rb +82 -0
  69. data/lib/awesome_explain/subscribers/active_record_subscriber.rb +187 -0
  70. data/lib/awesome_explain/subscribers/base.rb +3 -0
  71. data/lib/awesome_explain/subscribers/command_subscriber.rb +53 -0
  72. data/lib/awesome_explain/tasks/db.rb +325 -0
  73. data/lib/awesome_explain/utils/color.rb +16 -0
  74. data/lib/awesome_explain/version.rb +1 -1
  75. data/lib/tasks/ae.rake +28 -0
  76. data/lib/tasks/awesome_explain_tasks.rake +4 -0
  77. metadata +242 -25
  78. data/.travis.yml +0 -19
Binary file
@@ -1,7 +1,84 @@
1
- require 'awesome_explain/version'
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 'awesome_explain/renderers/mongoid'
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,5 @@
1
+ module AwesomeExplain
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace AwesomeExplain
4
+ end
5
+ 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