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