shiba 0.6.4 → 0.8.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA256:
3
- metadata.gz: beb49cd39a8eee9adba9a2d1e430c8f8d17c1534fc8a2f609a0e887191bf72cd
4
- data.tar.gz: e40ba883aaed19427fcb056c88bd63fe3d8ca1bfd9851575ecba44e54769f546
2
+ SHA1:
3
+ metadata.gz: f6dbcdad59562d6337cf6357012a6d7e9e08b1c1
4
+ data.tar.gz: 6583e1f52e1d3ba5f28ed41202b03e9bf6f92124
5
5
  SHA512:
6
- metadata.gz: 1458a067bbebe0b2910a3fa34cf02fa7666042093ae575ceecc1f771845e17e2b41fff6d9cc760a17103d65ac03718017c827035a9acc2365a10ce671eb410ea
7
- data.tar.gz: c173dfa72572b36275f0622641410a06d591a78f5168ddf38ea965d6f859ae250243adbad47ff21bff8c1aaa66ded23043eedb7838208dfc1f372bc30c238e3a
6
+ metadata.gz: 4612ff6c2cdb56c0ab24ed6bd0c938bbb035de7b881b68cf12ef71fd0507152c26cb60600cd5c7e1a039419a5e83a0af65fdea8f57a0695ecaa88287d079091e
7
+ data.tar.gz: 3aa4f8e1d74ae4c50e62fd604678ef1b25db1d4007519c9e4708a442039a913bd775d37a49f43db236fc7de44ae906f9b036d0fdd7967704e7113d4cc202e414
data/.gitignore CHANGED
@@ -9,3 +9,5 @@
9
9
  .*.sw*
10
10
  node_modules
11
11
  test/database.yml
12
+ web/dist/
13
+ logs
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- shiba (0.6.4)
4
+ shiba (0.8.1)
5
5
  activesupport
6
6
  mysql2
7
7
  pg
data/README.md CHANGED
@@ -12,7 +12,7 @@ Install using bundler. Note: this gem is not designed to be run on production.
12
12
 
13
13
  ```ruby
14
14
  # Gemfile
15
- gem 'shiba', :group => :test
15
+ gem 'shiba', :group => :test, :require => 'shiba/setup'
16
16
  ```
17
17
 
18
18
  If your application lazy loads gems, you will to manually require it.
@@ -110,7 +110,7 @@ after_script:
110
110
  - bundle exec shiba review --submit
111
111
  ```
112
112
 
113
- Add the Github API token you've generated as an environment variable named `GITHUB_TOKEN` at https://travis-ci.com/{organization}/{repo}/settings.
113
+ Add the Github API token you've generated as an environment variable named `SHIBA_GITHUB_TOKEN` at https://travis-ci.com/{organization}/{repo}/settings.
114
114
 
115
115
  #### CircleCI Integration
116
116
 
@@ -123,7 +123,7 @@ To integrate with CircleCI, add this after the the test run step in `.circleci/c
123
123
  command: bundle exec shiba review --submit
