active_record-sql_analyzer 0.2.2 → 0.2.3
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/.rubocop.yml +12 -0
- data/.travis.yml +3 -5
- data/Gemfile +6 -5
- data/Rakefile +10 -3
- data/lib/active_record/sql_analyzer/cli_processor.rb +2 -2
- data/lib/active_record/sql_analyzer/configuration.rb +2 -2
- data/lib/active_record/sql_analyzer/monkeypatches/query.rb +70 -49
- data/lib/active_record/sql_analyzer/redacted_logger.rb +4 -4
- data/lib/active_record/sql_analyzer/version.rb +1 -1
- data/spec/active_record/sql_analyzer/end_to_end_spec.rb +38 -9
- data/spec/support/db_connection.rb +1 -0
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 77bac67b331f9db59ff2dbc8f57ec2bca04505c0
|
4
|
+
data.tar.gz: 870a62bd36006b4098d8e88a3a3af1cd3fa310b2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 88f4ef3bbe57d34b89194e045e2845f4fa4bd9d5800e47943b8e9b2561fc2cb9025a870d976fc36e29c43564bc98c3c3baa7497ea6bd5ae2e894b505205ab3bd
|
7
|
+
data.tar.gz: 7042bf53a81900be2c19a3bfa4e5564d2c2ca0d9fa23d3af22838e6322dd584e460db80236a5f35f1a755efba7a72781516d7c33ac9c7c3c24d2660518126cd8
|
data/.rubocop.yml
ADDED
data/.travis.yml
CHANGED
@@ -1,15 +1,13 @@
|
|
1
1
|
bundler_args: --without development
|
2
|
-
env:
|
3
|
-
global:
|
4
|
-
- JRUBY_OPTS="$JRUBY_OPTS --debug"
|
5
2
|
language: ruby
|
6
3
|
rvm:
|
7
|
-
- 2.1
|
8
4
|
- 2.2
|
9
|
-
- 2.3.
|
5
|
+
- 2.3.3
|
6
|
+
- 2.4.0-preview3
|
10
7
|
- ruby-head
|
11
8
|
matrix:
|
12
9
|
allow_failures:
|
10
|
+
- rvm: 2.4.0-preview3
|
13
11
|
- rvm: ruby-head
|
14
12
|
fast_finish: true
|
15
13
|
sudo: false
|
data/Gemfile
CHANGED
@@ -1,13 +1,14 @@
|
|
1
1
|
source "https://rubygems.org"
|
2
2
|
|
3
|
+
gem "pry-byebug"
|
3
4
|
gem "rake"
|
4
5
|
|
5
6
|
group :test do
|
6
|
-
gem
|
7
|
-
gem
|
8
|
-
gem
|
9
|
-
gem
|
10
|
-
gem
|
7
|
+
gem "mysql2", "~> 0.4", ">= 0.4.0"
|
8
|
+
gem "rspec", "~> 3.4"
|
9
|
+
gem "rubocop", "~> 0.30"
|
10
|
+
gem "sql-parser", git: "https://github.com/nerdrew/sql-parser.git"
|
11
|
+
gem "timecop", "~> 0.8"
|
11
12
|
end
|
12
13
|
|
13
14
|
gemspec
|
data/Rakefile
CHANGED
@@ -1,6 +1,13 @@
|
|
1
1
|
require 'bundler/gem_tasks'
|
2
2
|
|
3
|
-
|
4
|
-
|
3
|
+
begin
|
4
|
+
require "rubocop/rake_task"
|
5
|
+
require "rspec/core/rake_task"
|
5
6
|
|
6
|
-
|
7
|
+
RuboCop::RakeTask.new
|
8
|
+
RSpec::Core::RakeTask.new(:spec)
|
9
|
+
|
10
|
+
task default: [:rubocop, :spec]
|
11
|
+
rescue LoadError
|
12
|
+
warn "rubocop, rspec only available in development"
|
13
|
+
end
|
@@ -35,7 +35,7 @@ module ActiveRecord
|
|
35
35
|
logs.each { |l| queue << l }
|
36
36
|
|
37
37
|
# Spin up threads to start processing the queue
|
38
|
-
threads = concurrency
|
38
|
+
threads = Array.new(concurrency) do
|
39
39
|
Thread.new(queue) do |t_queue|
|
40
40
|
# Create a local copy of each definitions then merge them in
|
41
41
|
CLIProcessor.process_queue(t_queue) do |local_definitions, line|
|
@@ -63,7 +63,7 @@ module ActiveRecord
|
|
63
63
|
logs.each { |l| queue << l }
|
64
64
|
|
65
65
|
# Spin up threads to start processing the queue
|
66
|
-
threads = concurrency
|
66
|
+
threads = Array.new(concurrency) do
|
67
67
|
Thread.new(queue) do |t_queue|
|
68
68
|
# Create a local copy of the usage for each SHA then merge it in at the end
|
69
69
|
CLIProcessor.process_queue(t_queue) do |local_usage, line|
|
@@ -108,7 +108,7 @@ module ActiveRecord
|
|
108
108
|
|
109
109
|
# How many total lines to log when the caller is ambiguous
|
110
110
|
def ambiguous_backtrace_lines(lines)
|
111
|
-
if !lines.is_a?(
|
111
|
+
if !lines.is_a?(Integer)
|
112
112
|
raise ArgumentError, "Lines must be a Fixnum"
|
113
113
|
elsif lines <= 1
|
114
114
|
raise ArgumentError, "Lines cannot be <= 1"
|
@@ -129,7 +129,7 @@ module ActiveRecord
|
|
129
129
|
|
130
130
|
private
|
131
131
|
|
132
|
-
def check_proc(proc,
|
132
|
+
def check_proc(proc, _arity, msg)
|
133
133
|
if !proc.is_a?(Proc)
|
134
134
|
raise ArgumentError, "You must pass a proc"
|
135
135
|
elsif proc.arity != 1
|
@@ -8,23 +8,30 @@ module ActiveRecord
|
|
8
8
|
|
9
9
|
def execute(sql, *args)
|
10
10
|
return super unless SqlAnalyzer.config
|
11
|
+
safe_sql = nil
|
12
|
+
query_analyzer_call = nil
|
11
13
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
14
|
+
# Record "full" transactions (see below for more information about "full")
|
15
|
+
if @_query_analyzer_private_in_transaction
|
16
|
+
if @_query_analyzer_private_record_transaction
|
17
|
+
safe_sql ||= sql.encode(Encoding::UTF_8, invalid: :replace, undef: :replace)
|
18
|
+
query_analyzer_call ||= QueryAnalyzerCall.new(safe_sql, caller)
|
19
|
+
@_query_analyzer_private_transaction_queue << query_analyzer_call
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Record interesting queries
|
24
|
+
SqlAnalyzer.config[:analyzers].each do |analyzer|
|
25
|
+
if SqlAnalyzer.config[:should_log_sample_proc].call(analyzer[:name])
|
26
|
+
# This is here rather than above intentionally.
|
27
|
+
# We assume we're not going to be analyzing 100% of queries and want to only re-encode
|
28
|
+
# when it's actually relevant.
|
29
|
+
safe_sql ||= sql.encode(Encoding::UTF_8, invalid: :replace, undef: :replace)
|
30
|
+
query_analyzer_call ||= QueryAnalyzerCall.new(safe_sql, caller)
|
31
|
+
|
32
|
+
if safe_sql =~ analyzer[:table_regex]
|
33
|
+
SqlAnalyzer.background_processor <<
|
34
|
+
_query_analyzer_private_query_stanza([query_analyzer_call], analyzer)
|
28
35
|
end
|
29
36
|
end
|
30
37
|
end
|
@@ -32,30 +39,62 @@ module ActiveRecord
|
|
32
39
|
super
|
33
40
|
end
|
34
41
|
|
42
|
+
def begin_db_transaction
|
43
|
+
@_query_analyzer_private_in_transaction = true
|
44
|
+
|
45
|
+
record_transaction = SqlAnalyzer.config[:analyzers].any? do |analyzer|
|
46
|
+
SqlAnalyzer.config[:should_log_sample_proc].call(analyzer[:name])
|
47
|
+
end
|
48
|
+
if record_transaction
|
49
|
+
@_query_analyzer_private_transaction_queue ||= []
|
50
|
+
@_query_analyzer_private_record_transaction = true
|
51
|
+
else
|
52
|
+
@_query_analyzer_private_record_transaction = nil
|
53
|
+
end
|
54
|
+
super
|
55
|
+
end
|
56
|
+
|
57
|
+
def commit_db_transaction
|
58
|
+
_query_analyzer_private_drain_transaction_queue("COMMIT")
|
59
|
+
super
|
60
|
+
ensure
|
61
|
+
@_query_analyzer_private_in_transaction = false
|
62
|
+
end
|
63
|
+
|
64
|
+
def exec_rollback_db_transaction
|
65
|
+
_query_analyzer_private_drain_transaction_queue("ROLLBACK")
|
66
|
+
super
|
67
|
+
ensure
|
68
|
+
@_query_analyzer_private_in_transaction = false
|
69
|
+
end
|
70
|
+
|
71
|
+
# "private" methods for this monkeypatch
|
72
|
+
|
35
73
|
# Drain the transaction query queue. Log the current transaction out to any logger that samples it.
|
36
|
-
def _query_analyzer_private_drain_transaction_queue
|
74
|
+
def _query_analyzer_private_drain_transaction_queue(last_query)
|
75
|
+
return unless @_query_analyzer_private_record_transaction
|
76
|
+
|
37
77
|
reencoded_calls = nil
|
38
78
|
|
39
79
|
SqlAnalyzer.config[:analyzers].each do |analyzer|
|
40
|
-
|
41
|
-
# Again, trying to only re-encode and join strings if the transaction is actually
|
42
|
-
# sampled.
|
43
|
-
reencoded_calls ||= @_query_analyzer_private_transaction_queue.map do |call|
|
44
|
-
QueryAnalyzerCall.new(call.sql.encode(Encoding::UTF_8, invalid: :replace, undef: :replace), call.caller)
|
45
|
-
end
|
46
|
-
|
47
|
-
has_matching_calls = reencoded_calls.any? { |call| call.sql =~ analyzer[:table_regex] }
|
80
|
+
reencoded_calls ||= @_query_analyzer_private_transaction_queue << QueryAnalyzerCall.new(last_query, caller)
|
48
81
|
|
49
|
-
|
50
|
-
matching_calls = reencoded_calls.select do |call|
|
51
|
-
(call.sql =~ /^(BEGIN|COMMIT|ROLLBACK|UPDATE|INSERT|DELETE)/) || (call.sql =~ analyzer[:table_regex])
|
52
|
-
end
|
82
|
+
has_matching_calls = reencoded_calls.any? { |call| call.sql =~ analyzer[:table_regex] }
|
53
83
|
|
54
|
-
|
55
|
-
|
84
|
+
# Record "full" transactions
|
85
|
+
# Record all INSERT, UPDATE, and DELETE
|
86
|
+
# Record all queries that match the analyzer's table_regex
|
87
|
+
if has_matching_calls
|
88
|
+
matching_calls = reencoded_calls.select do |call|
|
89
|
+
(call.sql =~ /^(BEGIN|COMMIT|ROLLBACK|UPDATE|INSERT|DELETE)/) || (call.sql =~ analyzer[:table_regex])
|
56
90
|
end
|
91
|
+
|
92
|
+
SqlAnalyzer.background_processor << _query_analyzer_private_query_stanza(matching_calls, analyzer)
|
57
93
|
end
|
58
94
|
end
|
95
|
+
|
96
|
+
@_query_analyzer_private_transaction_queue.clear
|
97
|
+
@_query_analyzer_private_record_transaction = nil
|
59
98
|
end
|
60
99
|
|
61
100
|
# Helper method to construct the event for a query or transaction.
|
@@ -70,24 +109,6 @@ module ActiveRecord
|
|
70
109
|
request_path: Thread.current[:_ar_analyzer_request_path],
|
71
110
|
}
|
72
111
|
end
|
73
|
-
|
74
|
-
def transaction(requires_new: nil, isolation: nil, joinable: true)
|
75
|
-
must_clear_transaction = false
|
76
|
-
|
77
|
-
if SqlAnalyzer.config[:consolidate_transactions]
|
78
|
-
if @_query_analyzer_private_transaction_queue.nil?
|
79
|
-
must_clear_transaction = true
|
80
|
-
@_query_analyzer_private_transaction_queue = []
|
81
|
-
end
|
82
|
-
end
|
83
|
-
|
84
|
-
super
|
85
|
-
ensure
|
86
|
-
if must_clear_transaction
|
87
|
-
_query_analyzer_private_drain_transaction_queue
|
88
|
-
@_query_analyzer_private_transaction_queue = nil
|
89
|
-
end
|
90
|
-
end
|
91
112
|
end
|
92
113
|
end
|
93
114
|
end
|
@@ -22,10 +22,10 @@ module ActiveRecord
|
|
22
22
|
|
23
23
|
def filter_caller(kaller)
|
24
24
|
kaller = if config[:ambiguous_tracers].any? { |regex| kaller.first =~ regex }
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
25
|
+
kaller[0, config[:ambiguous_backtrace_lines]].join(", ")
|
26
|
+
else
|
27
|
+
kaller.first
|
28
|
+
end
|
29
29
|
|
30
30
|
return '' unless kaller
|
31
31
|
|
@@ -216,6 +216,36 @@ RSpec.describe "End to End" do
|
|
216
216
|
"COMMIT;")
|
217
217
|
end
|
218
218
|
|
219
|
+
context "ActiveRecord generated transactions" do
|
220
|
+
before do
|
221
|
+
stub_const("Matching", Class.new(ActiveRecord::Base) do
|
222
|
+
self.table_name = "matching_table"
|
223
|
+
|
224
|
+
after_commit { self.class.last }
|
225
|
+
end)
|
226
|
+
|
227
|
+
stub_const("NonMatching", Class.new(ActiveRecord::Base) do
|
228
|
+
self.table_name = "nonmatching_table"
|
229
|
+
|
230
|
+
after_commit { self.class.last }
|
231
|
+
end)
|
232
|
+
end
|
233
|
+
|
234
|
+
it "Logs the matching statements in the transaction and logs after_commit hooks outside the transaction" do
|
235
|
+
Matching.transaction do
|
236
|
+
Matching.create!
|
237
|
+
NonMatching.create
|
238
|
+
NonMatching.last
|
239
|
+
end
|
240
|
+
NonMatching.create
|
241
|
+
|
242
|
+
expect(log_def_hash.map { |_hash, data| data["sql"] }).to match([
|
243
|
+
"INSERT INTO `matching_table` VALUES ()",
|
244
|
+
"BEGIN; INSERT INTO `matching_table` VALUES (); INSERT INTO `nonmatching_table` VALUES (); COMMIT;",
|
245
|
+
"SELECT `matching_table`.* FROM `matching_table` ORDER BY `matching_table`.`id` DESC"
|
246
|
+
])
|
247
|
+
end
|
248
|
+
end
|
219
249
|
|
220
250
|
it "Logs mixed matching-nonmatching with inserts correctly" do
|
221
251
|
transaction do
|
@@ -278,15 +308,14 @@ RSpec.describe "End to End" do
|
|
278
308
|
execute "SELECT * FROM matching_table WHERE id > 4 and id < 8"
|
279
309
|
end
|
280
310
|
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
"
|
287
|
-
"SELECT * FROM matching_table WHERE id = '[REDACTED]'
|
288
|
-
|
289
|
-
"COMMIT;")
|
311
|
+
expect(log_def_hash.map { |_hash, data| data["sql"] }).to match([
|
312
|
+
"SELECT * FROM matching_table WHERE id = '[REDACTED]'",
|
313
|
+
"BEGIN; "\
|
314
|
+
"SELECT * FROM matching_table WHERE id = '[REDACTED]'; "\
|
315
|
+
"SELECT * FROM matching_table WHERE test_string = '[REDACTED]'; "\
|
316
|
+
"COMMIT;",
|
317
|
+
"SELECT * FROM matching_table WHERE id = '[REDACTED]' and id = '[REDACTED]'"
|
318
|
+
])
|
290
319
|
end
|
291
320
|
end
|
292
321
|
|
@@ -19,6 +19,7 @@ class DBConnection
|
|
19
19
|
end
|
20
20
|
|
21
21
|
def self.setup_db
|
22
|
+
ActiveRecord::Base.raise_in_transactional_callbacks = true
|
22
23
|
conn = ActiveRecord::Base.establish_connection(configuration)
|
23
24
|
conn.connection.execute <<-SQL
|
24
25
|
CREATE DATABASE IF NOT EXISTS ar_sql_analyzer_test
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: active_record-sql_analyzer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Zachary Anker
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2016-
|
12
|
+
date: 2016-12-16 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activerecord
|
@@ -56,6 +56,7 @@ extra_rdoc_files: []
|
|
56
56
|
files:
|
57
57
|
- ".gitignore"
|
58
58
|
- ".rspec"
|
59
|
+
- ".rubocop.yml"
|
59
60
|
- ".travis.yml"
|
60
61
|
- CONTRIBUTING.md
|
61
62
|
- Gemfile
|
@@ -110,7 +111,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
110
111
|
version: '0'
|
111
112
|
requirements: []
|
112
113
|
rubyforge_project:
|
113
|
-
rubygems_version: 2.
|
114
|
+
rubygems_version: 2.5.1
|
114
115
|
signing_key:
|
115
116
|
specification_version: 4
|
116
117
|
summary: Logs a subset of ActiveRecord queries and dumps them for analyses.
|