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.
- checksums.yaml +5 -5
- data/.github/workflows/mongodb.yml +53 -0
- data/.github/workflows/postgres.yml +56 -0
- data/.gitignore +11 -0
- data/Appraisals +11 -0
- data/Gemfile.lock +209 -49
- data/LICENSE.txt +4 -20
- data/README.md +155 -7
- data/Rakefile +35 -1
- data/app/models/awesome_explain/application_record.rb +5 -0
- data/app/models/awesome_explain/controller.rb +20 -0
- data/app/models/awesome_explain/delayed_job.rb +7 -0
- data/app/models/awesome_explain/explain.rb +23 -0
- data/app/models/awesome_explain/log.rb +7 -0
- data/app/models/awesome_explain/pg_dml_stat.rb +4 -0
- data/app/models/awesome_explain/pg_seq_scan.rb +4 -0
- data/app/models/awesome_explain/plan_node.rb +52 -0
- data/app/models/awesome_explain/plan_tree.rb +66 -0
- data/app/models/awesome_explain/sidekiq_worker.rb +7 -0
- data/app/models/awesome_explain/sql_explain.rb +14 -0
- data/app/models/awesome_explain/sql_plan_node.rb +73 -0
- data/app/models/awesome_explain/sql_plan_stats.rb +34 -0
- data/app/models/awesome_explain/sql_plan_tree.rb +133 -0
- data/app/models/awesome_explain/sql_query.rb +7 -0
- data/app/models/awesome_explain/stacktrace.rb +11 -0
- data/awesome_explain.gemspec +16 -5
- data/bin/rails +14 -0
- data/data/mongodb/customers.bson +0 -0
- data/data/mongodb/customers.metadata.json +1 -0
- data/data/mongodb/line_items.bson +0 -0
- data/data/mongodb/line_items.metadata.json +1 -0
- data/data/mongodb/orders.bson +0 -0
- data/data/mongodb/orders.metadata.json +1 -0
- data/data/mongodb/products.bson +0 -0
- data/data/mongodb/products.metadata.json +1 -0
- data/data/postgresql/dvdrental.tar +0 -0
- data/db/migrate/20200507214801_stacktraces.rb +12 -0
- data/db/migrate/20200507214949_controllers.rb +16 -0
- data/db/migrate/20200507215205_logs.rb +22 -0
- data/db/migrate/20200507215243_explains.rb +27 -0
- data/gemfiles/rails_4.gemfile +7 -0
- data/gemfiles/rails_4.gemfile.lock +208 -0
- data/gemfiles/rails_5.gemfile +7 -0
- data/gemfiles/rails_5.gemfile.lock +209 -0
- data/gemfiles/rails_6.gemfile +7 -0
- data/gemfiles/rails_6.gemfile.lock +233 -0
- data/images/universe.png +0 -0
- data/lib/awesome_explain.rb +79 -2
- data/lib/awesome_explain/config.rb +196 -0
- data/lib/awesome_explain/engine.rb +5 -0
- data/lib/awesome_explain/insights/active_record_insights.rb +137 -0
- data/lib/awesome_explain/insights/base.rb +18 -0
- data/lib/awesome_explain/insights/mongoid_insights.rb +44 -0
- data/lib/awesome_explain/insights/sql_plans_insights.rb +64 -0
- data/lib/awesome_explain/kernel.rb +17 -0
- data/lib/awesome_explain/mongodb/base.rb +4 -0
- data/lib/awesome_explain/mongodb/command_start.rb +84 -0
- data/lib/awesome_explain/mongodb/command_success.rb +58 -0
- data/lib/awesome_explain/mongodb/formatter.rb +62 -0
- data/lib/awesome_explain/mongodb/helpers.rb +71 -0
- data/lib/awesome_explain/queue/command.rb +17 -0
- data/lib/awesome_explain/queue/simple_queue.rb +88 -0
- data/lib/awesome_explain/renderers/active_record.rb +114 -0
- data/lib/awesome_explain/renderers/base.rb +2 -0
- data/lib/awesome_explain/renderers/mongoid.rb +20 -33
- data/lib/awesome_explain/sidekiq_middleware.rb +17 -0
- data/lib/awesome_explain/stats/postgresql.rb +54 -0
- data/lib/awesome_explain/subscribers/active_record_passive_subscriber.rb +82 -0
- data/lib/awesome_explain/subscribers/active_record_subscriber.rb +187 -0
- data/lib/awesome_explain/subscribers/base.rb +3 -0
- data/lib/awesome_explain/subscribers/command_subscriber.rb +53 -0
- data/lib/awesome_explain/tasks/db.rb +325 -0
- data/lib/awesome_explain/utils/color.rb +16 -0
- data/lib/awesome_explain/version.rb +1 -1
- data/lib/tasks/ae.rake +28 -0
- data/lib/tasks/awesome_explain_tasks.rake +4 -0
- metadata +242 -25
- data/.travis.yml +0 -19
data/README.md
CHANGED
@@ -1,11 +1,15 @@
|
|
1
1
|
# AwesomeExplain
|
2
2
|
|
3
|
-
|
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
|
-
|
7
|
-
|
8
|
-
|
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
|
-
##
|
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
|
-
|
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,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,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,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,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
|