active_record-sql_analyzer 0.0.8 → 0.1.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d12fe56729a7e42b10fdb9b5aadd08ed74f23941
4
- data.tar.gz: b3608149a379ef161b541ffc1307cc263ad7750a
3
+ metadata.gz: 48238f31887e49755dbac5b74348f9f580dc0b65
4
+ data.tar.gz: 7c875b3aa9dfdb7e4449f12893ff1ddf2f7effff
5
5
  SHA512:
6
- metadata.gz: 18c45d31d25f34452e3244ec8c7605a766214c241724d8aaf8e2f4835a9fc78c6a52f6f43611b3ca08547b141e9e7b53633c8eeafd97cabfbac8a71e71c3c437
7
- data.tar.gz: 7343c8e1d69445cb1c4be107cba3cb128c3dc6f96d7636ac7c0f50c55c17ba295e8b7511a99b75f96d249ad3848ea275f66ab09fb6703f8f387e5e8e624b3ffa
6
+ metadata.gz: ceaaefc3ea471c547e81c4ce227a7fb1b1b13e6d61ea5caffd6d1391b1802fe35af2f5948d7624f1c376e75bfc0c4a89b8be6efb61667ddb5e1faac0bc34e33b
7
+ data.tar.gz: a89a8a5ae0d6e38c08123a64b0e1a2ec63b8fcfd8641e5eaff55507c122717177c97fc84ab50f605e08ee3a0954ef98a711abc8fa6188253b76ae2ff359cd05c
data/Gemfile CHANGED
@@ -7,6 +7,7 @@ group :test do
7
7
  gem 'rspec', '~> 3.4'
8
8
  gem 'rubocop', '~> 0.30'
9
9
  gem 'timecop', '~> 0.8'
10
+ gem 'sql-parser', git: 'https://github.com/nerdrew/sql-parser.git'
10
11
  end
11
12
 
12
13
  gemspec
@@ -146,19 +146,19 @@ module ActiveRecord
146
146
  end
147
147
 
148
148
  def setup_defaults