124
124
  ```
125
125
 
126
- An environment variable named `GITHUB_TOKEN` will need to be configured on CircleCI under *Project settings > Environment Variables*
126
+ An environment variable named `SHIBA_GITHUB_TOKEN` will need to be configured on CircleCI under *Project settings > Environment Variables*
127
127
 
128
128
  #### Custom CI Integration
129
129
 
data/Rakefile CHANGED
@@ -2,10 +2,12 @@ require "bundler/gem_tasks"
2
2
  require 'rake/testtask'
3
3
 
4
4
  Rake::TestTask.new do |t|
5
- t.libs << "test"
6
- t.test_files = FileList['test/*_test.rb']
7
- t.verbose = true
8
- end
5
+ t.libs << "test"
6
+ t.test_files = FileList['test/*_test.rb']
7
+ t.verbose = true
8
+ end
9
+
10
+ task :default => :test
9
11
 
10
12
  task :build_web do
11
13
  Dir.chdir(File.join(File.dirname(__FILE__), "web"))
@@ -13,6 +15,14 @@ task :build_web do
13
15
  sh("npm run build")
14
16
  end
15
17
 
18
+ task :check_master do
19
+ current_branch = `git rev-parse --abbrev-ref HEAD`.chomp
20
+ if "master" != current_branch
21
+ $stderr.puts "\n===== Warning: Not on master. running on branch #{current_branch} =====\n\n"
22
+ end
23
+ end
24
+
25
+ Rake::Task[:release].prerequisites.unshift(:check_master)
16
26
  Rake::Task[:release].prerequisites.unshift(:build_web)
17
27
 
18
- task :default => :test
28
+ task :default => :test
@@ -7,8 +7,6 @@ require 'shiba/table_stats'
7
7
  require 'shiba/configure'
8
8
  require 'shiba/output'
9
9
 
10
- require 'optionparser'
11
-
12
10
  options = {}
13
11
 
14
12
  parser = Shiba::Configure.make_options_parser(options)
data/bin/review CHANGED
@@ -1,71 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  $LOAD_PATH << File.expand_path("../lib", File.dirname(__FILE__))
4
- require 'optionparser'
5
- require 'shiba/configure'
6
- require 'shiba/reviewer'
7
- require 'shiba/review/explain_diff'
8
4
  require 'shiba/review/cli'
9
- require 'json'
10
5
 
11
6
  cli = Shiba::Review::CLI.new
12
- cli.report_options("diff", "branch", "pull_request")
13
-
14
- if !cli.valid?
15
- $stderr.puts cli.failure
16
- exit 1
17
- end
18
-
19
- explain_diff = Shiba::Review::ExplainDiff.new(cli.options["file"], cli.options)
20
-
21
- problems = if explain_diff.diff_requested_by_user?
22
- result = explain_diff.result
23
-
24
- if result.message
25
- $stderr.puts result.message
26
- end
27
-
28
- if result.status == :pass
29
- exit
30
- end
31
-
32
- explain_diff.problems
33
- else
34
- explains = File.open(cli.options["file"]).each_line.map { |json| JSON.parse(json) }
35
- bad = explains.select { |explain| explain["severity"] && explain["severity"] != 'none' }
36
- bad.map { |explain| [ "#{explain["sql"]}:-2", explain ] }
37
- end
38
-
39
- repo_cmd = "git config --get remote.origin.url"
40
- repo_url = `#{repo_cmd}`.chomp
41
-
42
- if cli.options["verbose"]
43
- $stderr.puts "#{repo_cmd}\t#{repo_url}"
44
- end
45
-
46
- if repo_url.empty?
47
- $stderr.puts "'#{Dir.pwd}' does not appear to be a git repo"
48
- exit 1
49
- end
50
-
51
- # Generate comments for the problem queries
52
- reviewer = Shiba::Reviewer.new(repo_url, problems, cli.options)
53
-
54
- if !cli.options["submit"] || cli.options["verbose"]
55
- reviewer.comments.each do |c|
56
- puts "#{c[:path]}:#{c[:line]} (#{c[:position]})"
57
- puts c[:body]
58
- puts ""
59
- end
60
- end
61
-
62
- if cli.options["submit"]
63
- if reviewer.repo_host.empty? || reviewer.repo_path.empty?
64
- $stderr.puts "Invalid repo url '#{repo_url}' from git config --get remote.origin.url"
65
- exit 1
66
- end
67
-
68
- reviewer.submit
69
- end
70
-
71
- exit 2
7
+ exit cli.run
data/bin/shiba CHANGED
@@ -7,7 +7,8 @@ APP = File.basename(__FILE__)
7
7
  commands = {
8
8
  "explain" => "Generate a report from logged SQL queries",
9
9
  "review" => "Review changed files for query problems",
10
- "dump_stats" => "Collect database statistics for more accurate analysis"
10
+ "dump_stats" => "Collect database statistics for more accurate analysis",
11
+ "web" => "Generate a report from JSON-explain"
11
12
  }
