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
data/README.md CHANGED
@@ -1,11 +1,15 @@
1
1
  # AwesomeExplain
2
2
 
3
- Awesome explain is a simple global method that provides quick insights into mongodb's query plan and execution stats.
4
- Currently the explain functionality only supports `Mongo::Collection::View::Aggregation` & ``Mongoid::Criteria`.
3
+ AwesomeExplain provides the same APM's level of query analysis under your development and test Rails environments.
5
4
 
6
- [![Build Status](https://travis-ci.com/sandboxws/awesome_explain.svg?branch=master)](https://travis-ci.com/sandboxws/awesome_explain)
7
- [![Maintainability](https://api.codeclimate.com/v1/badges/75e1a5cb4b6a5c1ba4c8/maintainability)](https://codeclimate.com/github/sandboxws/awesome_explain/maintainability)
8
- [![Test Coverage](https://api.codeclimate.com/v1/badges/75e1a5cb4b6a5c1ba4c8/test_coverage)](https://codeclimate.com/github/sandboxws/awesome_explain/test_coverage)
5
+ ## Main Features
6
+
7
+ * A set of utilities for analyzing MongoDB and SQL queries from Rails console.
8
+ * Tracking queries under your database of choice (SQLite3 or PostgreSQL)
9
+ which can be viewed under [Athena's](https://github.com/sandboxws/athena_dashboard) dashboard.
10
+
11
+ ![Build Status](https://github.com/sandboxws/awesome_explain/actions/workflows/mongodb.yml/badge.svg)
12
+ ![Build Status](https://github.com/sandboxws/awesome_explain/actions/workflows/postgres.yml/badge.svg)
9
13
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
10
14
 
11
15
  ## Installation
@@ -14,7 +18,21 @@ Add the following line to your application's Gemfile:
14
18
 
15
19
  `gem 'awesome_explain', require: true`
16
20
 
17
- ## Usage
21
+ ## Console Utility Methods
22
+
23
+ * **ae**: Prints a query's execution plan using a user friendly terminal table
24
+ * Currently supports the following classes:
25
+ * `Mongo::Collection::View::Aggregation`
26
+ * `Mongoid::Criteria`
27
+ * `ActiveRecord::Relation`
28
+ * **analyze**: Prints a summary of all MongoDB queries passed to the Ruby block
29
+ * **analyze_ar**: Prints a summary of all ActiveRecord queries passed to the Ruby block
30
+
31
+ *Detailed usage examples can be found below.*
32
+
33
+ ## MongoDB
34
+
35
+ ### Usage
18
36
 
19
37
  `ae Article.where(author_id: '5b9ec484d5cc2e697189d7c9')`
20
38
 
@@ -61,7 +79,29 @@ Add the following line to your application's Gemfile:
61
79
  +--------------------+-------------------------------------------------------------------------------------------------------------------------------------------+
62
80
  ```
63
81
 
64
- ## Winning Plan Examples
82
+ `ae Product.where(_id: 22)`
83
+
84
+ ```
85
+ +--------------------+----------------+
86
+ | Winning Plan | IDHACK (1 / 1) |
87
+ +--------------------+----------------+
88
+ | Used Indexes | |
89
+ +--------------------+----------------+
90
+ | Rejected Plans | 0 |
91
+ +--------------------+----------------+
92
+ | Documents Returned | 1 |
93
+ +--------------------+----------------+
94
+ | Documents Examined | 1 |
95
+ +--------------------+----------------+
96
+ | Keys Examined | 1 |
97
+ +--------------------+----------------+
98
+ | Execution time(ms) | 90 |
99
+ +--------------------+----------------+
100
+ | Execution time(s) | 0.09 |
101
+ +--------------------+----------------+
102
+ ```
103
+
104
+ ### Winning Plan Examples
65
105
 
66
106
  `FETCH (7 / 7) -> IXSCAN (7)`
67
107
 
@@ -72,3 +112,111 @@ Below is a breakdown of the above winning plan:
72
112
 
73
113
  For information about MongoDB's explain output, please refer to the official MongoDB Explain documentation:
74
114
  https://docs.mongodb.com/manual/reference/explain-results/
