tapping_device 0.4.1 → 0.4.2
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/Gemfile.lock +1 -1
- data/README.md +4 -4
- data/lib/tapping_device/payload.rb +1 -1
- data/lib/tapping_device/sql_tapping_methods.rb +63 -46
- data/lib/tapping_device/version.rb +1 -1
- data/lib/tapping_device.rb +16 -15
- metadata +2 -3
- data/lib/tapping_device/sql_listener.rb +0 -10
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fcaaf227b8114ef1cbfeabf472ac00bc86512e6426f98fc88bdf841f1ca71cee
|
4
|
+
data.tar.gz: e1a87da54066d42958915ef19e4c933edad4f9a2907e4df95893c68d4b5dcf95
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 437e25448ba4e10f984596d1c7502101f2afddf49400a0a72e8c753df8a2498981ef0812975d4b551c83caf3ab603352741830b6aa16019e323687ea487ee3fe
|
7
|
+
data.tar.gz: 1cf0b05b79cf2ea64a0862d051a493833d3bcec039d1f400b6184a0901c9c46122d5f2bf2a90588a61cedadc422c87e2d52aaee9d7df6ec247feb2def2defeb3
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -126,7 +126,7 @@ All tapping methods (start with `tap_`) takes a block and yield a `Payload` obje
|
|
126
126
|
{
|
127
127
|
:receiver=>#<Student:0x00007fabed02aeb8 @name="Stan", @age=18, @tapping_device=[#<TracePoint:return `age'@/PROJECT_PATH/tapping_device/spec/trackable_spec.rb:17>]>,
|
128
128
|
:method_name=>:age,
|
129
|
-
:arguments=>
|
129
|
+
:arguments=>{},
|
130
130
|
:return_value=>18,
|
131
131
|
:filepath=>"/PROJECT_PATH/tapping_device/spec/trackable_spec.rb",
|
132
132
|
:line_number=>"171",
|
@@ -142,7 +142,7 @@ The hash contains
|
|
142
142
|
- `method_name` - method’s name (symbol)
|
143
143
|
- e.g. `:name`
|
144
144
|
- `arguments` - arguments of the method call
|
145
|
-
- e.g. `
|
145
|
+
- e.g. `{name: “Stan”, age: 25}`
|
146
146
|
- `return_value` - return value of the method call
|
147
147
|
- `filepath` - path to the file that performs the method call
|
148
148
|
- `line_number`
|
@@ -152,7 +152,7 @@ The hash contains
|
|
152
152
|
|
153
153
|
#### Some useful helpers
|
154
154
|
- `method_name_and_location` - `"Method: :initialize, line: /PROJECT_PATH/tapping_device/spec/payload_spec.rb:7"`
|
155
|
-
- `method_name_and_arguments` - `"Method: :initialize, argments:
|
155
|
+
- `method_name_and_arguments` - `"Method: :initialize, argments: {:name=>\"Stan\", :age=>25}"`
|
156
156
|
|
157
157
|
|
158
158
|
### Options
|
@@ -193,7 +193,7 @@ end
|
|
193
193
|
Student.new("Stan", 18)
|
194
194
|
Student.new("Jane", 23)
|
195
195
|
|
196
|
-
puts(calls.to_s) #=> [[:initialize,
|
196
|
+
puts(calls.to_s) #=> [[:initialize, {:name=>"Stan", :age=>18}], [:initialize, {:name=>"Jane", :age=>23}]]
|
197
197
|
```
|
198
198
|
|
199
199
|
### `tap_on!`
|
@@ -1,6 +1,6 @@
|
|
1
1
|
class TappingDevice
|
2
2
|
class Payload < Hash
|
3
|
-
ATTRS = [:receiver, :method_name, :arguments, :return_value, :filepath, :line_number, :defined_class, :trace, :tp]
|
3
|
+
ATTRS = [:receiver, :method_name, :arguments, :return_value, :filepath, :line_number, :defined_class, :trace, :tp, :sql]
|
4
4
|
|
5
5
|
ATTRS.each do |attr|
|
6
6
|
define_method attr do
|
@@ -1,67 +1,84 @@
|
|
1
|
-
require "tapping_device/sql_listener"
|
2
|
-
|
3
1
|
class TappingDevice
|
4
2
|
module SqlTappingMethods
|
5
|
-
|
3
|
+
CALL_STACK_SKIPPABLE_METHODS = [:transaction, :tap]
|
6
4
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
listener.payload[:binds] = payload[:binds]
|
12
|
-
listener.device.record_call!(listener.payload)
|
13
|
-
end
|
14
|
-
end
|
5
|
+
# SQLListener acts like an interface for us to intercept activerecord query instrumentations
|
6
|
+
# this means we only need to register one subscriber no matter how many objects we want to tap on
|
7
|
+
class SQLListener
|
8
|
+
def call(name, start, finish, message_id, values);end
|
15
9
|
end
|
10
|
+
@@sql_listener = SQLListener.new
|
11
|
+
|
12
|
+
ActiveSupport::Notifications.subscribe("sql.active_record", @@sql_listener)
|
16
13
|
|
17
14
|
def tap_sql!(object)
|
18
|
-
@
|
15
|
+
@call_stack = []
|
16
|
+
@trace_point = with_trace_point_on_target(object, event: [:call, :c_call]) do |start_tp|
|
17
|
+
########## Check if the call is worth recording ##########
|
18
|
+
filepath, line_number = get_call_location(start_tp, padding: 1) # we need extra padding because of `with_trace_point_on_target`
|
19
19
|
method = start_tp.callee_id
|
20
|
+
next if should_be_skipped_by_paths?(filepath) || already_recording?(method)
|
20
21
|
|
21
|
-
|
22
|
-
|
22
|
+
########## Start the recording ##########
|
23
|
+
# 1. Mark recording state by pushing method into @call_stack
|
24
|
+
# 2. Subscribe sql instrumentations generated by activerecord
|
25
|
+
# 3. Record those sqls and run device callbacks
|
26
|
+
# 4. Start waiting for current call's return callback
|
27
|
+
@call_stack.push(method)
|
28
|
+
payload = build_payload(tp: start_tp, filepath: filepath, line_number: line_number)
|
29
|
+
device = tap_on_sql_instrumentation!(payload)
|
23
30
|
|
24
|
-
|
31
|
+
with_trace_point_on_target(object, event: :return) do |return_tp|
|
32
|
+
next unless return_tp.callee_id == method
|
25
33
|
|
26
|
-
|
34
|
+
########## End recording ##########
|
35
|
+
# 1. Close itself
|
36
|
+
# 2. Stop our subscription on SQLListener
|
37
|
+
# 3. Remove current method from @call_stack
|
38
|
+
# 4. Stop the device if stop condition is fulfilled
|
39
|
+
return_tp.disable
|
40
|
+
device.stop!
|
41
|
+
@call_stack.pop
|
42
|
+
stop_if_condition_fulfilled(payload)
|
27
43
|
|
28
|
-
|
29
|
-
#
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
44
|
+
########## Track descendant objects ##########
|
45
|
+
# if the method creates another Relation object
|
46
|
+
if return_tp.defined_class == ActiveRecord::QueryMethods
|
47
|
+
create_child_device.tap_sql!(return_tp.return_value)
|
48
|
+
end
|
49
|
+
end.enable
|
50
|
+
end
|
34
51
|
|
35
|
-
|
52
|
+
@trace_point.enable unless self.class.suspend_new
|
36
53
|
|
37
|
-
|
54
|
+
self
|
55
|
+
end
|
56
|
+
end
|
38
57
|
|
39
|
-
|
58
|
+
private
|
40
59
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
60
|
+
def tap_on_sql_instrumentation!(payload)
|
61
|
+
device = TappingDevice.new do |sql_listener_payload|
|
62
|
+
values = sql_listener_payload.arguments[:values]
|
63
|
+
next if ["SCHEMA", "TRANSACTION", nil].include? values[:name]
|
64
|
+
payload[:sql] = values[:sql]
|
65
|
+
record_call!(payload)
|
66
|
+
end
|
67
|
+
device.tap_on!(@@sql_listener)
|
68
|
+
end
|
50
69
|
|
51
|
-
|
52
|
-
|
53
|
-
|
70
|
+
# usually, AR's query methods (like `first`) will end up calling `find_by_sql`
|
71
|
+
# then to TappingDevice, both `first` and `find_by_sql` generates the sql
|
72
|
+
# but the results are duplicated, we should only consider the `first` call
|
73
|
+
def already_recording?(method)
|
74
|
+
!@call_stack.empty? || CALL_STACK_SKIPPABLE_METHODS.include?(method)
|
75
|
+
end
|
54
76
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
end
|
77
|
+
def with_trace_point_on_target(object, event:)
|
78
|
+
TracePoint.new(*event) do |tp|
|
79
|
+
if is_from_target?(object, tp)
|
80
|
+
yield(tp)
|
60
81
|
end
|
61
|
-
|
62
|
-
@trace_point.enable unless self.class.suspend_new
|
63
|
-
|
64
|
-
self
|
65
82
|
end
|
66
83
|
end
|
67
84
|
end
|
data/lib/tapping_device.rb
CHANGED
@@ -99,13 +99,13 @@ class TappingDevice
|
|
99
99
|
options[:descendants]
|
100
100
|
end
|
101
101
|
|
102
|
-
def record_call!(
|
102
|
+
def record_call!(payload)
|
103
103
|
return if @disabled
|
104
104
|
|
105
105
|
if @block
|
106
|
-
root_device.calls << @block.call(
|
106
|
+
root_device.calls << @block.call(payload)
|
107
107
|
else
|
108
|
-
root_device.calls <<
|
108
|
+
root_device.calls << payload
|
109
109
|
end
|
110
110
|
end
|
111
111
|
|
@@ -121,13 +121,13 @@ class TappingDevice
|
|
121
121
|
if send(condition, object, validation_params)
|
122
122
|
filepath, line_number = get_call_location(tp)
|
123
123
|
|
124
|
-
next if
|
124
|
+
next if should_be_skipped_by_paths?(filepath)
|
125
125
|
|
126
|
-
|
126
|
+
payload = build_payload(tp: tp, filepath: filepath, line_number: line_number)
|
127
127
|
|
128
|
-
record_call!(
|
128
|
+
record_call!(payload)
|
129
129
|
|
130
|
-
stop_if_condition_fulfilled(
|
130
|
+
stop_if_condition_fulfilled(payload)
|
131
131
|
end
|
132
132
|
end
|
133
133
|
|
@@ -136,22 +136,23 @@ class TappingDevice
|
|
136
136
|
self
|
137
137
|
end
|
138
138
|
|
139
|
-
def get_call_location(tp)
|
139
|
+
def get_call_location(tp, padding: 0)
|
140
140
|
if tp.event == :c_call
|
141
|
-
caller(C_CALLER_START_POINT)
|
141
|
+
caller(C_CALLER_START_POINT + padding)
|
142
142
|
else
|
143
|
-
caller(CALLER_START_POINT)
|
143
|
+
caller(CALLER_START_POINT + padding)
|
144
144
|
end.first.split(":")[0..1]
|
145
145
|
end
|
146
146
|
|
147
147
|
# this needs to be placed upfront so we can exclude noise before doing more work
|
148
|
-
def
|
148
|
+
def should_be_skipped_by_paths?(filepath)
|
149
149
|
options[:exclude_by_paths].any? { |pattern| pattern.match?(filepath) } ||
|
150
150
|
(options[:filter_by_paths].present? && !options[:filter_by_paths].any? { |pattern| pattern.match?(filepath) })
|
151
151
|
end
|
152
152
|
|
153
|
-
def
|
154
|
-
arguments =
|
153
|
+
def build_payload(tp:, filepath:, line_number:)
|
154
|
+
arguments = {}
|
155
|
+
tp.binding.local_variables.each { |name| arguments[name] = tp.binding.local_variable_get(name) }
|
155
156
|
|
156
157
|
Payload.init({
|
157
158
|
receiver: tp.self,
|
@@ -198,8 +199,8 @@ class TappingDevice
|
|
198
199
|
options
|
199
200
|
end
|
200
201
|
|
201
|
-
def stop_if_condition_fulfilled(
|
202
|
-
if @stop_when&.call(
|
202
|
+
def stop_if_condition_fulfilled(payload)
|
203
|
+
if @stop_when&.call(payload)
|
203
204
|
stop!
|
204
205
|
root_device.stop!
|
205
206
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tapping_device
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.4.
|
4
|
+
version: 0.4.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- st0012
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-12-
|
11
|
+
date: 2019-12-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -131,7 +131,6 @@ files:
|
|
131
131
|
- lib/tapping_device.rb
|
132
132
|
- lib/tapping_device/exceptions.rb
|
133
133
|
- lib/tapping_device/payload.rb
|
134
|
-
- lib/tapping_device/sql_listener.rb
|
135
134
|
- lib/tapping_device/sql_tapping_methods.rb
|
136
135
|
- lib/tapping_device/trackable.rb
|
137
136
|
- lib/tapping_device/version.rb
|