12
13
 
13
14
  global = OptionParser.new do |opts|
@@ -38,4 +39,4 @@ end
38
39
 
39
40
  path = File.join(File.dirname(__FILE__), command)
40
41
 
41
- Kernel.exec(path, *ARGV)
42
+ Kernel.exec(path, *ARGV)
data/bin/web ADDED
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'shiba/output'
5
+ require 'json'
6
+ require 'optionparser'
7
+ require 'set'
8
+
9
+ options = {}
10
+
11
+ parser = OptionParser.new do |opts|
12
+ opts.on("-h", "--html FILE", "write html report here.") do |h|
13
+ options["html"] = h
14
+ end
15
+ end
16
+
17
+ parser.banner = "Shiba web: accept JSON-explain on stdin, write out an HTML report"
18
+ parser.parse!
19
+
20
+ if !options['html']
21
+ $stderr.puts parser.help
22
+ $stderr.puts "required option: --html"
23
+ exit 2
24
+ end
25
+
26
+ queries = []
27
+ query_md5s = Set.new
28
+
29
+ while line = $stdin.gets
30
+ query = JSON.parse(line)
31
+ next if query_md5s.include?(query['md5'])
32
+ queries << JSON.parse(line)
33
+ query_md5s << query['md5']
34
+ end
35
+
36
+ page = Shiba::Output.new(queries, { 'output' => options['html'] }).make_web!
37
+ if !File.exist?(page)
38
+ $stderr.puts("Failed to generate #{page}")
39
+ exit 2
40
+ end
41
+
42
+ $stderr.puts "Report available at #{page}"
43
+
@@ -75,7 +75,7 @@ module Shiba
75
75
  end
76
76
 
77
77
  def self.path
78
- @log_path ||= ENV['SHIBA_PATH'] || try_tmp || use_tmpdir
78
+ @log_path ||= ENV['SHIBA_DIR'] || try_tmp || use_tmpdir
79
79
  end
80
80
 
81
81
  private
@@ -94,10 +94,4 @@ module Shiba
94
94
  Dir.mkdir(path) if !Dir.exist?(path)
95
95
  path
96
96
  end
97
- end
98
-
99
- # This goes at the end so that Shiba.root is defined.
100
- if defined?(ActiveSupport.on_load)
101
- require 'shiba/activerecord_integration'
102
- Shiba::ActiveRecordIntegration.install!
103
- end
97
+ end
@@ -7,13 +7,17 @@ module Shiba
7
7
  # Integrates ActiveRecord with the Query Watcher by setting up the query log path, and the
8
8
  # connection options for the explain command, which it runs when the process exits.
9
9
  #
10
- # SHIBA_OUT and SHIBA_DEBUG=true environment variables may be set.
10
+ # SHIBA_DIR, SHIBA_QUERY_LOG_NAME and SHIBA_DEBUG=true environment variables may be set.
11
11
  class ActiveRecordIntegration
12
12
 
13
13
  attr_reader :path, :watcher
14
14
 
15
15
  def self.install!
16
16
  return false if @installed
17
+ if defined?(Rails.env) && Rails.env.production?
18
+ Rails.logger.error("Shiba watcher is not intended to run in production, stopping install.")
19
+ return false
20
+ end
17
21
 
18
22
  ActiveSupport.on_load(:active_record) do
19
23
  Shiba::ActiveRecordIntegration.start_watcher
@@ -32,7 +36,7 @@ module Shiba
32
36
  c = { host: cx.host, database: cx.db, username: cx.user, password: cx.pass, port: cx.port, server: 'postgres' }
33
37
  end
34
38
 
