shiba 0.1.1 → 0.1.2
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.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/README.md +9 -6
- data/bin/analyze +21 -60
- data/bin/explain +5 -46
- data/bin/redmine/sample_redmine.rb +19 -3
- data/lib/shiba.rb +14 -3
- data/lib/shiba/configure.rb +98 -26
- data/lib/shiba/explain.rb +29 -6
- data/lib/shiba/output.rb +15 -17
- data/lib/shiba/output/tags.yaml +8 -3
- data/lib/shiba/query.rb +15 -2
- data/lib/shiba/query_watcher.rb +8 -5
- data/lib/shiba/version.rb +1 -1
- data/web/results.html.erb +6 -3
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f6436b087e61bcce7488d2fbb04003c3fd32fb36
|
4
|
+
data.tar.gz: 348c1fa32744f1879e489fa5530294a4ea70fe4e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e84dcdbd7cb06d3a2dfa7fa87e9c0bf40e8a3059a8396284c8503599d3d94cbb4c45681b081e8074abc700a7285f64239777cd30c5d1f5eed17f6302756ac98a
|
7
|
+
data.tar.gz: be47f54c87cad50e7260d64468b2c0489f55163a57db59173c3302da9e82a9f62380b239a65eecab6d6db849b044b38d1c0821fdb66457d8497d73f29a6b34f0
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -11,17 +11,20 @@ You can run shiba either as a gem in your test suite, or as a standalone utility
|
|
11
11
|
|
12
12
|
### Gem
|
13
13
|
|
14
|
-
|
14
|
+
Install
|
15
15
|
|
16
16
|
```ruby
|
17
|
-
group
|
18
|
-
|
19
|
-
end
|
17
|
+
bundle add shiba --group "development, test"
|
18
|
+
```
|
20
19
|
|
21
|
-
|
20
|
+
Run shiba
|
22
21
|
|
22
|
+
```ruby
|
23
23
|
# Run some some code using shiba to generate a SQL report
|
24
24
|
bundle exec shiba analyze rake test:functional
|
25
|
+
|
26
|
+
# Or run a single test
|
27
|
+
bundle exec shiba analyze rails test test/controllers/users_controller_test.rb
|
25
28
|
```
|
26
29
|
|
27
30
|
### Standalone:
|
@@ -50,4 +53,4 @@ local:$ bin/analyze.rb -h 127.0.0.1 -d TESTDB -u MYSQLUSER -p MYSQLPASS -s shiba
|
|
50
53
|
|
51
54
|
local:$ jq -C -s 'sort_by(.cost) | reverse' results.json | less -R
|
52
55
|
|
53
|
-
```
|
56
|
+
```
|
data/bin/analyze
CHANGED
@@ -4,55 +4,14 @@
|
|
4
4
|
|
5
5
|
require 'bundler/setup'
|
6
6
|
require 'optionparser'
|
7
|
+
require 'shiba'
|
7
8
|
require 'shiba/configure'
|
8
9
|
require 'shiba/output'
|
9
10
|
|
10
11
|
options = {}
|
11
|
-
parser = OptionParser.new do |opts|
|
12
|
-
opts.banner = "analyze <command>. Creates report of SQL from the running process."
|
13
|
-
|
14
|
-
opts.on("-h","--host HOST", "sql host") do |h|
|
15
|
-
options["host"] = h
|
16
|
-
end
|
17
|
-
|
18
|
-
opts.on("-d","--database DATABASE", "sql database") do |d|
|
19
|
-
options["database"] = d
|
20
|
-
end
|
21
|
-
|
22
|
-
opts.on("-u","--user USER", "sql user") do |u|
|
23
|
-
options["user"] = u
|
24
|
-
end
|
25
|
-
|
26
|
-
opts.on("-p","--password PASSWORD", "sql password") do |p|
|
27
|
-
options["password"] = p
|
28
|
-
end
|
29
|
-
|
30
|
-
opts.on("-i","--index INDEX", "index of query to inspect") do |i|
|
31
|
-
options["index"] = i.to_i
|
32
|
-
end
|
33
|
-
|
34
|
-
opts.on("-l", "--limit NUM", "stop after processing NUM queries") do |l|
|
35
|
-
options["limit"] = l.to_i
|
36
|
-
end
|
37
|
-
|
38
|
-
opts.on("-s","--stats FILES", "location of index statistics tsv file") do |f|
|
39
|
-
options["stats"] = f
|
40
|
-
end
|
41
|
-
|
42
|
-
opts.on("-f", "--file FILE", "location of file containing queries") do |f|
|
43
|
-
options["file"] = f
|
44
|
-
end
|
45
|
-
|
46
|
-
opts.on("-o", "--output FILE", "write to file instead of stdout") do |f|
|
47
|
-
options["output"] = f
|
48
|
-
end
|
49
|
-
|
50
|
-
opts.on("-t", "--test", "analyze queries at --file instead of analyzing a process") do |f|
|
51
|
-
options["test"] = true
|
52
|
-
end
|
53
|
-
|
54
|
-
end
|
55
12
|
|
13
|
+
parser = Shiba::Configure.make_options_parser(options)
|
14
|
+
parser.banner = "analyze <command>. Creates report of SQL from the running process."
|
56
15
|
parser.parse!
|
57
16
|
|
58
17
|
if options["test"] && !options["file"]
|
@@ -63,24 +22,26 @@ end
|
|
63
22
|
|
64
23
|
# Automagic configuration goes here
|
65
24
|
if !options["database"]
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
end
|
25
|
+
config = Shiba::Configure.activerecord_configuration
|
26
|
+
|
27
|
+
if tc = config && config['test']
|
28
|
+
$stderr.puts "Reading configuration from '#{`pwd`.chomp}/config/database.yml'[:test]."
|
29
|
+
options['host'] ||= tc['host']
|
30
|
+
options['database'] ||= tc['database']
|
31
|
+
options['username'] ||= tc['username']
|
32
|
+
options['password'] ||= tc['password']
|
75
33
|
end
|
34
|
+
end
|
76
35
|
|
77
|
-
|
78
|
-
options["file"] = `mktemp /tmp/shiba-query.log-#{Time.now.to_i}`.chomp
|
79
|
-
end
|
36
|
+
Shiba.configure(options)
|
80
37
|
|
81
|
-
|
82
|
-
|
83
|
-
|
38
|
+
if !options["file"]
|
39
|
+
options["file"] = `mktemp /tmp/shiba-query.log-#{Time.now.to_i}`.chomp
|
40
|
+
end
|
41
|
+
|
42
|
+
if !options["explain"]
|
43
|
+
options["explain"] = `mktemp /tmp/shiba-explain.log-#{Time.now.to_i}`.chomp
|
44
|
+
end
|
84
45
|
|
85
46
|
# Log process queries
|
86
47
|
if !options.delete("test")
|
@@ -105,7 +66,7 @@ if !options.delete("test")
|
|
105
66
|
end
|
106
67
|
|
107
68
|
# Explain
|
108
|
-
$stderr.puts "Analyzing SQL to '#{options["
|
69
|
+
$stderr.puts "Analyzing SQL to '#{options["explain"]}'..."
|
109
70
|
path = "#{File.dirname(__FILE__)}/explain"
|
110
71
|
args = options.select { |_,v| !v.nil? }.map { |k,v| [ "--#{k}", v ] }.flatten
|
111
72
|
|
data/bin/explain
CHANGED
@@ -10,50 +10,9 @@ require 'shiba/output'
|
|
10
10
|
require 'optionparser'
|
11
11
|
|
12
12
|
options = {}
|
13
|
-
parser = OptionParser.new do |opts|
|
14
|
-
opts.banner = "Usage: explain -h HOST -d DB -u USER -p PASS [-f QUERY_FILE] [-s STATS_FILE] "
|
15
|
-
|
16
|
-
opts.on("-h","--host HOST", "sql host") do |h|
|
17
|
-
options["host"] = h
|
18
|
-
end
|
19
|
-
|
20
|
-
opts.on("-d","--database DATABASE", "sql database") do |d|
|
21
|
-
options["database"] = d
|
22
|
-
end
|
23
|
-
|
24
|
-
opts.on("-u","--user USER", "sql user") do |u|
|
25
|
-
options["username"] = u
|
26
|
-
end
|
27
|
-
|
28
|
-
opts.on("-p","--password PASSWORD", "sql password") do |p|
|
29
|
-
options["password"] = p
|
30
|
-
end
|
31
|
-
|
32
|
-
opts.on("-i","--index INDEX", "index of query to inspect") do |i|
|
33
|
-
options["index"] = i.to_i
|
34
|
-
end
|
35
|
-
|
36
|
-
opts.on("-l", "--limit NUM", "stop after processing NUM queries") do |l|
|
37
|
-
options["limit"] = l.to_i
|
38
|
-
end
|
39
|
-
|
40
|
-
opts.on("-s","--stats FILES", "location of index statistics tsv file") do |f|
|
41
|
-
options["stats_file"] = f
|
42
|
-
end
|
43
|
-
|
44
|
-
opts.on("-f", "--file FILE", "location of file containing queries") do |f|
|
45
|
-
options["file"] = f
|
46
|
-
end
|
47
|
-
|
48
|
-
opts.on("-o", "--output FILE", "write to file instead of stdout") do |f|
|
49
|
-
options["output"] = f
|
50
|
-
end
|
51
|
-
|
52
|
-
opts.on("--debug") do
|
53
|
-
options["debug"] = true
|
54
|
-
end
|
55
|
-
end
|
56
13
|
|
14
|
+
parser = Shiba::Configure.make_options_parser(options)
|
15
|
+
parser.banner = "Run a list of queries through shiba's analyzer."
|
57
16
|
parser.parse!
|
58
17
|
|
59
18
|
["database", "username"].each do |opt|
|
@@ -67,12 +26,12 @@ end
|
|
67
26
|
file = options.delete("file")
|
68
27
|
file = File.open(file, "r") if file
|
69
28
|
|
70
|
-
output = options.delete("
|
29
|
+
output = options.delete("explain")
|
71
30
|
output = File.open(output, 'w') if output
|
72
31
|
|
73
32
|
Shiba.configure(options)
|
74
33
|
|
75
|
-
schema_stats_fname = options["
|
34
|
+
schema_stats_fname = options["stats"]
|
76
35
|
|
77
36
|
if schema_stats_fname && !File.exist?(schema_stats_fname)
|
78
37
|
$stderr.puts "No such file: #{schema_stats_fname}"
|
@@ -102,4 +61,4 @@ file = $stdin if file.nil?
|
|
102
61
|
output = $stdout if output.nil?
|
103
62
|
|
104
63
|
queries = Shiba::Analyzer.analyze(file, output, schema_stats, options)
|
105
|
-
Shiba::Output.new(queries).make_web!
|
64
|
+
Shiba::Output.new(queries, { 'output' => options['output'] }).make_web!
|
@@ -45,7 +45,15 @@ end
|
|
45
45
|
|
46
46
|
class Watcher < Redmine; end
|
47
47
|
class Relation < Redmine; end
|
48
|
-
class User < Redmine
|
48
|
+
class User < Redmine
|
49
|
+
def enhance(hash, owner)
|
50
|
+
if rand < 0.01
|
51
|
+
hash['type'] = 'admin'
|
52
|
+
else
|
53
|
+
hash['type'] = 'user'
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
49
57
|
|
50
58
|
class Sampler
|
51
59
|
def initialize(interested_in)
|
@@ -105,9 +113,17 @@ class Sampler
|
|
105
113
|
def sample_model(model, params = {})
|
106
114
|
# go exponentially up from 1.
|
107
115
|
last = find_max(model, 0, 1, params)
|
116
|
+
|
108
117
|
# go up from the last hit value.
|
109
|
-
|
110
|
-
|
118
|
+
last = find_max(model, last.id, 1, params)
|
119
|
+
|
120
|
+
max_id = last.id
|
121
|
+
num_to_sample = (max_id * 0.05).to_i
|
122
|
+
|
123
|
+
num_to_sample.times do
|
124
|
+
instance = get_instance(model, rand(max_id), params)
|
125
|
+
extract_hash(instance) if instance
|
126
|
+
sleep(1)
|
111
127
|
end
|
112
128
|
end
|
113
129
|
|
data/lib/shiba.rb
CHANGED
@@ -1,11 +1,22 @@
|
|
1
1
|
require "shiba/version"
|
2
|
+
require "shiba/configure"
|
2
3
|
require "mysql2"
|
3
4
|
|
4
5
|
module Shiba
|
5
6
|
class Error < StandardError; end
|
6
7
|
|
7
|
-
def self.configure(
|
8
|
-
@connection_hash =
|
8
|
+
def self.configure(options)
|
9
|
+
@connection_hash = options.select { |k, v| ['username', 'database', 'host', 'password'].include?(k) }
|
10
|
+
@main_config = Configure.read_config_file(options['config'], "config/shiba.yml")
|
11
|
+
@index_config = Configure.read_config_file(options['index'], "config/shiba_index.yml")
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.config
|
15
|
+
@main_config
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.index_config
|
19
|
+
@index_config
|
9
20
|
end
|
10
21
|
|
11
22
|
def self.connection
|
@@ -18,4 +29,4 @@ module Shiba
|
|
18
29
|
end
|
19
30
|
|
20
31
|
# This goes at the end so that Shiba.root is defined.
|
21
|
-
require "shiba/railtie" if defined?(Rails)
|
32
|
+
require "shiba/railtie" if defined?(Rails)
|
data/lib/shiba/configure.rb
CHANGED
@@ -1,31 +1,103 @@
|
|
1
1
|
require 'pathname'
|
2
|
-
|
2
|
+
require 'pp'
|
3
3
|
module Shiba
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
4
|
+
module Configure
|
5
|
+
|
6
|
+
# avoiding Rails dependency on the cli tools for now.
|
7
|
+
# yanked from https://github.com/rails/rails/blob/v5.0.5/railties/lib/rails/application/configuration.rb
|
8
|
+
def self.activerecord_configuration
|
9
|
+
yaml = Pathname.new("config/database.yml")
|
10
|
+
|
11
|
+
config = if yaml && yaml.exist?
|
12
|
+
require "yaml"
|
13
|
+
require "erb"
|
14
|
+
YAML.load(ERB.new(yaml.read).result) || {}
|
15
|
+
elsif ENV['DATABASE_URL']
|
16
|
+
# Value from ENV['DATABASE_URL'] is set to default database connection
|
17
|
+
# by Active Record.
|
18
|
+
{}
|
19
|
+
end
|
20
|
+
|
21
|
+
config
|
22
|
+
rescue Psych::SyntaxError => e
|
23
|
+
raise "YAML syntax error occurred while parsing #{yaml.to_s}. " \
|
24
|
+
"Please note that YAML must be consistently indented using spaces. Tabs are not allowed. " \
|
25
|
+
"Error: #{e.message}"
|
26
|
+
rescue => e
|
27
|
+
raise e, "Cannot load `#{path}`:\n#{e.message}", e.backtrace
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.read_config_file(option_file, default)
|
31
|
+
file_to_read = nil
|
32
|
+
if option_file
|
33
|
+
if !File.exist?(option_file)
|
34
|
+
$stderr.puts "no such file: #{option_file}"
|
35
|
+
exit 1
|
36
|
+
end
|
37
|
+
file_to_read = option_file
|
38
|
+
elsif File.exist?(default)
|
39
|
+
file_to_read = default
|
40
|
+
end
|
41
|
+
|
42
|
+
if file_to_read
|
43
|
+
YAML.load_file(file_to_read)
|
44
|
+
else
|
45
|
+
{}
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.make_options_parser(options)
|
50
|
+
parser = OptionParser.new do |opts|
|
51
|
+
# note that the key to the hash needs to stay the same as the
|
52
|
+
# option name since we re-pass them
|
53
|
+
opts.on("-h","--host HOST", "sql host") do |h|
|
54
|
+
options["host"] = h
|
55
|
+
end
|
56
|
+
|
57
|
+
opts.on("-d","--database DATABASE", "sql database") do |d|
|
58
|
+
options["database"] = d
|
59
|
+
end
|
60
|
+
|
61
|
+
opts.on("-u","--username USER", "sql user") do |u|
|
62
|
+
options["username"] = u
|
63
|
+
end
|
64
|
+
|
65
|
+
opts.on("-p","--password PASSWORD", "sql password") do |p|
|
66
|
+
options["password"] = p
|
28
67
|
end
|
29
68
|
|
69
|
+
opts.on("-c","--config FILE", "location of shiba.yml") do |f|
|
70
|
+
options["config"] = f
|
71
|
+
end
|
72
|
+
|
73
|
+
opts.on("-i","--index INDEX", "location of shiba_index.yml") do |i|
|
74
|
+
options["index"] = i.to_i
|
75
|
+
end
|
76
|
+
|
77
|
+
opts.on("-l", "--limit NUM", "stop after processing NUM queries") do |l|
|
78
|
+
options["limit"] = l.to_i
|
79
|
+
end
|
80
|
+
|
81
|
+
opts.on("-s","--stats FILES", "location of index statistics tsv file") do |f|
|
82
|
+
options["stats"] = f
|
83
|
+
end
|
84
|
+
|
85
|
+
opts.on("-f", "--file FILE", "location of file containing queries") do |f|
|
86
|
+
options["file"] = f
|
87
|
+
end
|
88
|
+
|
89
|
+
opts.on("-e", "--explain FILE", "write explain JSON to file. default: stdout") do |f|
|
90
|
+
options["explain"] = f
|
91
|
+
end
|
92
|
+
|
93
|
+
opts.on("-o", "--output PATH", "path to put generated report in. default: /tmp") do |p|
|
94
|
+
options["output"] = p
|
95
|
+
end
|
96
|
+
|
97
|
+
opts.on("-t", "--test", "analyze queries at --file instead of analyzing a process") do |f|
|
98
|
+
options["test"] = true
|
99
|
+
end
|
100
|
+
end
|
30
101
|
end
|
31
|
-
end
|
102
|
+
end
|
103
|
+
end
|
data/lib/shiba/explain.rb
CHANGED
@@ -3,10 +3,10 @@ require 'shiba/index'
|
|
3
3
|
|
4
4
|
module Shiba
|
5
5
|
class Explain
|
6
|
-
def initialize(sql, stats, options = {})
|
6
|
+
def initialize(sql, stats, backtrace, options = {})
|
7
7
|
@sql = sql
|
8
|
+
@backtrace = backtrace
|
8
9
|
|
9
|
-
@sql, _, @backtrace = @sql.partition(" /*shiba")
|
10
10
|
if options[:force_key]
|
11
11
|
@sql = @sql.sub(/(FROM\s*\S+)/i, '\1' + " FORCE INDEX(`#{options[:force_key]}`)")
|
12
12
|
end
|
@@ -20,8 +20,6 @@ module Shiba
|
|
20
20
|
end
|
21
21
|
|
22
22
|
def as_json
|
23
|
-
@backtrace.chomp!("*/")
|
24
|
-
|
25
23
|
{
|
26
24
|
sql: @sql,
|
27
25
|
table: get_table,
|
@@ -30,7 +28,7 @@ module Shiba
|
|
30
28
|
cost: @cost,
|
31
29
|
used_key_parts: first['used_key_parts'],
|
32
30
|
possible_keys: first['possible_keys'],
|
33
|
-
backtrace:
|
31
|
+
backtrace: @backtrace
|
34
32
|
}
|
35
33
|
end
|
36
34
|
|
@@ -217,7 +215,7 @@ module Shiba
|
|
217
215
|
end
|
218
216
|
|
219
217
|
def estimate_row_count_with_key(key)
|
220
|
-
Explain.new(@sql, @stats, force_key: key).estimate_row_count
|
218
|
+
Explain.new(@sql, @stats, @backtrace, force_key: key).estimate_row_count
|
221
219
|
rescue Mysql2::Error => e
|
222
220
|
if /Key .+? doesn't exist in table/ =~ e.message
|
223
221
|
return nil
|
@@ -226,7 +224,32 @@ module Shiba
|
|
226
224
|
raise e
|
227
225
|
end
|
228
226
|
|
227
|
+
def ignore?
|
228
|
+
!!ignore_line_and_backtrace_line
|
229
|
+
end
|
230
|
+
|
231
|
+
def ignore_line_and_backtrace_line
|
232
|
+
ignore_files = Shiba.config['ignore']
|
233
|
+
if ignore_files
|
234
|
+
ignore_files.each do |i|
|
235
|
+
file, method = i.split('#')
|
236
|
+
@backtrace.each do |b|
|
237
|
+
next unless b.include?(file)
|
238
|
+
next if method && !b.include?(method)
|
239
|
+
return [i, b]
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
243
|
+
nil
|
244
|
+
end
|
245
|
+
|
229
246
|
def run_checks!
|
247
|
+
if ignore?
|
248
|
+
@cost = 0
|
249
|
+
messages << "ignored"
|
250
|
+
return
|
251
|
+
end
|
252
|
+
|
230
253
|
@cost = estimate_row_count
|
231
254
|
end
|
232
255
|
end
|
data/lib/shiba/output.rb
CHANGED
@@ -5,28 +5,26 @@ require 'erb'
|
|
5
5
|
|
6
6
|
module Shiba
|
7
7
|
class Output
|
8
|
-
|
9
8
|
OUTPUT_PATH = "/tmp/shiba_results"
|
10
|
-
JS_PATH = OUTPUT_PATH + "/js"
|
11
9
|
|
12
10
|
WEB_PATH = File.dirname(__FILE__) + "/../../web"
|
13
11
|
def self.tags
|
14
12
|
@tags ||= YAML.load_file(File.dirname(__FILE__) + "/output/tags.yaml")
|
15
13
|
end
|
16
14
|
|
17
|
-
def
|
18
|
-
queries =
|
19
|
-
|
20
|
-
while line = f.gets
|
21
|
-
queries << JSON.parse(line)
|
22
|
-
end
|
23
|
-
end
|
24
|
-
new(queries)
|
15
|
+
def initialize(queries, options = {})
|
16
|
+
@queries = queries
|
17
|
+
@options = options
|
25
18
|
end
|
26
19
|
|
20
|
+
def output_path
|
21
|
+
path ||= File.join(@options['output'], "shiba_results") if @options['output']
|
22
|
+
path ||= Dir.pwd + "/log/shiba_results" if File.exist?(Dir.pwd + "/log")
|
23
|
+
path ||= OUTPUT_PATH
|
24
|
+
end
|
27
25
|
|
28
|
-
def
|
29
|
-
|
26
|
+
def js_path
|
27
|
+
File.join(output_path, "js")
|
30
28
|
end
|
31
29
|
|
32
30
|
def remote_url
|
@@ -40,11 +38,11 @@ module Shiba
|
|
40
38
|
end
|
41
39
|
|
42
40
|
def make_web!
|
43
|
-
FileUtils.mkdir_p(
|
41
|
+
FileUtils.mkdir_p(js_path)
|
44
42
|
|
45
43
|
js = Dir.glob(WEB_PATH + "/dist/*.js").map { |f| File.basename(f) }
|
46
44
|
js.each do |f|
|
47
|
-
system("cp #{WEB_PATH}/dist/#{f} #{
|
45
|
+
system("cp #{WEB_PATH}/dist/#{f} #{js_path}")
|
48
46
|
end
|
49
47
|
|
50
48
|
data = {
|
@@ -54,14 +52,14 @@ module Shiba
|
|
54
52
|
url: remote_url
|
55
53
|
}
|
56
54
|
|
57
|
-
system("cp #{WEB_PATH}/*.css #{
|
55
|
+
system("cp #{WEB_PATH}/*.css #{output_path}")
|
58
56
|
|
59
57
|
erb = ERB.new(File.read(WEB_PATH + "/../web/results.html.erb"))
|
60
|
-
File.open(
|
58
|
+
File.open(output_path + "/results.html", "w+") do |f|
|
61
59
|
f.write(erb.result(binding))
|
62
60
|
end
|
63
61
|
|
64
|
-
puts "done, results are in " + "
|
62
|
+
puts "done, results are in " + File.join(output_path, "results.html")
|
65
63
|
end
|
66
64
|
end
|
67
65
|
end
|
data/lib/shiba/output/tags.yaml
CHANGED
@@ -38,7 +38,12 @@ access_type_tablescan:
|
|
38
38
|
fuzzed_data:
|
39
39
|
title: Guessed Table Size
|
40
40
|
description: |
|
41
|
-
|
42
|
-
|
43
|
-
|
41
|
+
We're not sure how much data this table will hold in the future, so we've pretended
|
42
|
+
there's 6000 rows in it. This can lead to a lot of false positives. To
|
43
|
+
improve results, please give shiba your index statistics.
|
44
|
+
level: info
|
45
|
+
ignored:
|
46
|
+
title: Ignored Query
|
47
|
+
description: |
|
48
|
+
This query matched an "ignore" rule in shiba.yml, so we skipped any further analysis on it.
|
44
49
|
level: info
|
data/lib/shiba/query.rb
CHANGED
@@ -15,7 +15,15 @@ module Shiba
|
|
15
15
|
end
|
16
16
|
|
17
17
|
def initialize(sql, stats)
|
18
|
-
@sql = sql
|
18
|
+
@sql, _, @backtrace = sql.partition(" /*shiba")
|
19
|
+
|
20
|
+
if @backtrace.length > 0
|
21
|
+
@backtrace.chomp!("*/")
|
22
|
+
@backtrace = JSON.parse(@backtrace)
|
23
|
+
else
|
24
|
+
@backtrace = []
|
25
|
+
end
|
26
|
+
|
19
27
|
@stats = stats
|
20
28
|
@@index += 1
|
21
29
|
@index = @@index
|
@@ -23,12 +31,17 @@ module Shiba
|
|
23
31
|
|
24
32
|
attr_reader :sql, :index
|
25
33
|
|
34
|
+
|
26
35
|
def fingerprint
|
27
36
|
@fingerprint ||= self.class.get_fingerprint(@sql)
|
28
37
|
end
|
29
38
|
|
30
39
|
def explain
|
31
|
-
Explain.new(@sql, @stats)
|
40
|
+
Explain.new(@sql, @stats, @backtrace)
|
41
|
+
end
|
42
|
+
|
43
|
+
def backtrace
|
44
|
+
@backtrace
|
32
45
|
end
|
33
46
|
end
|
34
47
|
end
|
data/lib/shiba/query_watcher.rb
CHANGED
@@ -4,13 +4,14 @@ require 'rails'
|
|
4
4
|
|
5
5
|
module Shiba
|
6
6
|
class QueryWatcher
|
7
|
-
FINGERPRINTS = {}
|
8
7
|
IGNORE = /\.rvm|gem|vendor\/|rbenv|seed|db|shiba|test|spec/
|
9
8
|
|
10
9
|
def self.watch(file)
|
11
10
|
new(file).watch
|
12
11
|
end
|
13
12
|
|
13
|
+
attr_reader :queries
|
14
|
+
|
14
15
|
def initialize(file)
|
15
16
|
@file = file
|
16
17
|
# fixme mem growth on this is kinda nasty
|
@@ -23,11 +24,13 @@ module Shiba
|
|
23
24
|
sql = payload[:sql]
|
24
25
|
|
25
26
|
if sql.start_with?("SELECT")
|
26
|
-
|
27
|
-
if
|
28
|
-
|
27
|
+
fingerprint = Query.get_fingerprint(sql)
|
28
|
+
if !@queries[fingerprint]
|
29
|
+
if lines = app_backtrace
|
30
|
+
@file.puts("#{sql} /*shiba#{lines}*/")
|
31
|
+
end
|
29
32
|
end
|
30
|
-
@queries[
|
33
|
+
@queries[fingerprint] = true
|
31
34
|
end
|
32
35
|
end
|
33
36
|
end
|
data/lib/shiba/version.rb
CHANGED
data/web/results.html.erb
CHANGED
@@ -94,11 +94,13 @@
|
|
94
94
|
<query v-for="query in queries" v-bind:query="query" v-bind:key="query.sql" v-bind:tags="tags"></query>
|
95
95
|
</div>
|
96
96
|
<div class="row">
|
97
|
-
<div class="col-12">We also found {{ queriesLow.length }} queries that look fine.</div>
|
97
|
+
<div class="col-12">We also found <a href="#" v-on:click.prevent="lowExpanded = !lowExpanded">{{ queriesLow.length }} queries</a> that look fine.</div>
|
98
98
|
</div>
|
99
|
-
<
|
99
|
+
<a name="lowExapnded"></a>
|
100
|
+
<div class="queries" v-if="lowExpanded">
|
100
101
|
<query v-for="query in queriesLow" v-bind:query="query" v-bind:key="query.sql" v-bind:tags="tags"></query>
|
101
102
|
</div>
|
103
|
+
<div style="height:50px"></div>
|
102
104
|
</div>
|
103
105
|
</div>
|
104
106
|
|
@@ -188,7 +190,8 @@
|
|
188
190
|
data: {
|
189
191
|
queries: queriesByTable,
|
190
192
|
queriesLow: queriesByTableLow,
|
191
|
-
tags: data.tags
|
193
|
+
tags: data.tags,
|
194
|
+
lowExpanded: false
|
192
195
|
},
|
193
196
|
methods: { }
|
194
197
|
});
|