shiba 0.2.0 → 0.2.2

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
2
  SHA1:
3
- metadata.gz: 4cd7eeaf515680a21faf4b12533d160db2b86a89
4
- data.tar.gz: c44357f6abd8e38ec6c8df9139e81826b39aabcf
3
+ metadata.gz: 1976edef35324cc1d8c62239938ef8fa2ceaa247
4
+ data.tar.gz: 26425a11975b434ca85f100e1608ac79812b2b8d
5
5
  SHA512:
6
- metadata.gz: 46d865b67f759ed0e694a9c88aaaf4dbbc932c006b2f0ea088a715d55d07d45409d441b0b518ebd10cc7cf9b5eb3dc861437f8473f1eb293c443e54373476f2b
7
- data.tar.gz: 6a5bba249b95220a9fbd8bf1f84d4c04a4a8ba74ec500ddc826994f7b2b07c7d23ef67d6e0ad9cbda9aaebc99983eb81eac3a656688bdb612bd4505168e653e4
6
+ metadata.gz: 728df84438f0eb7d40dc3c05a718ee913dafc6a7a9275859f489875e91ccf6ba73ddb5431eb61650f7e4ddf8be9b39f3f52dbfec3a8f45bfe8b893429bb77fdb
7
+ data.tar.gz: 113296428f8336a3a21e639415badf893bd0ed178c17b536aa282e4575ecd6e6b2dc385bae336ca88ce5ceafe216e3da2c2943cb615b969294d0e449779947a7
data/Gemfile CHANGED
@@ -3,3 +3,7 @@ source "https://rubygems.org"
3
3
  gem "mysql2"
4
4
  gem "byebug"
5
5
  gemspec
6
+
7
+ group :test do
8
+ gem 'activerecord'
9
+ end
data/Gemfile.lock CHANGED
@@ -1,17 +1,24 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- shiba (0.2.0)
4
+ shiba (0.2.2)
5
5
  activesupport
6
6
 
7
7
  GEM
8
8
  remote: https://rubygems.org/
9
9
  specs:
10
+ activemodel (5.2.2)
11
+ activesupport (= 5.2.2)
12
+ activerecord (5.2.2)
13
+ activemodel (= 5.2.2)
14
+ activesupport (= 5.2.2)
15
+ arel (>= 9.0)
10
16
  activesupport (5.2.2)
11
17
  concurrent-ruby (~> 1.0, >= 1.0.2)
12
18
  i18n (>= 0.7, < 2)
13
19
  minitest (~> 5.1)
14
20
  tzinfo (~> 1.1)
21
+ arel (9.0.0)
15
22
  byebug (10.0.2)
16
23
  concurrent-ruby (1.1.4)
17
24
  i18n (1.5.3)
@@ -27,6 +34,7 @@ PLATFORMS
27
34
  ruby
28
35
 
29
36
  DEPENDENCIES
37
+ activerecord
30
38
  bundler (~> 2.0)
31
39
  byebug
32
40
  mysql2
data/README.md CHANGED
@@ -5,11 +5,9 @@
5
5
  Shiba is a tool that helps catch poorly performing queries before they cause problems in production, including:
6
6
 
7
7
  * Full table scans
8
- * Non selective indexes
8
+ * Poorly performing indexes
9
9
 
10
- By default, it will pretty much only detect queries that miss indexes. As it's fed more information, it warns about advanced problems, such as queries that use indexes but are still very expensive.
11
-
12
- To help find such queries, Shiba monitors test runs for ActiveRecord queries. A warning and report are then generated. Shiba is further capable of only warning on changes that occured on a particular git branch/pull request to allow for CI integration.
10
+ By default, it will pretty much only detect queries that miss indexes. As it's fed more information, it warns about advanced problems, such as queries that use indexes but are still very expensive. To help find such queries, Shiba monitors test runs for ActiveRecord queries. A warning and report are then generated
13
11
 
14
12
  ## Installation
15
13
 
@@ -19,8 +17,17 @@ Install using bundler. Note: this gem is not designed to be run on production.
19
17
  gem 'shiba', :group => :test, :require => true
