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 +4 -4
- data/Gemfile +4 -0
- data/Gemfile.lock +9 -1
- data/README.md +21 -14
- data/bin/dump_stats +8 -2
- data/lib/shiba.rb +4 -1
- data/lib/shiba/activerecord_integration.rb +71 -0
- data/lib/shiba/backtrace.rb +13 -5
- data/lib/shiba/index_stats.rb +2 -0
- data/lib/shiba/query_watcher.rb +14 -22
- data/lib/shiba/version.rb +1 -1
- metadata +3 -3
- data/lib/shiba/railtie.rb +0 -43
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1976edef35324cc1d8c62239938ef8fa2ceaa247
|
4
|
+
data.tar.gz: 26425a11975b434ca85f100e1608ac79812b2b8d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 728df84438f0eb7d40dc3c05a718ee913dafc6a7a9275859f489875e91ccf6ba73ddb5431eb61650f7e4ddf8be9b39f3f52dbfec3a8f45bfe8b893429bb77fdb
|
7
|
+
data.tar.gz: 113296428f8336a3a21e639415badf893bd0ed178c17b536aa282e4575ecd6e6b2dc385bae336ca88ce5ceafe216e3da2c2943cb615b969294d0e449779947a7
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,17 +1,24 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
shiba (0.2.
|
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
|
-
*
|
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
|
-
|
108
|
-
name:
|
114
|
+
index_users_on_email:
|
115
|
+
name: index_users_on_email
|
109
116
|
columns:
|
110
|
-
- column:
|
111
|
-
rows_per: 1
|
117
|
+
- column: email
|
118
|
+
rows_per: 1 # one row per email address (also unique)
|
112
119
|
unique: true
|
113
|
-
|
114
|
-
name:
|
120
|
+
index_users_on_organization_id:
|
121
|
+
name: index_users_on_organization_id
|
115
122
|
columns:
|
116
|
-
- column:
|
117
|
-
rows_per:
|
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
|
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
@@ -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
|
data/lib/shiba/backtrace.rb
CHANGED
@@ -2,11 +2,15 @@ require 'open3'
|
|
2
2
|
|
3
3
|
module Shiba
|
4
4
|
module Backtrace
|
5
|
-
|
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 !~
|
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!(
|
24
|
+
line.sub!(backtrace_clean_pattern, '')
|
21
25
|
line
|
22
26
|
end
|
23
27
|
|
24
28
|
protected
|
25
29
|
|
26
|
-
def self.
|
27
|
-
@
|
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
|
data/lib/shiba/index_stats.rb
CHANGED
data/lib/shiba/query_watcher.rb
CHANGED
@@ -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
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
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
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.
|
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.
|
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
|