115
+
116
+ ## PostgreSQL
117
+
118
+ ### ae Usage
119
+
120
+ #### Query using PK index
121
+
122
+ `ae Film.where(film_id: 1)`
123
+
124
+ ```
125
+ +--------------------+-------+
126
+ | General Stats |
127
+ +--------------------+-------+
128
+ | Table | Count |
129
+ +--------------------+-------+
130
+ | Total Rows Planned | 1 |
131
+ | Total Rows | 1 |
132
+ | Total Loops | 1 |
133
+ | Seq Scans | 0 |
134
+ | Indexes Used | 1 |
135
+ +--------------------+-------+
136
+ | Table Stats |
137
+ +--------------------+-------+
138
+ | Table | Count |
139
+ +--------------------+-------+
140
+ | film | 1 |
141
+ +--------------------+-------+
142
+ | Node Type Stats |
143
+ +--------------------+-------+
144
+ | Node Type | Count |
145
+ +--------------------+-------+
146
+ | Index Scan | 1 |
147
+ +--------------------+-------+
148
+ | Index Stats |
149
+ +--------------------+-------+
150
+ | Index Name | Count |
151
+ +--------------------+-------+
152
+ | film_pkey | 1 |
153
+ +--------------------+-------+
154
+ ```
155
+
156
+ #### Query not using any index
157
+
158
+ `ae Film.where(description: 'Alien Center')`
159
+
160
+ ```
161
+ +--------------------+-------+
162
+ | General Stats |
163
+ +--------------------+-------+
164
+ | Table | Count |
165
+ +--------------------+-------+
166
+ | Total Rows Planned | 1 |
167
+ | Total Rows | 0 |
168
+ | Total Loops | 1 |
169
+ | Seq Scans | 1 |
170
+ | Indexes Used | 0 |
171
+ +--------------------+-------+
172
+ | Table Stats |
173
+ +--------------------+-------+
174
+ | Table | Count |
175
+ +--------------------+-------+
176
+ | film | 1 |
177
+ +--------------------+-------+
178
+ | Node Type Stats |
179
+ +--------------------+-------+
180
+ | Node Type | Count |
181
+ +--------------------+-------+
182
+ | Seq Scan | 1 |
183
+ +--------------------+-------+
184
+ ```
185
+
186
+ ### analyze_ar Usage
187
+
188
+ `analyze_ar { Film.where(film_id: 1).to_a; Actor.where(last_name: 'Cage').to_a };0`
189
+
190
+ ```
191
+ +--------------------+----------------+
192
+ | Time (sec) | 0.0 |
193
+ +--------------------+----------------+
194
+ | Total Rows Planned | 3 |
195
+ +--------------------+----------------+
196
+ | Total Rows | 3 |
197
+ +--------------------+----------------+
198
+ | Total Loops | 2 |
199
+ +--------------------+----------------+
200
+ | Seq Scans | 1 |
201
+ +--------------------+----------------+
202
+ | Tables | film (1) |
203
+ | | actor (1) |
204
+ +--------------------+----------------+
205
+ | Node Types | Index Scan (1) |
206
+ | | Seq Scan (1) |
207
+ +--------------------+----------------+
208
+ | Indexes | film_pkey (1) |
209
+ +--------------------+----------------+
210
+ ```
211
+
212
+ ## Contributing
213
+
214
+ Bug reports and pull requests are welcome on GitHub at https://github.com/sandboxws/awesome_explain. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
215
+
216
+ ## License
217
+
218
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
219
+
220
+ ## Code of Conduct
221
+
222
+ Everyone interacting in the AwesomeExplain project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/sandboxws/awesome_explain/blob/master/CODE_OF_CONDUCT.md).
data/Rakefile CHANGED
@@ -1,6 +1,40 @@
1
1
  require "bundler/gem_tasks"
2
2
  require "rspec/core/rake_task"
3
+ require "wwtd/tasks"
3
4
 
4
5
  RSpec::Core::RakeTask.new(:spec)
5
6
 