20
18
  ```
21
19
 
20
+ If this doesn't magically work, you can manually configure it using an initializer:
21
+
22
+ ```ruby
23
+ require 'shiba/activerecord_integration'
24
+ Shiba::ActiveRecordIntegration.install!
25
+ ```
26
+
22
27
  ## Usage
23
28
 
29
+ A report will only be generated when problem queries are detected.
30
+
24
31
  ```ruby
25
32
  # Install
26
33
  bundle
@@ -83,7 +90,7 @@ For smarter analysis, Shiba requires general statistics about production data, s
83
90
  This information can be obtained by running the bin/dump_stats command in production.
84
91
 
85
92
  ```console
86
- production$
93
+ production$
87
94
  git clone https://github.com/burrito-brothers/shiba.git
88
95
  cd shiba ; bundle
89
96
  bin/dump_stats DATABASE_NAME [MYSQLOPTS] > ~/shiba_index.yml
@@ -102,18 +109,18 @@ users:
102
109
  name: PRIMARY
103
110
  columns:
104
111
  - column: id
105
- rows_per: 1
112
+ rows_per: 1 # one row per unique `id`
106
113
  unique: true
107
- index_users_on_login:
108
- name: index_users_on_login
114
+ index_users_on_email:
115
+ name: index_users_on_email
109
116
  columns:
110
- - column: login
111
- rows_per: 1
117
+ - column: email
118
+ rows_per: 1 # one row per email address (also unique)
112
119
  unique: true
113
- index_users_on_created_by_id:
114
- name: index_users_on_created_by_id
120
+ index_users_on_organization_id:
121
+ name: index_users_on_organization_id
115
122
  columns:
116
- - column: created_by_id
117
- rows_per: 3
123
+ - column: organization_id
124
+ rows_per: 20% # each organization has, on average, 20% or 2000 users.
118
125
  unique: false
119
126
  ```
data/bin/dump_stats CHANGED
@@ -10,7 +10,7 @@ esac
10
10
  DATABASE=$1
11
11
  if [ -z "$DATABASE" ]
12
12
  then
13
- echo "usage: dump_stats -tables [table1,table2...] DATABASE [ ...mysql_args]"
13
+ echo "usage: dump_stats [-tables table1,table2...] DATABASE [ ...mysql_args]" >&2
14
14
  exit 1
15
15
  fi
16
16
 
@@ -23,6 +23,12 @@ shift
23
23
  MYSQL_STATS=`mktemp`
24
24
  mysql $* -ABe "select * from information_schema.statistics where table_schema = '$DATABASE'" > $MYSQL_STATS
25
25
 
26
+ if ! [ -s $MYSQL_STATS ]
27
+ then
28
+ echo "no such database: $DATABASE" >&2
29
+ exit 1
30
+ fi
31
+
26
32
  if [ "$TABLES" ]
27
33
  then
28
34
  filtered=`mktemp`
@@ -35,4 +41,4 @@ then
35
41
  MYSQL_STATS=$filtered
36
42
  fi
37
43
 
38
- bundle exec ruby -e "require 'shiba/index'; puts Shiba::Index.parse('$MYSQL_STATS').to_yaml"
44
+ bundle exec ruby -e "require 'shiba/index'; puts Shiba::Index.parse('$MYSQL_STATS').to_yaml"
data/lib/shiba.rb CHANGED
@@ -31,4 +31,7 @@ module Shiba
31
31
  end
32
32
 
33
33
  # This goes at the end so that Shiba.root is defined.
