appmap 0.34.0 → 0.35.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 +4 -4
- data/.gitignore +1 -1
- data/.rbenv-gemsets +1 -0
- data/CHANGELOG.md +31 -0
- data/README.md +17 -2
- data/Rakefile +10 -3
- data/appmap.gemspec +5 -0
- data/ext/appmap/appmap.c +95 -0
- data/ext/appmap/extconf.rb +6 -0
- data/lib/appmap.rb +4 -1
- data/lib/appmap/class_map.rb +5 -6
- data/lib/appmap/config.rb +2 -2
- data/lib/appmap/cucumber.rb +19 -2
- data/lib/appmap/event.rb +28 -10
- data/lib/appmap/hook.rb +13 -20
- data/lib/appmap/hook/method.rb +59 -27
- data/lib/appmap/rails/request_handler.rb +88 -0
- data/lib/appmap/rails/sql_handler.rb +13 -10
- data/lib/appmap/railtie.rb +3 -5
- data/lib/appmap/rspec.rb +10 -0
- data/lib/appmap/trace.rb +9 -7
- data/lib/appmap/util.rb +18 -1
- data/lib/appmap/version.rb +1 -1
- data/spec/abstract_controller4_base_spec.rb +1 -1
- data/spec/abstract_controller_base_spec.rb +6 -0
- data/spec/fixtures/hook/instance_method.rb +4 -0
- data/spec/fixtures/hook/singleton_method.rb +21 -12
- data/spec/hook_spec.rb +85 -8
- metadata +50 -4
- data/lib/appmap/rails/action_handler.rb +0 -91
data/lib/appmap/hook/method.rb
CHANGED
@@ -1,63 +1,95 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module AppMap
|
2
4
|
class Hook
|
3
5
|
class Method
|
4
|
-
attr_reader :
|
6
|
+
attr_reader :hook_package, :hook_class, :hook_method
|
7
|
+
|
8
|
+
# +method_display_name+ may be nil if name resolution gets
|
9
|
+
# deferred until runtime (e.g. for a singleton method on an
|
10
|
+
# embedded Struct).
|
11
|
+
attr_reader :method_display_name
|
5
12
|
|
6
13
|
HOOK_DISABLE_KEY = 'AppMap::Hook.disable'
|
7
14
|
private_constant :HOOK_DISABLE_KEY
|
8
15
|
|
9
|
-
|
16
|
+
# Grab the definition of Time.now here, to avoid interfering
|
17
|
+
# with the method we're hooking.
|
18
|
+
TIME_NOW = Time.method(:now)
|
19
|
+
private_constant :TIME_NOW
|
20
|
+
|
21
|
+
def initialize(hook_package, hook_class, hook_method)
|
22
|
+
@hook_package = hook_package
|
10
23
|
@hook_class = hook_class
|
11
24
|
@hook_method = hook_method
|
25
|
+
|
26
|
+
# Get the class for the method, if it's known.
|
12
27
|
@defined_class, method_symbol = Hook.qualify_method_name(@hook_method)
|
13
|
-
@method_display_name = [@defined_class, method_symbol, @hook_method.name].join
|
28
|
+
@method_display_name = [@defined_class, method_symbol, @hook_method.name].join if @defined_class
|
14
29
|
end
|
15
30
|
|
16
31
|
def activate
|
17
|
-
|
32
|
+
if Hook::LOG
|
33
|
+
msg = if method_display_name
|
34
|
+
"#{method_display_name}"
|
35
|
+
else
|
36
|
+
"#{hook_method.name} (class resolution deferred)"
|
37
|
+
end
|
38
|
+
warn "AppMap: Hooking " + msg
|
39
|
+
end
|
18
40
|
|
41
|
+
defined_class = @defined_class
|
19
42
|
hook_method = self.hook_method
|
20
43
|
before_hook = self.method(:before_hook)
|
21
44
|
after_hook = self.method(:after_hook)
|
22
45
|
with_disabled_hook = self.method(:with_disabled_hook)
|
23
46
|
|
24
|
-
|
25
|
-
|
47
|
+
hook_method_def = nil
|
48
|
+
hook_class.instance_eval do
|
49
|
+
hook_method_def = Proc.new do |*args, &block|
|
50
|
+
instance_method = hook_method.bind(self).to_proc
|
26
51
|
|
27
|
-
|
28
|
-
|
29
|
-
|
52
|
+
# We may not have gotten the class for the method during
|
53
|
+
# initialization (e.g. for a singleton method on an embedded
|
54
|
+
# struct), so make sure we have it now.
|
55
|
+
defined_class,_ = Hook.qualify_method_name(hook_method) unless defined_class
|
30
56
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
exception =
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
57
|
+
hook_disabled = Thread.current[HOOK_DISABLE_KEY]
|
58
|
+
enabled = true if !hook_disabled && AppMap.tracing.enabled?
|
59
|
+
return instance_method.call(*args, &block) unless enabled
|
60
|
+
|
61
|
+
call_event, start_time = with_disabled_hook.() do
|
62
|
+
before_hook.(self, defined_class, args)
|
63
|
+
end
|
64
|
+
return_value = nil
|
65
|
+
exception = nil
|
66
|
+
begin
|
67
|
+
return_value = instance_method.(*args, &block)
|
68
|
+
rescue
|
69
|
+
exception = $ERROR_INFO
|
70
|
+
raise
|
71
|
+
ensure
|
72
|
+
with_disabled_hook.() do
|
73
|
+
after_hook.(self, call_event, start_time, return_value, exception)
|
74
|
+
end
|
44
75
|
end
|
45
76
|
end
|
46
77
|
end
|
78
|
+
hook_class.define_method_with_arity(hook_method.name, hook_method.arity, hook_method_def)
|
47
79
|
end
|
48
80
|
|
49
81
|
protected
|
50
82
|
|
51
|
-
def before_hook(receiver, args)
|
83
|
+
def before_hook(receiver, defined_class, args)
|
52
84
|
require 'appmap/event'
|
53
85
|
call_event = AppMap::Event::MethodCall.build_from_invocation(defined_class, hook_method, receiver, args)
|
54
|
-
AppMap.tracing.record_event call_event, defined_class: defined_class, method: hook_method
|
55
|
-
[ call_event,
|
86
|
+
AppMap.tracing.record_event call_event, package: hook_package, defined_class: defined_class, method: hook_method
|
87
|
+
[ call_event, TIME_NOW.call ]
|
56
88
|
end
|
57
89
|
|
58
|
-
def after_hook(call_event, start_time, return_value, exception)
|
90
|
+
def after_hook(receiver, call_event, start_time, return_value, exception)
|
59
91
|
require 'appmap/event'
|
60
|
-
elapsed =
|
92
|
+
elapsed = TIME_NOW.call - start_time
|
61
93
|
return_event = \
|
62
94
|
AppMap::Event::MethodReturn.build_from_invocation call_event.id, elapsed, return_value, exception
|
63
95
|
AppMap.tracing.record_event return_event
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'appmap/event'
|
4
|
+
require 'appmap/hook'
|
5
|
+
|
6
|
+
module AppMap
|
7
|
+
module Rails
|
8
|
+
module RequestHandler
|
9
|
+
class HTTPServerRequest < AppMap::Event::MethodEvent
|
10
|
+
attr_accessor :request_method, :path_info, :params
|
11
|
+
|
12
|
+
def initialize(request)
|
13
|
+
super AppMap::Event.next_id_counter, :call, Thread.current.object_id
|
14
|
+
|
15
|
+
@request_method = request.request_method
|
16
|
+
@path_info = request.path_info.split('?')[0]
|
17
|
+
@params = ActionDispatch::Http::ParameterFilter.new(::Rails.application.config.filter_parameters).filter(request.params)
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_h
|
21
|
+
super.tap do |h|
|
22
|
+
h[:http_server_request] = {
|
23
|
+
request_method: request_method,
|
24
|
+
path_info: path_info
|
25
|
+
}
|
26
|
+
|
27
|
+
h[:message] = params.keys.map do |key|
|
28
|
+
val = params[key]
|
29
|
+
{
|
30
|
+
name: key,
|
31
|
+
class: val.class.name,
|
32
|
+
value: self.class.display_string(val),
|
33
|
+
object_id: val.__id__
|
34
|
+
}
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class HTTPServerResponse < AppMap::Event::MethodReturnIgnoreValue
|
41
|
+
attr_accessor :status, :mime_type
|
42
|
+
|
43
|
+
def initialize(response, parent_id, elapsed)
|
44
|
+
super AppMap::Event.next_id_counter, :return, Thread.current.object_id
|
45
|
+
|
46
|
+
self.status = response.status
|
47
|
+
self.mime_type = response.headers['Content-Type']
|
48
|
+
self.parent_id = parent_id
|
49
|
+
self.elapsed = elapsed
|
50
|
+
end
|
51
|
+
|
52
|
+
def to_h
|
53
|
+
super.tap do |h|
|
54
|
+
h[:http_server_response] = {
|
55
|
+
status: status,
|
56
|
+
mime_type: mime_type
|
57
|
+
}.compact
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
class HookMethod < AppMap::Hook::Method
|
63
|
+
def initialize
|
64
|
+
# ActionController::Instrumentation has issued start_processing.action_controller and
|
65
|
+
# process_action.action_controller since Rails 3. Therefore it's a stable place to hook
|
66
|
+
# the request. Rails controller notifications can't be used directly because they don't
|
67
|
+
# provide response headers, and we want the Content-Type.
|
68
|
+
super(nil, ActionController::Instrumentation, ActionController::Instrumentation.instance_method(:process_action))
|
69
|
+
end
|
70
|
+
|
71
|
+
protected
|
72
|
+
|
73
|
+
def before_hook(receiver, defined_class, _) # args
|
74
|
+
call_event = HTTPServerRequest.new(receiver.request)
|
75
|
+
# http_server_request events are i/o and do not require a package name.
|
76
|
+
AppMap.tracing.record_event call_event, defined_class: defined_class, method: hook_method
|
77
|
+
[ call_event, TIME_NOW.call ]
|
78
|
+
end
|
79
|
+
|
80
|
+
def after_hook(receiver, call_event, start_time, _, _) # return_value, exception
|
81
|
+
elapsed = TIME_NOW.call - start_time
|
82
|
+
return_event = HTTPServerResponse.new receiver.response, call_event.id, elapsed
|
83
|
+
AppMap.tracing.record_event return_event
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -72,21 +72,24 @@ module AppMap
|
|
72
72
|
end
|
73
73
|
|
74
74
|
class ActiveRecordExaminer
|
75
|
+
@@db_version_warning_issued = {}
|
76
|
+
|
77
|
+
def issue_warning
|
78
|
+
db_type = database_type
|
79
|
+
return if @@db_version_warning_issued[db_type]
|
80
|
+
warn("AppMap: Unable to determine database version for #{db_type.inspect}")
|
81
|
+
@@db_version_warning_issued[db_type] = true
|
82
|
+
end
|
83
|
+
|
75
84
|
def server_version
|
76
|
-
|
77
|
-
when :postgres
|
78
|
-
ActiveRecord::Base.connection.postgresql_version
|
79
|
-
when :sqlite
|
80
|
-
ActiveRecord::Base.connection.database_version.to_s
|
81
|
-
else
|
82
|
-
warn "Unable to determine database version for #{database_type.inspect}"
|
83
|
-
end
|
85
|
+
ActiveRecord::Base.connection.try(:database_version) || issue_warning
|
84
86
|
end
|
85
87
|
|
86
88
|
def database_type
|
87
|
-
|
89
|
+
type = ActiveRecord::Base.connection.adapter_name.downcase.to_sym
|
90
|
+
type = :postgres if type == :postgresql
|
88
91
|
|
89
|
-
|
92
|
+
type
|
90
93
|
end
|
91
94
|
|
92
95
|
def execute_query(sql)
|
data/lib/appmap/railtie.rb
CHANGED
@@ -13,13 +13,11 @@ module AppMap
|
|
13
13
|
# AppMap events.
|
14
14
|
initializer 'appmap.subscribe', after: 'appmap.init' do |_| # params: app
|
15
15
|
require 'appmap/rails/sql_handler'
|
16
|
-
require 'appmap/rails/
|
16
|
+
require 'appmap/rails/request_handler'
|
17
17
|
ActiveSupport::Notifications.subscribe 'sql.sequel', AppMap::Rails::SQLHandler.new
|
18
18
|
ActiveSupport::Notifications.subscribe 'sql.active_record', AppMap::Rails::SQLHandler.new
|
19
|
-
|
20
|
-
|
21
|
-
ActiveSupport::Notifications.subscribe \
|
22
|
-
'process_action.action_controller', AppMap::Rails::ActionHandler::HTTPServerResponse.new
|
19
|
+
|
20
|
+
AppMap::Rails::RequestHandler::HookMethod.new.activate
|
23
21
|
end
|
24
22
|
|
25
23
|
# appmap.trace begins recording an AppMap trace and writes it to appmap.json.
|
data/lib/appmap/rspec.rb
CHANGED
@@ -124,8 +124,18 @@ module AppMap
|
|
124
124
|
def initialize(example)
|
125
125
|
super
|
126
126
|
|
127
|
+
webdriver_port = lambda do
|
128
|
+
return unless defined?(page) && page&.driver
|
129
|
+
|
130
|
+
# This is the ugliest thing ever but I don't want to lose it.
|
131
|
+
# All the WebDriver calls are getting app-mapped and it's really unclear
|
132
|
+
# what they are.
|
133
|
+
page.driver.options[:http_client].instance_variable_get('@server_url').port
|
134
|
+
end
|
135
|
+
|
127
136
|
warn "Starting recording of example #{example}" if AppMap::RSpec::LOG
|
128
137
|
@trace = AppMap.tracing.trace
|
138
|
+
@webdriver_port = webdriver_port.()
|
129
139
|
end
|
130
140
|
|
131
141
|
def finish
|
data/lib/appmap/trace.rb
CHANGED
@@ -3,9 +3,10 @@
|
|
3
3
|
module AppMap
|
4
4
|
module Trace
|
5
5
|
class ScopedMethod < SimpleDelegator
|
6
|
-
attr_reader :defined_class, :static
|
7
|
-
|
8
|
-
def initialize(defined_class, method, static)
|
6
|
+
attr_reader :package, :defined_class, :static
|
7
|
+
|
8
|
+
def initialize(package, defined_class, method, static)
|
9
|
+
@package = package
|
9
10
|
@defined_class = defined_class
|
10
11
|
@static = static
|
11
12
|
super(method)
|
@@ -32,9 +33,9 @@ module AppMap
|
|
32
33
|
@tracing.any?(&:enabled?)
|
33
34
|
end
|
34
35
|
|
35
|
-
def record_event(event, defined_class: nil, method: nil)
|
36
|
+
def record_event(event, package: nil, defined_class: nil, method: nil)
|
36
37
|
@tracing.each do |tracer|
|
37
|
-
tracer.record_event(event, defined_class: defined_class, method: method)
|
38
|
+
tracer.record_event(event, package: package, defined_class: defined_class, method: method)
|
38
39
|
end
|
39
40
|
end
|
40
41
|
|
@@ -71,11 +72,12 @@ module AppMap
|
|
71
72
|
# Record a program execution event.
|
72
73
|
#
|
73
74
|
# The event should be one of the MethodEvent subclasses.
|
74
|
-
def record_event(event, defined_class: nil, method: nil)
|
75
|
+
def record_event(event, package: nil, defined_class: nil, method: nil)
|
75
76
|
return unless @enabled
|
76
77
|
|
77
78
|
@events << event
|
78
|
-
@methods << Trace::ScopedMethod.new(defined_class, method, event.static)
|
79
|
+
@methods << Trace::ScopedMethod.new(package, defined_class, method, event.static) \
|
80
|
+
if package && defined_class && method && (event.event == :call)
|
79
81
|
end
|
80
82
|
|
81
83
|
# Gets a unique list of the methods that were invoked by the program.
|
data/lib/appmap/util.rb
CHANGED
@@ -36,6 +36,23 @@ module AppMap
|
|
36
36
|
[ fname, extension ].join
|
37
37
|
end
|
38
38
|
|
39
|
+
# sanitize_paths removes ephemeral values from objects with
|
40
|
+
# embedded paths (e.g. an event or a classmap), making events
|
41
|
+
# easier to compare across runs.
|
42
|
+
def sanitize_paths(h)
|
43
|
+
require 'hashie'
|
44
|
+
h.extend(Hashie::Extensions::DeepLocate)
|
45
|
+
keys = %i(path location)
|
46
|
+
h.deep_locate ->(k,v,o) {
|
47
|
+
next unless keys.include?(k)
|
48
|
+
|
49
|
+
fix = ->(v) {v.gsub(%r{#{Gem.dir}/gems/.*(?=lib)}, '')}
|
50
|
+
keys.each {|k| o[k] = fix.(o[k]) if o[k] }
|
51
|
+
}
|
52
|
+
|
53
|
+
h
|
54
|
+
end
|
55
|
+
|
39
56
|
# sanitize_event removes ephemeral values from an event, making
|
40
57
|
# events easier to compare across runs.
|
41
58
|
def sanitize_event(event, &block)
|
@@ -49,7 +66,7 @@ module AppMap
|
|
49
66
|
|
50
67
|
case event[:event]
|
51
68
|
when :call
|
52
|
-
event
|
69
|
+
sanitize_paths(event)
|
53
70
|
end
|
54
71
|
|
55
72
|
event
|
data/lib/appmap/version.rb
CHANGED
@@ -39,7 +39,7 @@ describe 'AbstractControllerBase' do
|
|
39
39
|
expect(appmap).to include(<<-SERVER_REQUEST.strip)
|
40
40
|
http_server_request:
|
41
41
|
request_method: POST
|
42
|
-
path_info: "/api/users
|
42
|
+
path_info: "/api/users"
|
43
43
|
SERVER_REQUEST
|
44
44
|
end
|
45
45
|
it 'Properly captures method parameters in the appmap' do
|
@@ -45,6 +45,12 @@ describe 'AbstractControllerBase' do
|
|
45
45
|
request_method: POST
|
46
46
|
path_info: "/api/users"
|
47
47
|
SERVER_REQUEST
|
48
|
+
|
49
|
+
expect(appmap).to include(<<-SERVER_RESPONSE.strip)
|
50
|
+
http_server_response:
|
51
|
+
status: 201
|
52
|
+
mime_type: application/json; charset=utf-8
|
53
|
+
SERVER_RESPONSE
|
48
54
|
end
|
49
55
|
|
50
56
|
it 'properly captures method parameters in the appmap' do
|
@@ -15,6 +15,20 @@ class SingletonMethod
|
|
15
15
|
'defined with self class scope'
|
16
16
|
end
|
17
17
|
|
18
|
+
module AddMethod
|
19
|
+
def self.included(base)
|
20
|
+
base.module_eval do
|
21
|
+
define_method "added_method" do
|
22
|
+
_added_method
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def _added_method
|
28
|
+
'defined by including a module'
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
18
32
|
# When called, do_include calls +include+ to bring in the module
|
19
33
|
# AddMethod. AddMethod defines a new instance method, which gets
|
20
34
|
# added to the singleton class of SingletonMethod.
|
@@ -32,23 +46,18 @@ class SingletonMethod
|
|
32
46
|
end
|
33
47
|
end
|
34
48
|
end
|
35
|
-
|
36
|
-
def to_s
|
37
|
-
'Singleton Method fixture'
|
38
|
-
end
|
39
|
-
end
|
40
49
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
_added_method
|
50
|
+
STRUCT_TEST = Struct.new(:attr) do
|
51
|
+
class << self
|
52
|
+
def say_struct_singleton
|
53
|
+
'singleton for a struct'
|
46
54
|
end
|
47
55
|
end
|
48
56
|
end
|
49
57
|
|
50
|
-
def
|
51
|
-
'
|
58
|
+
def to_s
|
59
|
+
'Singleton Method fixture'
|
52
60
|
end
|
53
61
|
end
|
54
62
|
|
63
|
+
|