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
@@ -0,0 +1,44 @@
1
+ module AwesomeExplain::Insights
2
+ class MongoidInsights
3
+ attr_accessor :command_subscriber, :metrics
4
+
5
+ def self.analyze(metrics = [], &block)
6
+ instance = new
7
+ instance.init
8
+ instance.metrics = metrics
9
+ block_result = instance.instance_eval(&block)
10
+ instance.tear_down
11
+ block_result unless metrics.size.positive?
12
+ end
13
+
14
+ def init
15
+ command_subscribers = Mongoid.default_client.send(:monitoring).subscribers['Command'] || Mongo::Monitoring::Global.subscribers['Command']
16
+ @command_subscriber = command_subscribers.select do |s|
17
+ s.class == AwesomeExplain::CommandSubscriber
18
+ end.first
19
+ @command_subscriber.clear
20
+ Thread.current['ae_analyze'] = true
21
+ Thread.current['ae_source'] = 'console'
22
+ end
23
+
24
+ def tear_down
25
+ if @command_subscriber.nil?
26
+ puts 'Configure the command subscriber then try again.'
27
+ return
28
+ end
29
+
30
+ if @metrics.size.positive?
31
+ result = {}
32
+ @metrics.each do |m|
33
+ result[m] = @command_subscriber.get(m)
34
+ end
35
+
36
+ @command_subscriber.clear
37
+ return result
38
+ else
39
+ @command_subscriber.clear
40
+ end
41
+ Thread.current['ae_analyze'] = false
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,64 @@
1
+ module AwesomeExplain::Insights
2
+ class SqlPlansInsights
3
+ include Singleton
4
+
5
+ attr_accessor :plans_stats, :queries
6
+ attr_reader :mutex
7
+
8
+ def initialize
9
+ @plans_stats = []
10
+ @queries = []
11
+ @mutex = Mutex.new
12
+ end
13
+
14
+ def add(plan_stats)
15
+ with_mutex { @plans_stats << plan_stats }
16
+ end
17
+
18
+ def add_query(query)
19
+ with_mutex {
20
+ query = Niceql::Prettifier.prettify_sql query
21
+ @queries << query
22
+ }
23
+ end
24
+
25
+ def plans_stats
26
+ with_mutex { @plans_stats }
27
+ end
28
+
29
+ def queries
30
+ with_mutex { @queries }
31
+ end
32
+
33
+ def clear
34
+ plans_stats.clear
35
+ queries.clear
36
+ end
37
+
38
+ def self.add(plan_stats)
39
+ instance.add(plan_stats)
40
+ end
41
+
42
+ def self.add_query(query)
43
+ instance.add_query(query)
44
+ end
45
+
46
+ def self.plans_stats
47
+ instance.plans_stats
48
+ end
49
+
50
+ def self.queries
51
+ instance.queries
52
+ end
53
+
54
+ def self.clear
55
+ instance.clear
56
+ end
57
+
58
+ private
59
+
60
+ def with_mutex
61
+ @mutex.synchronize { yield }
62
+ end
63
+ end
64
+ end
@@ -1,9 +1,21 @@
1
1
  module Kernel
2
2
  def ae(query)
3
3
  return AwesomeExplain::Renderers::Mongoid.new(query).print if mongoid_query?(query)
4
+ return AwesomeExplain::Renderers::ActiveRecord.new(query).print if active_record_query?(query)
5
+
4
6
  query
5
7
  end
6
8
 
9
+ def analyze(&block)
10
+ ::AwesomeExplain::MongoiddInsights.analyze(&block)
11
+ end
12
+
13
+ def analyze_ar(options = {}, &block)
14
+ Thread.current['ae_analyze'] = true
15
+ Thread.current['ae_source'] = 'console'
16
+ ::AwesomeExplain::Insights::ActiveRecordInsights.analyze(options, &block)
17
+ end
18
+
7
19
  private
8
20
 
9
21
  def mongoid_query?(query)
@@ -11,4 +23,9 @@ module Kernel
11
23
  defined?(Mongoid::Criteria) &&
12
24
  (query.is_a?(Mongo::Collection::View::Aggregation) || query.is_a?(Mongoid::Criteria))
13
25
  end
26
+
27
+ def active_record_query?(query)
28
+ defined?(ActiveRecord::Relation) &&
29
+ query.is_a?(ActiveRecord::Relation)
30
+ end
14
31
  end
@@ -0,0 +1,4 @@
1
+ require 'awesome_explain/mongodb/helpers'
2
+ require 'awesome_explain/mongodb/formatter'
3
+ require 'awesome_explain/mongodb/command_start'
4
+ require 'awesome_explain/mongodb/command_success'
@@ -0,0 +1,84 @@
1
+ module AwesomeExplain::Mongodb
2
+ module CommandStart
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ def handle_command_start(event)
7
+ command = event.command
8
+ command_name = event.command_name.to_sym
9
+
10
+ if db_explain_enabled?(command_name)
11
+ case AwesomeExplain::Config.instance.enabled?
12
+ when Rails.env.development?
13
+ handle_command_start_sync(event)
14
+ when Rails.env.staging?
15
+ handle_command_start_async(event)
16
+ end
17
+ end
18
+ end
19
+
20
+ def process_command_start(event)
21
+ command = event.command
22
+ command_name = event.command_name.to_sym
23
+
24
+ begin
25
+ request_id = event.request_id
26
+ if command_name == :getMore
27
+ collection_name = event.command['collection']
28
+ else
29
+ collection_name = event.command[event.command_name]
30
+ end
31
+ @stats[:collections][collection_name] = Hash.new(0) if !@stats[:collections].include?(collection_name)
32
+ @stats[:collections][collection_name][command_name] += 1
33
+ @queries[request_id] = {
34
+ command_name: event.command_name,
35
+ command: command.include?('pipeline') ? command['pipeline'] : command.select {|k, v| COMMAND_ALLOWED_KEYS.include?(k)},
36
+ collection_name: collection_name,
37
+ stacktrace: caller,
38
+ lsid: command.dig('lsid', 'id').to_json
39
+ }.with_indifferent_access
40
+
41
+ command = event.command
42
+ if command.include?('aggregate')
43
+ command = {
44
+ 'aggregate': command['aggregate'],
45
+ 'pipeline': command['pipeline'],
46
+ 'cursor': command['cursor'],
47
+ }
48
+ end
49
+ r = ::AwesomeExplain::Renderers::Mongoid.new(nil, Mongoid.default_client.database.command({explain: command}).documents.first)
50
+ exp = AwesomeExplain::Explain.create({
51
+ collection: collection_name,
52
+ source_name: AwesomeExplain::Config.instance.app_name,
53
+ command: @queries[request_id][:command].to_json,
54
+ winning_plan: r.winning_plan_data.first,
55
+ winning_plan_raw: r.winning_plan.to_json,
56
+ used_indexes: r.winning_plan_data.last.join(', '),
57
+ duration: (r.execution_stats&.dig('executionTimeMillis').to_f/1000).round(5),
58
+ documents_returned: r.execution_stats&.dig('nReturned'),
59
+ documents_examined: r.execution_stats&.dig('totalDocsExamined'),
60
+ keys_examined: r.execution_stats&.dig('totalKeysExamined'),
61
+ rejected_plans: r.rejected_plans&.size,
62
+ session_id: Thread.current[:ae_session_id],
63
+ lsid: @queries[request_id][:lsid],
64
+ stacktrace_id: resolve_stracktrace_id(request_id),
65
+ controller_id: resolve_controller_id,
66
+ })
67
+ @queries[request_id][:explain_id] = exp&.id
68
+ @queries[request_id][:collscan] = exp&.collscan
69
+ rescue => exception
70
+ logger.warn exception.to_s
71
+ logger.warn exception.backtrace[0..5]
72
+ end
73
+ end
74
+
75
+ def handle_command_start_sync(event)
76
+ # process_command_start(event)
77
+ ::AwesomeExplain::Config.instance.queue << ::AwesomeExplain::Queue::Command.new(:process_command_start, event, self)
78
+ end
79
+
80
+ def handle_command_start_async(event)
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,58 @@
1
+ module AwesomeExplain::Mongodb
2
+ module CommandSuccess
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ def handle_command_success(event)
7
+ if db_logging_enbled?
8
+ case AwesomeExplain::Config.instance.enabled?
9
+ when Rails.env.development?
10
+ handle_command_success_sync(event)
11
+ when Rails.env.staging?
12
+ handle_command_success_async(event)
13
+ end
14
+ end
15
+ end
16
+
17
+ def process_command_success(event)
18
+ begin
19
+ command_name = event.command_name.to_sym
20
+ request_id = event.request_id
21
+ duration = event.duration.round(5)
22
+ @stats[:performed_queries][command_name] += 1
23
+ @stats[:total_duration] += duration
24
+ @queries[request_id][:duration] = duration
25
+
26
+ log = {
27
+ operation: command_name,
28
+ app_name: AwesomeExplain::Config.instance.app_name,
29
+ source_name: resolve_source_name,
30
+ collscan: @queries[request_id][:collscan],
31
+ collection: @queries[request_id][:collection_name],
32
+ duration: duration,
33
+ command: @queries[request_id][:command].to_json,
34
+ session_id: Thread.current[:ae_session_id],
35
+ lsid: @queries[request_id][:lsid],
36
+ stacktrace_id: resolve_stracktrace_id(request_id),
37
+ explain_id: @queries[request_id][:explain_id],
38
+ controller_id: resolve_controller_id,
39
+ sidekiq_worker_id: resolve_sidekiq_class_id,
40
+ }
41
+ AwesomeExplain::Log.create(log)
42
+ rescue => exception
43
+ logger.warn exception.to_s
44
+ logger.warn exception.backtrace[0..5]
45
+ end
46
+ end
47
+
48
+ def handle_command_success_sync(event)
49
+ # process_command_success(event)
50
+ ::AwesomeExplain::Config.instance.queue << ::AwesomeExplain::Queue::Command.new(:process_command_success, event, self)
51
+ end
52
+
53
+ def handle_command_success_async(event)
54
+ # TODO: How to handle using Queues
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,62 @@
1
+ module AwesomeExplain::Mongodb
2
+ module Formatter
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ def stats_table
7
+ table = Terminal::Table.new(title: 'Query Stats') do |t|
8
+ t << [
9
+ 'Total Duration',
10
+ @stats[:total_duration] >= 1 ? "#{@stats[:total_duration]} seconds".purpleish : "#{@stats[:total_duration]} seconds".green
11
+ ]
12
+ t << :separator
13
+ t << [
14
+ total_performed_queries >= 100 ? "Performed Queries [#{total_performed_queries}]".purpleish : "Performed Queries [#{total_performed_queries}]".green,
15
+ formatted_performed_queries
16
+ ]
17
+ t << :separator
18
+ t << ['Collections Queried', formatted_collections]
19
+
20
+ sq = slowest_query
21
+ if sq[:duration] >= 0.5
22
+ t << :separator
23
+ t << ["Slowest Query [#{sq.dig(:duration)}]".purpleish, sq.dig(:command).inspect]
24
+ end
25
+ end
26
+ puts table
27
+ end
28
+
29
+ def slowest_query
30
+ @queries.sort_by {|id, data| data[:duration]}.last[1]
31
+ end
32
+
33
+ def total_performed_queries
34
+ @stats[:performed_queries].sum {|op, count| count}
35
+ end
36
+
37
+ def formatted_performed_queries
38
+ find = @stats[:performed_queries][:find]
39
+ count = @stats[:performed_queries][:count]
40
+ distinct = @stats[:performed_queries][:distinct]
41
+ update = @stats[:performed_queries][:update]
42
+ insert = @stats[:performed_queries][:insert]
43
+ get_more = @stats[:performed_queries][:getMore]
44
+ delete = @stats[:performed_queries][:delete]
45
+
46
+ QUERIES.reject { |q| !@stats[:performed_queries][q].positive? }.sort_by {|q| @stats[:performed_queries][q]}.reverse.map do |q|
47
+ @stats[:performed_queries][q] >= 10 ? "#{q}: #{@stats[:performed_queries][q]}".purpleish : "#{q}: #{@stats[:performed_queries][q]}"
48
+ end.join(', ')
49
+ end
50
+
51
+ def formatted_collections
52
+ @stats[:collections].sort_by {|c, values| values.sum {|k, v| v}}.reverse.each.map do |data|
53
+ c = data[0]
54
+ sum = @stats[:collections][c].sum {|k, v| v}
55
+ cmds = @stats[:collections][c].map {|cmd, count| "#{cmd} [#{count}]"}.join(', ')
56
+ cmds = "#{c}: #{cmds}"
57
+ sum >= 10 ? cmds.purpleish : cmds
58
+ end.join("\n")
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,71 @@
1
+ module AwesomeExplain::Mongodb
2
+ module Helpers
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ def resolve_stracktrace_id(request_id)
7
+ stacktrace_str = @queries[request_id][:stacktrace]
8
+ .select {|c| c =~ /^#{Rails.root.to_s + '\/(lib|app|db)\/'}/ }
9
+ .map {|c| c.gsub Rails.root.to_s, ''}
10
+ .to_json
11
+ stacktrace = AwesomeExplain::Stacktrace.find_or_create_by({
12
+ stacktrace: stacktrace_str
13
+ })
14
+
15
+ stacktrace.id
16
+ end
17
+
18
+ def resolve_controller_id
19
+ data = controller_data
20
+ return nil unless data.present?
21
+ AwesomeExplain::Controller.find_or_create_by({
22
+ name: controller_data[:controller],
23
+ action: controller_data[:action],
24
+ path: controller_data[:path],
25
+ params: controller_data[:params].to_json,
26
+ session_id: Thread.current['ae_session_id']
27
+ }).id
28
+ end
29
+
30
+ def resolve_sidekiq_class_id
31
+ return unless Thread.current[:sidekiq_worker_class].present?
32
+ sidekiq_worker_class_str = Thread.current[:sidekiq_worker_class]
33
+ sidekiq_queue_str = Thread.current[:sidekiq_queue].to_s
34
+ sidekiq_worker = AwesomeExplain::SidekiqWorker.find_or_create_by({
35
+ worker: sidekiq_worker_class_str,
36
+ queue: sidekiq_queue_str,
37
+ jid: extract_sidekiq_jid(Thread.current[:sidekiq_job]),
38
+ params: Thread.current[:sidekiq_job].present? ? Thread.current[:sidekiq_job].to_json : {}
39
+ })
40
+
41
+ sidekiq_worker.id
42
+ end
43
+
44
+ def controller_data
45
+ Thread.current['ae_controller_data']
46
+ end
47
+
48
+ def extract_sidekiq_jid(args)
49
+ Thread.current[:sidekiq_job].dig('jid')
50
+ end
51
+
52
+ def resolve_source_name
53
+ Thread.current['ae_source'] || DEFAULT_SOURCE_NAME
54
+ end
55
+
56
+ def db_explain_enabled?(command_name)
57
+ return false if DML_COMMANDS.include?(command_name)
58
+ return false if command_name == :getMore
59
+ return true if Thread.current['ae_analyze']
60
+ return false if Rails.const_defined?('Console')
61
+ true
62
+ end
63
+
64
+ def db_logging_enbled?
65
+ return true if Thread.current['ae_analyze']
66
+ return false if Rails.const_defined?('Console')
67
+ true
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,17 @@
1
+ module AwesomeExplain::Queue
2
+ class Command
3
+ attr_writer :method_name
4
+ attr_writer :event
5
+ attr_writer :object
6
+
7
+ def initialize(method_name, event, object)
8
+ @method_name = method_name
9
+ @event = event
10
+ @object = object
11
+ end
12
+
13
+ def run
14
+ object.send method_name, event
15
+ end
16
+ end
17
+ end