34
- require "shiba/railtie" if defined?(Rails)
34
+ if defined?(ActiveSupport.on_load)
35
+ require 'shiba/activerecord_integration'
36
+ Shiba::ActiveRecordIntegration.install!
37
+ end
@@ -0,0 +1,71 @@
1
+ require 'shiba/query_watcher'
2
+ require 'active_support/notifications'
3
+ require 'active_support/lazy_load_hooks'
4
+
5
+ module Shiba
6
+ # Integrates ActiveRecord with the Query Watcher by setting up the query log path, and the
7
+ # connection options for the explain command, which it runs when the process exits.
8
+ #
9
+ # SHIBA_OUT=<log path> and SHIBA_DEBUG=true environment variables may be set.
10
+ class ActiveRecordIntegration
11
+
12
+ attr_reader :path, :watcher
13
+
14
+ def self.install!
15
+ return false if @installed
16
+
17
+ ActiveSupport.on_load(:active_record) do
18
+ Shiba::ActiveRecordIntegration.start_watcher
19
+ end
20
+
21
+ @installed = true
22
+ end
23
+
24
+ protected
25
+
26
+ def self.start_watcher
27
+ if ENV['SHIBA_DEBUG']
28
+ $stderr.puts("starting shiba watcher")
29
+ end
30
+
31
+ path = ENV['SHIBA_OUT'] || make_tmp_path
32
+
33
+ file = File.open(path, 'a')
34
+ watcher = QueryWatcher.new(file)
35
+
36
+ ActiveSupport::Notifications.subscribe('sql.active_record', watcher)
37
+ at_exit { run_explain(file, path) }
38
+ rescue => e
39
+ $stderr.puts("Shiba failed to load")
40
+ $stderr.puts(e.message, e.backtrace.join("\n"))
41
+ end
42
+
43
+ def self.make_tmp_path
44
+ "/tmp/shiba-query.log-#{Time.now.to_i}"
45
+ end
46
+
47
+ def self.run_explain(file, path)
48
+ file.close
49
+ puts ""
50
+ cmd = "shiba explain #{database_args} --file #{path}"
51
+ if ENV['SHIBA_DEBUG']
52
+ $stderr.puts("running:")
53
+ $stderr.puts(cmd)
54
+ end
55
+ system(cmd)
56
+ end
57
+
58
+ def self.database_args
59
+ c = ActiveRecord::Base.connection.raw_connection.query_options
60
+ options = {
61
+ 'host': c[:host],
62
+ 'database': c[:database],
63
+ 'user': c[:username],
64
+ 'password': c[:password]
65
+ }
66
+
67
+ options.reject { |k,v| v.nil? }.map { |k,v| "--#{k} #{v}" }.join(" ")
68
+ end
69
+
70
+ end
71
+ end
@@ -2,11 +2,15 @@ require 'open3'
2
2
 
3
3
  module Shiba
4
4
  module Backtrace
5
- IGNORE = /\.rvm|gem|vendor\/|rbenv|seed|db|shiba|test|spec/
5
+
6
+ def self.ignore
7
+ @ignore ||= [ '.rvm', 'gem', 'vendor', 'rbenv', 'seed',
8
+ 'db', 'test', 'spec', 'lib/shiba' ]
9
+ end
6
10
 
7
11
  # 8 backtrace lines starting from the app caller, cleaned of app/project cruft.
8
12
  def self.from_app
9
- app_line_idx = caller_locations.index { |line| line.to_s !~ IGNORE }
13
+ app_line_idx = caller_locations.index { |line| line.to_s !~ ignore_pattern }
10
14
  if app_line_idx == nil
11
15
  return
12
16
  end
@@ -17,14 +21,18 @@ module Shiba
17
21
  end
18
22
 
19
23
  def self.clean!(line)
20
- line.sub!(backtrace_ignore_pattern, '')
24
+ line.sub!(backtrace_clean_pattern, '')
21
25
  line
22
26
  end
23
27
 
24
28
  protected
25
29
 
26
- def self.backtrace_ignore_pattern
27
- @roots ||= begin
30
+ def self.ignore_pattern
31
+ @pattern ||= Regexp.new(ignore.map { |word| Regexp.escape(word) }.join("|"))
32
+ end
33
+
34
+ def self.backtrace_clean_pattern
35
+ @backtrace_clean_pattern ||= begin
28
36
  paths = Gem.path
29
37
  paths << Rails.root.to_s if defined?(Rails.root)
30
38
  paths << repo_root
@@ -49,6 +49,8 @@ module Shiba
49
49
  def encode_with(coder)
50
50
  coder.map = self.to_h.stringify_keys
51
51
  coder.map.delete('table')
52
+ coder.map.delete('unique') unless unique
53
+
52
54
  coder.tag = nil
53
55
  end
54
56
  end
@@ -1,15 +1,10 @@
1
1
  require 'shiba/query'
2
2
  require 'shiba/backtrace'
3
- require 'json'
4
- require 'rails'
5
3
 
6
4
  module Shiba
5
+ # Logs ActiveRecord SELECT queries that originate from application code.
7
6
  class QueryWatcher
8
7
 