35
- options = {
39
+ {
36
40
  'host' => c[:host],
37
41
  'database' => c[:database],
38
42
  'user' => c[:username],
@@ -61,17 +65,16 @@ module Shiba
61
65
  end
62
66
 
63
67
  def self.log_path
64
- name = ENV["SHIBA_OUT"] || "query.log-#{Time.now.to_i}"
68
+ name = ENV["SHIBA_QUERY_LOG_NAME"] || "query.log-#{Time.now.to_i}"
65
69
  File.join(Shiba.path, name)
66
70
  end
67
71
 
68
72
  def self.run_explain(file, path)
69
73
  file.close
70
- puts ""
71
74
 
72
75
  cmd = "shiba explain #{database_args} --file #{path}"
73
- if ENV['SHIBA_OUT']
74
- cmd << " --json #{File.join(Shiba.path, "#{ENV["SHIBA_OUT"]}.json")}"
76
+ if ENV['SHIBA_QUERY_LOG_NAME']
77
+ cmd << " --json #{File.join(Shiba.path, "#{ENV["SHIBA_QUERY_LOG_NAME"]}.json")}"
75
78
  elsif Shiba::Configure.ci?
76
79
  cmd << " --json #{File.join(Shiba.path, 'ci.json')}"
77
80
  end
@@ -129,6 +129,12 @@ module Shiba
129
129
  end
130
130
 
131
131
  @cost = Shiba::Explain::COST_PER_ROW_READ * rows_read
132
+
133
+ # pin fully missed indexes to a 'low' threshold
134
+ if @access_type == 'access_type_tablescan'
135
+ @cost = [0.01, @cost].max
136
+ end
137
+
132
138
  @result.cost += @cost
133
139
 
134
140
  @tbl_message['cost'] = @cost
@@ -61,7 +61,11 @@ module Shiba
61
61
  def make_web!
62
62
  data = as_json
63
63
 
64
- index = File.read(File.join(WEB_PATH, "dist", "index.html"))
64
+ index_path = File.join(WEB_PATH, "dist", "index.html")
65
+ if !File.exist?(index_path)
66
+ raise Shiba::Error.new("dist/index.html not found. Try running 'rake build_web'")
67
+ end
68
+ index = File.read(index_path)
65
69
  data_block = "var shibaData = #{JSON.dump(as_json)};\n"
66
70
 
67
71
  index.sub!(%r{<script src=(.*?)>}) do |match|
@@ -52,7 +52,10 @@ module Shiba
52
52
  tables[table] << col
53
53
  elsif sc.scan(/(\d+|NULL|'.*?') AS `(.*?)`/m)
54
54
  else
55
- raise "unknown stuff: in #{@sql}: #{@sc.rest}"
55
+ if ENV['SHIBA_DEBUG']
56
+ raise Shiba::Error.new("unknown stuff: in #{@sql}: #{@sc.rest}")
57
+ end
58
+ return {}
56
59
  end
57
60
 
58
61
  sc.scan(/,/)
@@ -1,3 +1,9 @@
1
+ require 'optionparser'
2
+ require 'shiba/configure'
3
+ require 'shiba/reviewer'
4
+ require 'shiba/review/explain_diff'
5
+ require 'json'
6
+
1
7
  module Shiba
2
8
  module Review
3
9
  # Builds options for interacting with the reviewer via the command line.
@@ -18,13 +24,108 @@ module Shiba
18
24
  # => "An error message with command line help."
19
25
  class CLI
20
26
 
21
- attr_reader :errors
27
+ attr_reader :out, :err, :input
22
28
 
23
- def initialize
24
- @user_options = {}
29
+ # Options may be provided for testing, in which case the option parser is skipped.
30
+ # When this happens, default options are also skipped.
31
+ def initialize(out: $stdout, err: $stderr, input: $stdin, options: nil)
32
+ @out = out
33
+ @err = err
34
+ @input = input
35
+ @user_options = options || {}
25
36
  @errors = []
26
- parser.parse!
27
- @options = default_options.merge(@user_options)
37
+ parser.parse! if options.nil?
38
+ @options = options || default_options.merge(@user_options)
39
+ end
40
+
41
+ # Generates the review, returning an exit status code.
42
+ # Prints to @out / @err, which default to STDOUT/STDERR.
43
+ def run
44
+ report_options("diff", "branch", "pull_request")
45
+
46
+ if !valid?
47
+ err.puts failure
48
+ return 1
49
+ end
50
+
51
+ explain_diff = Shiba::Review::ExplainDiff.new(options["file"], options)
52
+
53
+ problems = if explain_diff.diff_requested_by_user?
54
+ result = explain_diff.result
55
+
56
+ if result.message
57
+ @err.puts result.message
58
+ end
59
+
60
+ if result.status == :pass
61
+ return 0
62
+ end
63
+
64
+ explain_diff.problems
65
+ else
66
+ # Find all problem explains
67
+ begin
68
+ explains = explain_file.each_line.map { |json| JSON.parse(json) }
69
+ bad = explains.select { |explain| explain["severity"] && explain["severity"] != 'none' }
70
+ bad.map { |explain| [ "#{explain["sql"]}:-2", explain ] }
71
+ rescue Interrupt
72
+ @err.puts "SIGINT: Canceled reading from STDIN. To read from an explain log, provide the --file option."
73
+ exit 1
74
+ end
75
+ end
76
+
77
+ if problems.empty?
78
+ return 0
79
+ end
80
+
81
+ # Dedup
82
+ problems.uniq! { |_,p| p["md5"] }
83
+
84
+ # Output problem explains, this can be provided as a file to shiba review for comments.
85
+ if options["raw"]
86
+ pr = options["pull_request"]
87
+ if pr
88
+ problems.each { |_,problem| problem["pull_request"] = pr }
89
+ end
90
+
91
+
92
+ problems.each { |_,problem| @out.puts JSON.dump(problem) }
93
+ return 2
94
+ end
95
+
96
+ # Generate comments for the problem queries
97
+ repo_cmd = "git config --get remote.origin.url"
98
+ repo_url = `#{repo_cmd}`.chomp
99
+
100
+ if options["verbose"]
101
+ @err.puts "#{repo_cmd}\t#{repo_url}"
102
+ end
103
+
104
+ if repo_url.empty?
105
+ @err.puts "'#{Dir.pwd}' does not appear to be a git repo"
106
+ return 1
107
+ end
108
+
109
+ reviewer = Shiba::Reviewer.new(repo_url, problems, options)
110
+
111
+ if !options["submit"] || options["verbose"]
112
+ reviewer.comments.each do |c|
113
+ @out.puts "#{c[:path]}:#{c[:line]} (#{c[:position]})"
114
+ @out.puts c[:body]
115
+ @out.puts ""
116
+ end
117
+ end
118
+
119
+ if options["submit"]
120
+ if reviewer.repo_host.empty? || reviewer.repo_path.empty?
121
+ @err.puts "Invalid repo url '#{repo_url}' from git config --get remote.origin.url"
122
+ return 1
123
+ end
124
+
125
+ reviewer.submit
126
+ end
127
+
128
+ return 2
28
129
  end
29
130
 
30
131
  def options
@@ -36,14 +137,12 @@ module Shiba
36
137
 
37
138
  validate_log_path
38
139
  #validate_git_repo if branch || options["submit"]
39
- description = "Provide an explain log, or run 'shiba explain' to generate one."
40
-
41
- require_option("file", description: description)
42
140
 
43
141
  if options["submit"]
44
142
  require_option("branch") if options["diff"].nil?
45
- require_option("token")
143
+ require_option("token", description: "This can be read from the $SHIBA_GITHUB_TOKEN environment variable.")
46
144
  require_option("pull_request")
145
+ error("Must specify either 'submit' or 'raw' output option, not both") if options["raw"]
47
146
  end
48
147
 
49
148
  @errors.empty?
@@ -69,16 +168,25 @@ module Shiba
69
168
 
70
169
  protected
71
170
 
171
+ def explain_file
172
+ options.key?('file') ? File.open(options['file']) : @input
173
+ end
174
+
72
175
  def parser
73
176
  @parser ||= OptionParser.new do |opts|
74
- opts.banner = "Review changes for query problems. Optionally submit the comments to a Github pull request."
177
+ opts.banner = "Reads from a file or stdin to review changes for query problems. Optionally submit the comments to a Github pull request."
75
178
 
76
- opts.separator "Required:"
179
+ opts.separator ""
180
+ opts.separator "IO options:"
77
181
 
78
- opts.on("-f","--file FILE", "The explain output log to compare with. Automatically configured when $CI environment variable is set") do |f|
182
+ opts.on("-f","--file FILE", "The JSON explain log to compare with. Automatically configured when $CI environment variable is set") do |f|
79
183
  @user_options["file"] = f
80
184
  end
81
185
 
186
+ opts.on("--raw", "Print the raw JSON with the pull request id") do |r|
187
+ @user_options["raw"] = r
188
+ end
189
+
82
190
  opts.separator ""
83
191
  opts.separator "Git diff options:"
84
192
 
@@ -105,7 +213,7 @@ module Shiba
105
213
  @user_options["pull_request"] = p
106
214
  end
107
215
 
108
- opts.on("-t", "--token TOKEN", "The Github API token to use for commenting. Defaults to $GITHUB_TOKEN.") do |t|
216
+ opts.on("-t", "--token TOKEN", "The Github API token to use for commenting. Defaults to $SHIBA_GITHUB_TOKEN.") do |t|
109
217
  @user_options["token"] = t
110
218
  end
111
219
 
@@ -117,13 +225,13 @@ module Shiba
117
225
  end
118
226
 
119
227
  opts.on_tail("-h", "--help", "Show this message") do
120
- puts opts
228
+ @out.puts opts
121
229
  exit
122
230
  end
123
231
 
124
232
  opts.on_tail("--version", "Show version") do
125
233
  require 'shiba/version'
126
- puts Shiba::VERSION
234
+ @out.puts Shiba::VERSION
127
235
  exit
128
236
  end
129
237
  end
@@ -139,12 +247,12 @@ module Shiba
139
247
  if Shiba::Configure.ci?
140
248
  report("Finding default options from CI environment.")
141
249
 
142
- defaults["file"] = ci_explain_log_path
143
- defaults["pull_request"] = ci_pull_request
144
- defaults["branch"] = ci_branch if !defaults['diff']
250
+ defaults["file"] = ci_explain_log_path if ci_explain_log_path
251
+ defaults["pull_request"] = ci_pull_request if ci_pull_request
252
+ defaults["branch"] = ci_branch if !defaults['diff'] && ci_branch
145
253
  end
146
254
 
147
- defaults["token"] = ENV['GITHUB_TOKEN'] if ENV['GITHUB_TOKEN']
255
+ defaults["token"] = ENV['SHIBA_GITHUB_TOKEN'] if ENV['SHIBA_GITHUB_TOKEN']
148
256
 
149
257
  defaults
150
258
  end
@@ -157,7 +265,8 @@ module Shiba
157
265
  end
158
266
 
159
267
  def ci_explain_log_path
160
- File.join(Shiba.path, 'ci.json')
268
+ name = ENV['SHIBA_QUERY_LOG_NAME'] || 'ci'
269
+ File.join(Shiba.path, "#{name}.json")
161
270
  end
162
271
 
163
272
  def ci_branch
@@ -184,7 +293,7 @@ module Shiba
184
293
  end
185
294
 
186
295
  def report(message)
187
- $stderr.puts message if @user_options["verbose"]
296
+ @err.puts message if @user_options["verbose"]
188
297
  end
189
298
 
190
299
  def error(message, help: false)