shiba 0.1.1 → 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
});
|