9
- def self.watch(file)
10
- new(file).tap { |w| w.watch }
11
- end
12
-
13
8
  attr_reader :queries
14
9
 
15
10
  def initialize(file)
@@ -18,22 +13,19 @@ module Shiba
18
13
  @queries = {}
19
14
  end
20
15
 
21
- # Logs ActiveRecord SELECT queries that originate from application code.
22
- def watch
23
- ActiveSupport::Notifications.subscribe('sql.active_record') do |name, start, finish, id, payload|
24
- sql = payload[:sql]
25
-
26
- if sql.start_with?("SELECT")
27
- fingerprint = Query.get_fingerprint(sql)
28
- if !@queries[fingerprint]
29
- if lines = Backtrace.from_app
30
- @file.puts("#{sql} /*shiba#{lines}*/")
31
- end
32
- end
33
- @queries[fingerprint] = true
34
- end
35
- end
16
+ def call(name, start, finish, id, payload)
17
+ sql = payload[:sql]
18
+ return if !sql.start_with?("SELECT")
19
+
20
+ fingerprint = Query.get_fingerprint(sql)
21
+ return if @queries[fingerprint]
22
+
23
+ lines = Backtrace.from_app
24
+ return if !lines
25
+
26
+ @file.puts("#{sql} /*shiba#{lines}*/")
27
+ @queries[fingerprint] = true
36
28
  end
37
29
 
38
30
  end
39
- end
31
+ end
data/lib/shiba/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Shiba
2
- VERSION = "0.2.0"
2
+ VERSION = "0.2.2"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: shiba
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ben Osheroff
@@ -88,6 +88,7 @@ files:
88
88
  - cmd/inspect.go
89
89
  - cmd/parse.go
90
90
  - lib/shiba.rb
91
+ - lib/shiba/activerecord_integration.rb
91
92
  - lib/shiba/analyzer.rb
92
93
  - lib/shiba/backtrace.rb
93
94
  - lib/shiba/checker.rb
@@ -101,7 +102,6 @@ files:
101
102
  - lib/shiba/output/tags.yaml
102
103
  - lib/shiba/query.rb
103
104
  - lib/shiba/query_watcher.rb
104
- - lib/shiba/railtie.rb
105
105
  - lib/shiba/table_stats.rb
106
106
  - lib/shiba/version.rb
107
107
  - shiba.gemspec
@@ -135,7 +135,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
135
135
  version: '0'
136
136
  requirements: []
137
137
  rubyforge_project:
138
- rubygems_version: 2.5.1
138
+ rubygems_version: 2.6.14.1
139
139
  signing_key:
140
140
  specification_version: 4
141
141
  summary: A gem that attempts to find bad queries before you shoot self in foot
data/lib/shiba/railtie.rb DELETED
@@ -1,43 +0,0 @@
1
- require 'shiba/query_watcher'
2
-
3
- class Shiba::Railtie < Rails::Railtie
4
- # Logging is enabled when:
5
- # 1. SHIBA_OUT environment variable is set to an existing file path.
6
- # 2. RSpec/MiniTest exists, in which case a fallback query log is generated at /tmp
7
- config.after_initialize do
8
- begin
9
- path = ENV['SHIBA_OUT'] || "/tmp/shiba-query.log-#{Time.now.to_i}"
10
- f = File.open(path, 'a')
11
- watcher = Shiba::QueryWatcher.watch(f)
12
- next if watcher.nil?
13
-
14
- at_exit do
15
- f.close
16
- puts ""
17
- cmd = "shiba explain #{database_args} --file #{path}"
18
- if ENV['SHIBA_DEBUG']
19
- $stderr.puts("running:")
20
- $stderr.puts(cmd)
21
- end
22
- system(cmd)
23
- end
24
-
25
- rescue => e
26
- $stderr.puts("Shiba failed to load")
27
- $stderr.puts(e.message, e.backtrace.join("\n"))
28
- end
29
- end
30
-
31
- def self.database_args
32
- c = ActiveRecord::Base.configurations['test']
33
- options = {
34
- 'host': c['host'],
35
- 'database': c['database'],
36
- 'user': c['username'],
37
- 'password': c['password']
38
- }
39
-
40
- options.reject { |k,v| v.nil? }.map { |k,v| "--#{k} #{v}" }.join(" ")
41
- end
42
-
43
- end