149
- quotedValuePattern = "'[^'\\\\]*(?:\\\\.[^'\\\\]*)*'"
149
+ quotedValuePattern = %{('([^\\\\']|\\\\.|'')*'|"([^\\\\"]|\\\\.|"")*")}
150
150
  @options[:sql_redactors] = [
151
151
  Redactor.new(/\n/, " "),
152
152
  Redactor.new(/\s+/, " "),
153
- Redactor.new(/(\s|\b|`)(=|!=|>=|>|<=|<) ?(BINARY )?-?\d+(\.\d+)?/i, " = [REDACTED]"),
153
+ Redactor.new(/IN \([^)]+\)/i, "IN ('[REDACTED]')"),
154
+ Redactor.new(/(\s|\b|`)(=|!=|>=|>|<=|<) ?(BINARY )?-?\d+(\.\d+)?/i, " = '[REDACTED]'"),
154
155
  Redactor.new(/(\s|\b|`)(=|!=|>=|>|<=|<) ?(BINARY )?x?#{quotedValuePattern}/i, " = '[REDACTED]'"),
155
- Redactor.new(/VALUES \(.+\)$/i, "VALUES ([REDACTED])"),
156
- Redactor.new(/IN \([^)]+\)/i, "IN ([REDACTED])"),
156
+ Redactor.new(/VALUES \(.+\)$/i, "VALUES ('[REDACTED]')"),
157
157
  Redactor.new(/BETWEEN #{quotedValuePattern} AND #{quotedValuePattern}/i, "BETWEEN '[REDACTED]' AND '[REDACTED]'"),
158
- Redactor.new(/LIKE #{quotedValuePattern}/i, "LIKE '[REDACTED]'"),
158
+ Redactor.new(/LIKE #{quotedValuePattern}/i, "LIKE '[REDACTED]'"),
159
159
  Redactor.new(/ LIMIT \d+/i, ""),
160
160
  Redactor.new(/ OFFSET \d+/i, ""),
161
- Redactor.new(/INSERT INTO (`?\w+`?) \([^)]+\)/i, 'INSERT INTO \1 ([COLUMNS])'),
161
+ Redactor.new(/INSERT INTO (`?\w+`?) \([^)]+\)/i, "INSERT INTO \\1 (REDACTED_COLUMNS)"),
162
162
  ]
163
163
 
164
164
  @options[:should_log_sample_proc] = Proc.new { |_name| false }
@@ -1,3 +1,4 @@
1
+ require 'securerandom'
1
2
 
2
3
  module ActiveRecord
3
4
  module SqlAnalyzer
@@ -9,7 +10,7 @@ module ActiveRecord
9
10
  safe_sql = nil
10
11
 
11
12
  SqlAnalyzer.config[:analyzers].each do |analyzer|
12
- if SqlAnalyzer.config[:should_log_sample_proc].call(analyzer[:name])
13
+ if _query_analyzer_private_should_sample_query(analyzer[:name])
13
14
  # This is here rather than above intentionally.
14
15
  # We assume we're not going to be analyzing 100% of queries and want to only re-encode
15
16
  # when it's actually relevant.
@@ -21,7 +22,8 @@ module ActiveRecord
21
22
  caller: caller,
22
23
  logger: analyzer[:logger_instance],
23
24
  tag: Thread.current[:_ar_analyzer_tag],
24
- request_path: Thread.current[:_ar_analyzer_request_path]
25
+ request_path: Thread.current[:_ar_analyzer_request_path],
26
+ transaction: @_query_analyzer_private_transaction_uuid
25
27
  }
26
28
  end
27
29
  end
@@ -29,6 +31,42 @@ module ActiveRecord
29
31
 
30
32
  super
31
33
  end
34
+
35
+ # Whether or not we should sample the query. If it's part of a transaction, check whether we
36
+ # are sampling the transaction. Otherwise, run the sample proc.
37
+ def _query_analyzer_private_should_sample_query(analyzer_name)
38
+ if @_query_analyzer_private_transaction_uuid.present?
39
+ @_query_analyzer_private_should_sample_transaction[analyzer_name]
40
+ else
41
+ SqlAnalyzer.config[:should_log_sample_proc].call(analyzer_name)
42
+ end
43
+ end
44
+
45
+ # Calculate hash of analyzer_name -> boolean representing whether we should sample the
46
+ # whole transaction for this analyzer.
47
+ def _query_analyzer_private_calculate_sampling_for_all_analyzers
48
+ SqlAnalyzer.config[:analyzers].map do |analyzer|
49
+ [analyzer[:name], SqlAnalyzer.config[:should_log_sample_proc].call(analyzer[:name])]
50
+ end.to_h
51
+ end
52
+
53
+ def transaction(requires_new: nil, isolation: nil, joinable: true)
54
+ must_clear_uuid = false
55
+
56
+ if @_query_analyzer_private_transaction_uuid.nil? then
57
+ must_clear_uuid = true
58
+ @_query_analyzer_private_transaction_uuid = SecureRandom.uuid
59
+ @_query_analyzer_private_should_sample_transaction =
60
+ _query_analyzer_private_calculate_sampling_for_all_analyzers
61
+ end
62
+
63
+ super
64
+ ensure
65
+ if must_clear_uuid
66
+ @_query_analyzer_private_transaction_uuid = nil
67
+ @_query_analyzer_private_should_sample_transaction = nil
68
+ end
69
+ end
32
70
  end
33
71
  end
34
72
  end
@@ -1,5 +1,5 @@
1
1
  module ActiveRecord
2
2
  module SqlAnalyzer
3
- VERSION = '0.0.8'
3
+ VERSION = '0.1.0'
4
4
  end
5
5
  end
@@ -74,7 +74,7 @@ RSpec.describe "End to End" do
74
74
 
75
75
  expect(log_def_hash.length).to eq(2)
76
76
 
77
- expect(log_def_hash[id_sha]["sql"]).to include("mt.id = [REDACTED]")
77
+ expect(log_def_hash[id_sha]["sql"]).to include("mt.id = '[REDACTED]'")
78
78
  expect(log_def_hash[str_sha]["sql"]).to include("mt.test_string = '[REDACTED]'")
79
79
  end
80
80
 
@@ -90,7 +90,7 @@ RSpec.describe "End to End" do
90
90
 
91
91
  expect(log_def_hash.length).to eq(2)
92
92
 
93
- expect(log_def_hash[id_sha]["sql"]).to include("id = [REDACTED]")
93
+ expect(log_def_hash[id_sha]["sql"]).to include("id = '[REDACTED]'")
94
94
  expect(log_def_hash[str_sha]["sql"]).to include("test_string = '[REDACTED]'")
95
95
  end
96
96
 
@@ -104,6 +104,102 @@ RSpec.describe "End to End" do
104
104
  expect(log_def_hash[sha]["sql"]).to include("test_string = '[REDACTED]'")
105
105
  end
106
106
 
107
+ it "logs multiple queries in a transaction correctly" do
108
+ DBConnection.connection.transaction do
109
+ 4.times { execute "SELECT * FROM matching_table WHERE id = 4321" }
110
+ 3.times { execute "SELECT * FROM matching_table WHERE test_string = 'abc'" }
111
+ end
112
+
113
+ DBConnection.connection.transaction do
114
+ 2.times { execute "SELECT * FROM matching_table WHERE test_string LIKE 'abc'" }
115
+ execute "SELECT * FROM matching_table WHERE id > 2 and id < 4"
116
+ end
117
+
118
+ id_eq_sha = log_reverse_hash[4]
119
+ str_eq_sha = log_reverse_hash[3]
120
+ str_like_sha = log_reverse_hash[2]
121
+ id_gt_sha = log_reverse_hash[1]
122
+
123
+ expect(log_def_hash[id_gt_sha]["sql"]).to include("id = [REDACTED] and id = [REDACTED]")
124
+ expect(log_def_hash[str_like_sha]["sql"]).to include("test_string LIKE '[REDACTED]'")
125
+ expect(log_def_hash[id_gt_sha]["transaction"]).to eq(log_def_hash[str_like_sha]["transaction"])
126
+
127
+ expect(log_def_hash[id_eq_sha]["sql"]).to include("id = [REDACTED]")
128
+ expect(log_def_hash[str_eq_sha]["sql"]).to include("test_string = '[REDACTED]'")
129
+ expect(log_def_hash[str_eq_sha]["transaction"]).to eq(log_def_hash[id_eq_sha]["transaction"])
130
+
131
+ expect(log_def_hash[str_like_sha]["transaction"]).to_not eq(log_def_hash[id_eq_sha]["transaction"])
132
+ end
133
+
134
+ it "Logs nested transactions correctly" do
135
+ DBConnection.connection.transaction do
136
+ execute "SELECT * FROM matching_table WHERE id = 4321"
137
+ DBConnection.connection.transaction do
138
+ 2.times { execute "SELECT * FROM matching_table WHERE test_string = 'abc'" }
139
+ end
140
+ end
141
+
142
+
143
+ id_sha = log_reverse_hash[1]
144
+ str_sha = log_reverse_hash[2]
145
+
146
+ expect(log_def_hash[id_sha]["transaction"]).to eq(log_def_hash[str_sha]["transaction"])
147
+ expect(log_def_hash[str_sha]["transaction"]).not_to be_nil
148
+ end
149
+
150
+ it "Logs empty transactions correctly" do
151
+ DBConnection.connection.transaction do
152
+ execute "SELECT * FROM matching_table WHERE id = 4321"
153
+ end
154
+
155
+ 2.times { execute "SELECT * FROM matching_table WHERE test_string = 'abc'" }
156
+
157
+ id_sha = log_reverse_hash[1]
158
+ str_sha = log_reverse_hash[2]
159
+
160
+ expect(log_def_hash[id_sha]["sql"]).to include("id = [REDACTED]")
161
+ expect(log_def_hash[str_sha]["sql"]).to include("test_string = '[REDACTED]'")
162
+
163
+ expect(log_def_hash[id_sha]["transaction"]).not_to be_nil
164
+ expect(log_def_hash[str_sha]["transaction"]).to be_nil
165
+ end
166
+
167
+ context "Selectively sampling" do
168
+ before do
169
+ ActiveRecord::SqlAnalyzer.configure do |c|
170
+ times_called = 0
171
+ # Return true every other call, starting with the first call
172
+ c.log_sample_proc Proc.new { |_name| (times_called += 1) % 2 == 1 }
173
+ end
174
+ end
175
+
176
+ it "Samples some but not other selects" do
177
+ execute "SELECT * FROM matching_table WHERE id = 1"
178
+ execute "SELECT * FROM matching_table WHERE test_string = 'abc'"
179
+
180
+ expect(log_def_hash.size).to eq(1)
181
+ expect(log_def_hash.map { |_hash, query| query['sql'] }).to eq([
182
+ "SELECT * FROM matching_table WHERE id = [REDACTED]"])
183
+ end
184
+
185
+ it "Samples some but not other whole transactions" do
186
+ DBConnection.connection.transaction do
187
+ execute "SELECT * FROM matching_table WHERE id = 1"
188
+ execute "SELECT * FROM matching_table WHERE test_string = 'abc'"
189
+ end
190
+
191
+ DBConnection.connection.transaction do
192
+ execute "SELECT * FROM matching_table WHERE id = 1 and test_string = 'abc'"
193
+ execute "SELECT * FROM matching_table WHERE id > 4 and id < 8"
194
+ end
195
+
196
+ expect(log_def_hash.size).to eq(2)
197
+ expect(Set.new(log_def_hash.map { |_hash, query| query['sql'] })).to eq(Set.new([
198
+ "SELECT * FROM matching_table WHERE id = [REDACTED]",
199
+ "SELECT * FROM matching_table WHERE test_string = '[REDACTED]'"]))
200
+ end
201
+ end
202
+
107
203
  context "when sampling is disabled" do
108
204
  before do
109
205
  ActiveRecord::SqlAnalyzer.configure do |c|
@@ -1,5 +1,9 @@
1
+ require "spec_helper"
2
+ require "sql-parser"
3
+
1
4
  RSpec.describe ActiveRecord::SqlAnalyzer::RedactedLogger do
2
5
  let(:tmp_dir) { Dir.mktmpdir }
6
+ let(:parser) { SQLParser::Parser.new }
3
7
  after { FileUtils.remove_entry(tmp_dir) }
4
8
 
5
9
  context "#filter_event" do
@@ -17,6 +21,13 @@ RSpec.describe ActiveRecord::SqlAnalyzer::RedactedLogger do
17
21
  [/erb_erb_[0-9]+_[0-9]+/, ""]
18
22
  ]
19
23
  end
24
+ # All raw SQL should be valid :)
25
+ expect { parser.scan_str(event[:sql]) }.not_to raise_exception if event[:sql].present?
26
+ end
27
+
28
+ after do
29
+ # All redacted SQL should be valid
30
+ expect { parser.scan_str(filter_event[:sql]) }.not_to raise_exception if filter_event[:sql].present?
20
31
  end
21
32
 
22
33
  context "ambiguous backtraces" do
@@ -101,7 +112,7 @@ RSpec.describe ActiveRecord::SqlAnalyzer::RedactedLogger do
101
112
 
102
113
  it "redacts" do
103
114
  expect(filter_event[:sql]).to eq("SELECT * FROM foo WHERE name LIKE '[REDACTED]'")
104
- end
115
+ end
105
116
  end
106
117
 
107
118
  context "sql" do
@@ -113,7 +124,7 @@ RSpec.describe ActiveRecord::SqlAnalyzer::RedactedLogger do
113
124
  end
114
125
 
115
126
  it "redacts" do
116
- expect(filter_event[:sql]).to eq("SELECT * FROM foo WHERE id = [REDACTED]")
127
+ expect(filter_event[:sql]).to eq("SELECT * FROM foo WHERE id = '[REDACTED]'")
117
128
  end
118
129
  end
119
130
 
@@ -121,12 +132,12 @@ RSpec.describe ActiveRecord::SqlAnalyzer::RedactedLogger do
121
132
  let(:event) do
122
133
  {
123
134
  caller: [""],
124
- sql: "SELECT * FROM foo WHERE name LIKE 'A \\'quoted\\' value.'"
135
+ sql: %{SELECT * FROM foo WHERE name LIKE 'A \\'quoted\\' value.' OR name LIKE "another ""quoted"" \\"value\\""}
125
136
  }
126
137
  end
127
138
 
128
139
  it "redacts" do
129
- expect(filter_event[:sql]).to eq("SELECT * FROM foo WHERE name LIKE '[REDACTED]'")
140
+ expect(filter_event[:sql]).to eq("SELECT * FROM foo WHERE name LIKE '[REDACTED]' OR name LIKE '[REDACTED]'")
130
141
  end
131
142
  end
132
143
 
@@ -147,12 +158,12 @@ RSpec.describe ActiveRecord::SqlAnalyzer::RedactedLogger do
147
158
  let(:event) do
148
159
  {
149
160
  caller: [""],
150
- sql: "SELECT * FROM foo WHERE name IN ('A \\'quoted\\' value.')"
161
+ sql: "SELECT * FROM foo WHERE name IN ('A ''quoted'' value.')"
151
162
  }
152
163
  end
153
164
 
154
165
  it "redacts" do
155
- expect(filter_event[:sql]).to eq("SELECT * FROM foo WHERE name IN ([REDACTED])")
166
+ expect(filter_event[:sql]).to eq("SELECT * FROM foo WHERE name IN ('[REDACTED]')")
156
167
  end
157
168
  end
158
169
 
@@ -160,20 +171,20 @@ RSpec.describe ActiveRecord::SqlAnalyzer::RedactedLogger do
160
171
  let(:event) do
161
172
  {
162
173
  caller: [""],
163
- sql: "SELECT * FROM foo WHERE name IN ('A \\\'quoted\\\' value.')"
174
+ sql: %{SELECT * FROM foo WHERE name IN ('A ''quoted'' value.', "another ""quoted"" \\"value\\"")}
164
175
  }
165
176
  end
166
177
 
167
178
  it "redacts" do
168
- expect(filter_event[:sql]).to eq("SELECT * FROM foo WHERE name IN ([REDACTED])")
179
+ expect(filter_event[:sql]).to eq("SELECT * FROM foo WHERE name IN ('[REDACTED]')")
169
180
  end
170
181
  end
171
182
 
172
- context "between quoted" do
183
+ context "between strings" do
173
184
  let(:event) do
174
185
  {
175
186
  caller: [""],
176
- sql: "SELECT * FROM foo WHERE name BETWEEN 'A \\'quoted\\' value.' AND 'Another value'"
187
+ sql: "SELECT * FROM foo WHERE name BETWEEN 'A value.' AND 'Another value'"
177
188
  }
178
189
  end
179
190
 
@@ -182,11 +193,11 @@ RSpec.describe ActiveRecord::SqlAnalyzer::RedactedLogger do
182
193
  end
183
194
  end
184
195
 
185
- context "between escaped and quoted" do
196
+ context "between strings with escaped quotes" do
186
197
  let(:event) do
187
198
  {
188
199
  caller: [""],
189
- sql: "SELECT * FROM foo WHERE name BETWEEN 'A \\\'quoted\\\' value.' AND 'Another value'"
200
+ sql: "SELECT * FROM foo WHERE name BETWEEN 'A ''quoted'' value.' AND 'Another \\'value\\''"
190
201
  }
191
202
  end
192
203
 
@@ -194,5 +205,31 @@ RSpec.describe ActiveRecord::SqlAnalyzer::RedactedLogger do
194
205
  expect(filter_event[:sql]).to eq("SELECT * FROM foo WHERE name BETWEEN '[REDACTED]' AND '[REDACTED]'")
195
206
  end
196
207
  end
208
+
209
+ context "in with = and other where clauses" do
210
+ let(:event) do
211
+ {
212
+ caller: [""],
213
+ sql: "SELECT * FROM foo WHERE name IN ('value=') AND name = 'value'"
214
+ }
215
+ end
216
+
217
+ it "redacts" do
218
+ expect(filter_event[:sql]).to eq("SELECT * FROM foo WHERE name IN ('[REDACTED]') AND name = '[REDACTED]'")
219
+ end
220
+ end
221
+
222
+ context "insert" do
223
+ let(:event) do
224
+ {
225
+ caller: [""],
226
+ sql: "INSERT INTO `boom` (`bam`, `foo`) VALUES ('howdy', 'dowdy')"
227
+ }
228
+ end
229
+
230
+ it "redacts" do
231
+ expect(filter_event[:sql]).to eq("INSERT INTO `boom` (REDACTED_COLUMNS) VALUES ('[REDACTED]')")
232
+ end
233
+ end
197
234
  end
198
235
  end
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.0.8
4
+ version: 0.1.0
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-05-09 00:00:00.000000000 Z
12
+ date: 2016-08-09 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activerecord
@@ -110,7 +110,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
110
110
  version: '0'
111
111
  requirements: []
112
112
  rubyforge_project:
113
- rubygems_version: 2.4.8
113
+ rubygems_version: 2.5.1
114
114
  signing_key:
115
115
  specification_version: 4
116
116
  summary: Logs a subset of ActiveRecord queries and dumps them for analyses.
@@ -127,4 +127,3 @@ test_files:
127
127
  - spec/support/stub_logger.rb
128
128
  - spec/support/stub_rails.rb
129
129
  - spec/support/wait_for_pop.rb
130
- has_rdoc: