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