awesome_explain 0.3.0 → 1.0.0

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.
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