6
- task :default => :spec
7
+ # task :default => :spec
8
+
9
+ begin
10
+ require 'bundler/setup'
11
+ rescue LoadError
12
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
13
+ end
14
+
15
+ require 'rdoc/task'
16
+
17
+ RDoc::Task.new(:rdoc) do |rdoc|
18
+ rdoc.rdoc_dir = 'rdoc'
19
+ rdoc.title = 'AwesomeExplain'
20
+ rdoc.options << '--line-numbers'
21
+ rdoc.rdoc_files.include('README.md')
22
+ rdoc.rdoc_files.include('lib/**/*.rb')
23
+ end
24
+
25
+ APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
26
+ # load 'rails/tasks/engine.rake'
27
+
28
+ load 'rails/tasks/statistics.rake'
29
+
30
+ require 'bundler/gem_tasks'
31
+
32
+ require 'rake/testtask'
33
+
34
+ Rake::TestTask.new(:test) do |t|
35
+ t.libs << 'test'
36
+ t.pattern = 'test/**/*_test.rb'
37
+ t.verbose = false
38
+ end
39
+
40
+ task default: :test
@@ -0,0 +1,5 @@
1
+ module AwesomeExplain
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -0,0 +1,20 @@
1
+ class AwesomeExplain::Controller < ActiveRecord::Base
2
+ establish_connection AwesomeExplain::Config.instance.db_config
3
+ self.table_name = 'controllers'
4
+
5
+ has_many :logs
6
+ has_many :explains
7
+
8
+ def total_duration
9
+ logs.sum(:duration).round(3)
10
+ end
11
+
12
+ def gql?
13
+ path =~ /graphql/ || name =~ /GraphqlController/
14
+ end
15
+
16
+ def formatted_params
17
+ h_params = JSON.parse(params)
18
+ gql? ? h_params.dig('graphql') : h_params
19
+ end
20
+ end
@@ -0,0 +1,7 @@
1
+ class AwesomeExplain::DelayedJob < ActiveRecord::Base
2
+ establish_connection AwesomeExplain::Config.instance.db_config
3
+ self.table_name = 'delayed_jobs'
4
+
5
+ has_many :logs
6
+ has_many :sql_queries
7
+ end
@@ -0,0 +1,23 @@
1
+ class AwesomeExplain::Explain < ActiveRecord::Base
2
+ establish_connection AwesomeExplain::Config.instance.db_config
3
+ self.table_name = 'explains'
4
+
5
+ belongs_to :stacktrace
6
+ before_create :init_collscan
7
+
8
+ def to_s
9
+ "collection: #{collection}, winning_plan: #{winning_plan}, duration: #{duration}, documents_returned: #{documents_returned}, documents_examined: #{documents_examined}"
10
+ end
11
+
12
+ def init_collscan
13
+ self.collscan = winning_plan_tree.collscan?
14
+ end
15
+
16
+ def treeviz
17
+ winning_plan_tree.treeviz.to_json
18
+ end
19
+
20
+ def winning_plan_tree
21
+ @tree ||= AwesomeExplain::PlanTree.build(JSON.parse(winning_plan_raw).with_indifferent_access)
22
+ end
23
+ end
@@ -0,0 +1,7 @@
1
+ class AwesomeExplain::Log < ActiveRecord::Base
2
+ establish_connection AwesomeExplain::Config.instance.db_config
3
+ self.table_name = 'logs'
4
+
5
+ belongs_to :stacktrace
6
+ belongs_to :explain
7
+ end
@@ -0,0 +1,4 @@
1
+ class AwesomeExplain::PgDmlStat < ActiveRecord::Base
2
+ establish_connection AwesomeExplain::Config.instance.db_config
3
+ self.table_name = 'pg_dml_stats'
4
+ end
@@ -0,0 +1,4 @@
1
+ class AwesomeExplain::PgSeqScan < ActiveRecord::Base
2
+ establish_connection AwesomeExplain::Config.instance.db_config
3
+ self.table_name = 'pg_seq_scans'
4
+ end
@@ -0,0 +1,52 @@
1
+ module AwesomeExplain
2
+ class PlanNode
3
+ attr_accessor :id,
4
+ :parent,
5
+ :children,
6
+ :label,
7
+ :documents_returned,
8
+ :n_returned,
9
+ :documents_examined,
10
+ :duration,
11
+ :keys_examined,
12
+ :index_name,
13
+ :treeviz
14
+
15
+ def self.build(data, parent = nil)
16
+ instance = PlanNode.new
17
+ instance.label = data.dig(:stage)
18
+ instance.documents_returned = data.dig(:docsReturned)
19
+ instance.n_returned = data.dig(:nReturned)
20
+ instance.documents_examined = data.dig(:docsExamined)
21
+ instance.keys_examined = data.dig(:keysExamined)
22
+ instance.duration = data.dig(:executionTimeMillisEstimate)
23
+ instance.index_name = data.dig(:indexName)
24
+ instance.parent = parent
25
+ instance.children = []
26
+ instance
27
+ end
28
+
29
+ def collscan?
30
+ label.downcase == 'collscan'
31
+ end
32
+
33
+ def treeviz
34
+ { id: id, text_1: label, text_2: meta_data_str, father: parent&.id, color: '#ffffff' }
35
+ end
36
+
37
+ def meta_data_str
38
+ meta_data.join('<hr />')
39
+ end
40
+
41
+ def meta_data
42
+ data = []
43
+ data << "<strong>Docs Returned:</strong> #{documents_returned}" if documents_returned.present?
44
+ data << "<strong>N Returned:</strong> #{n_returned}" if n_returned.present?
45
+ data << "<strong>Docs Examined:</strong> #{documents_examined}" if documents_examined.present?
46
+ data << "<strong>Keys Examined</strong> #{keys_examined}" if keys_examined.present?
47
+ data << "<strong>Duration</strong> #{duration}" if duration.present?
48
+ data << "<strong>Index</strong> #{index_name}" if index_name.present?
49
+ data
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,66 @@
1
+ module AwesomeExplain
2
+ class PlanTree
3
+ attr_accessor :root, :ids, :stages_count, :collscan
4
+
5
+ def collscan?
6
+ collscan
7
+ end
8
+
9
+ def self.build(plan)
10
+ tree = PlanTree.new
11
+ tree.ids = (2..500).to_a
12
+ root = PlanNode.build(plan)
13
+ root.id = 1
14
+ tree.stages_count = 1
15
+ build_recursive(plan.dig('inputStage'), root, tree)
16
+ tree.root = root
17
+ tree
18
+ end
19
+
20
+ def self.build_recursive(data, parent, tree)
21
+ return unless data.present?
22
+ if data.dig('inputStages').present?
23
+ # Parent doesn't change
24
+ data.dig('inputStages').each do |stage|
25
+ node = PlanNode.build(stage, parent)
26
+ node.id = tree.ids.shift
27
+ parent.children << node
28
+ tree.stages_count += 1
29
+ tree.collscan = 1 if node.collscan? && !tree.collscan
30
+ build_recursive(stage.dig('inputStage'), node, tree)
31
+ end
32
+ elsif data.dig('inputStage').present?
33
+ # Parent changes
34
+ node = PlanNode.build(data, parent)
35
+ node.id = tree.ids.shift
36
+ parent.children << node
37
+ tree.stages_count += 1
38
+ tree.collscan = 1 if node.collscan? && !tree.collscan
39
+ build_recursive(data.dig('inputStage'), node, tree)
40
+ elsif data.dig('inputStage').nil?
41
+ # Parent doesn't change
42
+ node = PlanNode.build(data, parent)
43
+ node.id = tree.ids.shift
44
+ tree.stages_count += 1
45
+ tree.collscan = 1 if node.collscan? && !tree.collscan
46
+ parent.children << node
47
+ end
48
+ end
49
+
50
+ # Breadth First Traversal
51
+ def treeviz
52
+ return unless root.present?
53
+ output = []
54
+ queue = [root]
55
+ while(!queue.empty?) do
56
+ node = queue.shift
57
+ output << node.treeviz
58
+ node.children.each do |child|
59
+ queue << child
60
+ end
61
+ end
62
+
63
+ output
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,7 @@
1
+ class AwesomeExplain::SidekiqWorker < ActiveRecord::Base
2
+ establish_connection AwesomeExplain::Config.instance.db_config
3
+ self.table_name = 'sidekiq_workers'
4
+
5
+ has_many :logs
6
+ has_many :sql_queries
7
+ end
@@ -0,0 +1,14 @@
1
+ class AwesomeExplain::SqlExplain < ActiveRecord::Base
2
+ establish_connection AwesomeExplain::Config.instance.db_config
3
+ self.table_name = 'sql_explains'
4
+
5
+ belongs_to :stacktrace
6
+
7
+ def tree
8
+ @tree ||= ::AwesomeExplain::SqlPlanTree.build(JSON.parse(explain_output))
9
+ end
10
+
11
+ def tree_root
12
+ tree.root
13
+ end
14
+ end