appmap 0.44.0 → 0.47.1

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.
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'appmap/event'
4
+
5
+ module AppMap
6
+ module Handler
7
+ module Rails
8
+ class Template
9
+ LOG = (ENV['APPMAP_TEMPLATE_DEBUG'] == 'true' || ENV['DEBUG'] == 'true')
10
+
11
+ # All the code which is touched by the AppMap is recorded in the classMap.
12
+ # This duck-typed 'method' is used to represent a view template as a package,
13
+ # class, and method in the classMap.
14
+ # The class name is generated from the template path. The package name is
15
+ # 'app/views', and the method name is 'render'. The source location of the method
16
+ # is, of course, the path to the view template.
17
+ TemplateMethod = Struct.new(:path) do
18
+ private_instance_methods :path
19
+ attr_reader :class_name
20
+
21
+ def initialize(path)
22
+ super
23
+
24
+ @class_name = path.parameterize.underscore
25
+ end
26
+
27
+ def package
28
+ 'app/views'
29
+ end
30
+
31
+ def name
32
+ 'render'
33
+ end
34
+
35
+ def source_location
36
+ path
37
+ end
38
+
39
+ def static
40
+ true
41
+ end
42
+
43
+ def comment
44
+ nil
45
+ end
46
+
47
+ def labels
48
+ [ 'mvc.template' ]
49
+ end
50
+ end
51
+
52
+ # TemplateCall is a type of function call which is specialized to view template rendering. Since
53
+ # there isn't really a perfect method in Rails to hook, this one is synthesized from the available
54
+ # information.
55
+ class TemplateCall < AppMap::Event::MethodEvent
56
+ # This is basically the +self+ parameter.
57
+ attr_reader :render_instance
58
+ # Path to the view template.
59
+ attr_accessor :path
60
+
61
+ def initialize(render_instance)
62
+ super :call
63
+
64
+ AppMap::Event::MethodEvent.build_from_invocation(:call, event: self)
65
+ @render_instance = render_instance
66
+ end
67
+
68
+ def static?
69
+ true
70
+ end
71
+
72
+ def to_h
73
+ super.tap do |h|
74
+ h[:defined_class] = path ? path.parameterize.underscore : 'inline_template'
75
+ h[:method_id] = 'render'
76
+ h[:path] = path
77
+ h[:static] = static?
78
+ h[:parameters] = []
79
+ h[:receiver] = {
80
+ class: AppMap::Event::MethodEvent.best_class_name(render_instance),
81
+ object_id: render_instance.__id__,
82
+ value: AppMap::Event::MethodEvent.display_string(render_instance)
83
+ }
84
+ h.compact
85
+ end
86
+ end
87
+ end
88
+
89
+ TEMPLATE_RENDERER = 'appmap.handler.rails.template.renderer'
90
+
91
+ # Hooks the ActionView::Resolver methods +find_all+, +find_all_anywhere+. The resolver is used
92
+ # during template rendering to lookup the template file path from parameters such as the
93
+ # template name, prefix, and partial (boolean).
94
+ class ResolverHandler
95
+ class << self
96
+ # Handled as a normal function call.
97
+ def handle_call(defined_class, hook_method, receiver, args)
98
+ name, prefix, partial = args
99
+ warn "Resolver: #{{ name: name, prefix: prefix, partial: partial }}" if LOG
100
+
101
+ AppMap::Handler::Function.handle_call(defined_class, hook_method, receiver, args)
102
+ end
103
+
104
+ # When the resolver returns, look to see if there is template rendering underway.
105
+ # If so, populate the template path. In all cases, add a TemplateMethod so that the
106
+ # template will be recorded in the classMap.
107
+ def handle_return(call_event_id, elapsed, return_value, exception)
108
+ renderer = Array(Thread.current[TEMPLATE_RENDERER]).last
109
+ path_obj = Array(return_value).first
110
+
111
+ warn "Resolver return: #{path_obj}" if LOG
112
+
113
+ if path_obj
114
+ path = if path_obj.respond_to?(:identifier) && path_obj.inspect.index('#<')
115
+ path_obj.identifier
116
+ else
117
+ path_obj.inspect
118
+ end
119
+ path = path[Dir.pwd.length + 1..-1] if path.index(Dir.pwd) == 0
120
+ AppMap.tracing.record_method(TemplateMethod.new(path))
121
+ renderer.path ||= path if renderer
122
+ end
123
+
124
+ AppMap::Handler::Function.handle_return(call_event_id, elapsed, return_value, exception)
125
+ end
126
+ end
127
+ end
128
+
129
+ # Hooks the ActionView::Renderer method +render+. This method is used by Rails to perform
130
+ # template rendering. The TemplateCall event which is emitted by this handler has a
131
+ # +path+ parameter, which is nil until it's filled in by a ResolverHandler.
132
+ class RenderHandler
133
+ class << self
134
+ def handle_call(defined_class, hook_method, receiver, args)
135
+ context, options = args
136
+
137
+ warn "Renderer: #{options}" if LOG
138
+
139
+ TemplateCall.new(receiver).tap do |call|
140
+ Thread.current[TEMPLATE_RENDERER] ||= []
141
+ Thread.current[TEMPLATE_RENDERER] << call
142
+ end
143
+ end
144
+
145
+ def handle_return(call_event_id, elapsed, return_value, exception)
146
+ Array(Thread.current[TEMPLATE_RENDERER]).pop
147
+
148
+ AppMap::Event::MethodReturnIgnoreValue.build_from_invocation(call_event_id, elapsed: elapsed)
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end
154
+ end
155
+ end
data/lib/appmap/hook.rb CHANGED
@@ -5,6 +5,7 @@ require 'English'
5
5
  module AppMap
6
6
  class Hook
7
7
  LOG = (ENV['APPMAP_DEBUG'] == 'true' || ENV['DEBUG'] == 'true')
8
+ LOG_HOOK = (ENV['DEBUG_HOOK'] == 'true')
8
9
 
9
10
  OBJECT_INSTANCE_METHODS = %i[! != !~ <=> == === =~ __id__ __send__ class clone define_singleton_method display dup enum_for eql? equal? extend freeze frozen? hash inspect instance_eval instance_exec instance_of? instance_variable_defined? instance_variable_get instance_variable_set instance_variables is_a? itself kind_of? method methods nil? object_id private_methods protected_methods public_method public_methods public_send remove_instance_variable respond_to? send singleton_class singleton_method singleton_methods taint tainted? tap then to_enum to_s to_h to_a trust untaint untrust untrusted? yield_self].freeze
10
11
  OBJECT_STATIC_METHODS = %i[! != !~ < <= <=> == === =~ > >= __id__ __send__ alias_method allocate ancestors attr attr_accessor attr_reader attr_writer autoload autoload? class class_eval class_exec class_variable_defined? class_variable_get class_variable_set class_variables clone const_defined? const_get const_missing const_set constants define_method define_singleton_method deprecate_constant display dup enum_for eql? equal? extend freeze frozen? hash include include? included_modules inspect instance_eval instance_exec instance_method instance_methods instance_of? instance_variable_defined? instance_variable_get instance_variable_set instance_variables is_a? itself kind_of? method method_defined? methods module_eval module_exec name new nil? object_id prepend private_class_method private_constant private_instance_methods private_method_defined? private_methods protected_instance_methods protected_method_defined? protected_methods public_class_method public_constant public_instance_method public_instance_methods public_method public_method_defined? public_methods public_send remove_class_variable remove_instance_variable remove_method respond_to? send singleton_class singleton_class? singleton_method singleton_methods superclass taint tainted? tap then to_enum to_s trust undef_method untaint untrust untrusted? yield_self].freeze
@@ -35,73 +36,21 @@ module AppMap
35
36
 
36
37
  def initialize(config)
37
38
  @config = config
39
+ @trace_locations = []
40
+ # Paths that are known to be non-tracing
41
+ @notrace_paths = Set.new
38
42
  end
39
43
 
40
44
  # Observe class loading and hook all methods which match the config.
41
- def enable &block
45
+ def enable(&block)
42
46
  require 'appmap/hook/method'
43
47
 
44
48
  hook_builtins
45
49
 
46
- tp = TracePoint.new(:end) do |trace_point|
47
- cls = trace_point.self
50
+ @trace_begin = TracePoint.new(:class, &method(:trace_class))
51
+ @trace_end = TracePoint.new(:end, &method(:trace_end))
48
52
 
49
- instance_methods = cls.public_instance_methods(false) - OBJECT_INSTANCE_METHODS
50
- # NoMethodError: private method `singleton_class' called for Rack::MiniProfiler:Class
51
- class_methods = begin
52
- if cls.respond_to?(:singleton_class)
53
- cls.singleton_class.public_instance_methods(false) - instance_methods - OBJECT_STATIC_METHODS
54
- else
55
- []
56
- end
57
- rescue NameError
58
- []
59
- end
60
-
61
- hook = lambda do |hook_cls|
62
- lambda do |method_id|
63
- # Don't try and trace the AppMap methods or there will be
64
- # a stack overflow in the defined hook method.
65
- return if (hook_cls&.name || '').split('::')[0] == AppMap.name
66
-
67
- method = begin
68
- hook_cls.public_instance_method(method_id)
69
- rescue NameError
70
- warn "AppMap: Method #{hook_cls} #{method.name} is not accessible" if LOG
71
- return
72
- end
73
-
74
- warn "AppMap: Examining #{hook_cls} #{method.name}" if LOG
75
-
76
- disasm = RubyVM::InstructionSequence.disasm(method)
77
- # Skip methods that have no instruction sequence, as they are obviously trivial.
78
- next unless disasm
79
-
80
- next if config.never_hook?(method)
81
-
82
- next unless \
83
- config.always_hook?(hook_cls, method.name) ||
84
- config.included_by_location?(method)
85
-
86
- package = config.package_for_method(method)
87
-
88
- hook_method = Hook::Method.new(package, hook_cls, method)
89
-
90
- hook_method.activate
91
- end
92
- end
93
-
94
- instance_methods.each(&hook.(cls))
95
- # NoMethodError: private method `singleton_class' called for Rack::MiniProfiler:Class
96
- begin
97
- class_methods.each(&hook.(cls.singleton_class)) if cls.respond_to?(:singleton_class)
98
- rescue NameError
99
- # NameError:
100
- # uninitialized constant Faraday::Connection
101
- end
102
- end
103
-
104
- tp.enable(&block)
53
+ @trace_begin.enable(&block)
105
54
  end
106
55
 
107
56
  # hook_builtins builds hooks for code that is built in to the Ruby standard library.
@@ -115,30 +64,119 @@ module AppMap
115
64
  end
116
65
  end
117
66
 
118
- config.builtin_methods.each do |class_name, hooks|
67
+ config.builtin_hooks.each do |class_name, hooks|
119
68
  Array(hooks).each do |hook|
120
69
  require hook.package.package_name if hook.package.package_name
121
70
  Array(hook.method_names).each do |method_name|
122
71
  method_name = method_name.to_sym
72
+ base_cls = class_from_string.(class_name)
123
73
 
124
- cls = class_from_string.(class_name)
125
- method = \
126
- begin
127
- cls.instance_method(method_name)
128
- rescue NameError
129
- cls.method(method_name) rescue nil
130
- end
131
-
132
- next if config.never_hook?(method)
74
+ hook_method = lambda do |entry|
75
+ cls, method = entry
76
+ return false if config.never_hook?(cls, method)
133
77
 
134
- if method
135
78
  Hook::Method.new(hook.package, cls, method).activate
79
+ end
80
+
81
+ methods = []
82
+ methods << [ base_cls, base_cls.public_instance_method(method_name) ] rescue nil
83
+ if base_cls.respond_to?(:singleton_class)
84
+ methods << [ base_cls.singleton_class, base_cls.singleton_class.public_instance_method(method_name) ] rescue nil
85
+ end
86
+ methods.compact!
87
+ if methods.empty?
88
+ warn "Method #{method_name} not found on #{base_cls.name}"
136
89
  else
137
- warn "Method #{method_name} not found on #{cls.name}"
90
+ methods.each(&hook_method)
138
91
  end
139
92
  end
140
93
  end
141
94
  end
142
95
  end
96
+
97
+ protected
98
+
99
+ def trace_class(trace_point)
100
+ path = trace_point.path
101
+
102
+ return if @notrace_paths.member?(path)
103
+
104
+ if config.path_enabled?(path)
105
+ location = trace_location(trace_point)
106
+ warn "Entering hook-enabled location #{location}" if Hook::LOG || Hook::LOG_HOOK
107
+ @trace_locations << location
108
+ unless @trace_end.enabled?
109
+ warn "Enabling hooking" if Hook::LOG || Hook::LOG_HOOK
110
+ @trace_end.enable
111
+ end
112
+ else
113
+ @notrace_paths << path
114
+ end
115
+ end
116
+
117
+ def trace_location(trace_point)
118
+ [ trace_point.path, trace_point.lineno ].join(':')
119
+ end
120
+
121
+ def trace_end(trace_point)
122
+ cls = trace_point.self
123
+
124
+ instance_methods = cls.public_instance_methods(false) - OBJECT_INSTANCE_METHODS
125
+ # NoMethodError: private method `singleton_class' called for Rack::MiniProfiler:Class
126
+ class_methods = begin
127
+ if cls.respond_to?(:singleton_class)
128
+ cls.singleton_class.public_instance_methods(false) - instance_methods - OBJECT_STATIC_METHODS
129
+ else
130
+ []
131
+ end
132
+ rescue NameError
133
+ []
134
+ end
135
+
136
+ hook = lambda do |hook_cls|
137
+ lambda do |method_id|
138
+ # Don't try and trace the AppMap methods or there will be
139
+ # a stack overflow in the defined hook method.
140
+ next if %w[Marshal AppMap ActiveSupport].member?((hook_cls&.name || '').split('::')[0])
141
+
142
+ next if method_id == :call
143
+
144
+ method = begin
145
+ hook_cls.public_instance_method(method_id)
146
+ rescue NameError
147
+ warn "AppMap: Method #{hook_cls} #{method.name} is not accessible" if LOG
148
+ next
149
+ end
150
+
151
+ warn "AppMap: Examining #{hook_cls} #{method.name}" if LOG
152
+
153
+ disasm = RubyVM::InstructionSequence.disasm(method)
154
+ # Skip methods that have no instruction sequence, as they are obviously trivial.
155
+ next unless disasm
156
+
157
+ package = config.lookup_package(hook_cls, method)
158
+ next unless package
159
+
160
+ Hook::Method.new(package, hook_cls, method).activate
161
+ end
162
+ end
163
+
164
+ instance_methods.each(&hook.(cls))
165
+ begin
166
+ # NoMethodError: private method `singleton_class' called for Rack::MiniProfiler:Class
167
+ class_methods.each(&hook.(cls.singleton_class)) if cls.respond_to?(:singleton_class)
168
+ rescue NameError
169
+ # NameError:
170
+ # uninitialized constant Faraday::Connection
171
+ warn "NameError in #{__FILE__}: #{$!.message}"
172
+ end
173
+
174
+ location = @trace_locations.pop
175
+ warn "Leaving hook-enabled location #{location}" if Hook::LOG || Hook::LOG_HOOK
176
+ if @trace_locations.empty?
177
+ warn "Disabling hooking" if Hook::LOG || Hook::LOG_HOOK
178
+ @trace_end.disable
179
+ end
180
+ end
143
181
  end
144
182
  end
@@ -76,7 +76,7 @@ module AppMap
76
76
  raise
77
77
  ensure
78
78
  with_disabled_hook.call do
79
- after_hook.call(self, call_event, start_time, return_value, exception)
79
+ after_hook.call(self, call_event, start_time, return_value, exception) if call_event
80
80
  end
81
81
  end
82
82
  end
@@ -87,18 +87,16 @@ module AppMap
87
87
  protected
88
88
 
89
89
  def before_hook(receiver, defined_class, args)
90
- require 'appmap/event'
91
- call_event = AppMap::Event::MethodCall.build_from_invocation(defined_class, hook_method, receiver, args)
92
- AppMap.tracing.record_event call_event, package: hook_package, defined_class: defined_class, method: hook_method
90
+ call_event = hook_package.handler_class.handle_call(defined_class, hook_method, receiver, args)
91
+ AppMap.tracing.record_event(call_event, package: hook_package, defined_class: defined_class, method: hook_method) if call_event
93
92
  [ call_event, TIME_NOW.call ]
94
93
  end
95
94
 
96
95
  def after_hook(_receiver, call_event, start_time, return_value, exception)
97
- require 'appmap/event'
98
96
  elapsed = TIME_NOW.call - start_time
99
- return_event = \
100
- AppMap::Event::MethodReturn.build_from_invocation call_event.id, elapsed, return_value, exception
101
- AppMap.tracing.record_event return_event
97
+ return_event = hook_package.handler_class.handle_return(call_event.id, elapsed, return_value, exception)
98
+ AppMap.tracing.record_event(return_event) if return_event
99
+ nil
102
100
  end
103
101
 
104
102
  def with_disabled_hook(&function)
@@ -8,12 +8,12 @@ module AppMap
8
8
  # appmap.subscribe subscribes to ActiveSupport Notifications so that they can be recorded as
9
9
  # AppMap events.
10
10
  initializer 'appmap.subscribe' do |_| # params: app
11
- require 'appmap/rails/sql_handler'
12
- require 'appmap/rails/request_handler'
13
- ActiveSupport::Notifications.subscribe 'sql.sequel', AppMap::Rails::SQLHandler.new
14
- ActiveSupport::Notifications.subscribe 'sql.active_record', AppMap::Rails::SQLHandler.new
11
+ require 'appmap/handler/rails/sql_handler'
12
+ require 'appmap/handler/rails/request_handler'
13
+ ActiveSupport::Notifications.subscribe 'sql.sequel', AppMap::Handler::Rails::SQLHandler.new
14
+ ActiveSupport::Notifications.subscribe 'sql.active_record', AppMap::Handler::Rails::SQLHandler.new
15
15
 
16
- AppMap::Rails::RequestHandler::HookMethod.new.activate
16
+ AppMap::Handler::Rails::RequestHandler::HookMethod.new.activate
17
17
  end
18
18
 
19
19
  # appmap.trace begins recording an AppMap trace and writes it to appmap.json.
data/lib/appmap/trace.rb CHANGED
@@ -2,14 +2,36 @@
2
2
 
3
3
  module AppMap
4
4
  module Trace
5
- class ScopedMethod < SimpleDelegator
6
- attr_reader :package, :defined_class, :static
5
+ class RubyMethod
6
+ attr_reader :class_name, :static
7
7
 
8
- def initialize(package, defined_class, method, static)
8
+ def initialize(package, class_name, method, static)
9
9
  @package = package
10
- @defined_class = defined_class
10
+ @class_name = class_name
11
+ @method = method
11
12
  @static = static
12
- super(method)
13
+ end
14
+
15
+ def source_location
16
+ @method.source_location
17
+ end
18
+
19
+ def comment
20
+ @method.comment
21
+ rescue MethodSource::SourceNotFoundError
22
+ nil
23
+ end
24
+
25
+ def package
26
+ @package.name
27
+ end
28
+
29
+ def name
30
+ @method.name
31
+ end
32
+
33
+ def labels
34
+ @package.labels
13
35
  end
14
36
  end
15
37
 
@@ -43,6 +65,12 @@ module AppMap
43
65
  end
44
66
  end
45
67
 
68
+ def record_method(method)
69
+ @tracers.each do |tracer|
70
+ tracer.record_method(method)
71
+ end
72
+ end
73
+
46
74
  def delete(tracer)
47
75
  return unless @tracers.member?(tracer)
48
76
 
@@ -82,10 +110,23 @@ module AppMap
82
110
 
83
111
  @last_package_for_thread[Thread.current.object_id] = package if package
84
112
  @events << event
85
- @methods << Trace::ScopedMethod.new(package, defined_class, method, event.static) \
113
+ static = event.static if event.respond_to?(:static)
114
+ @methods << Trace::RubyMethod.new(package, defined_class, method, static) \
86
115
  if package && defined_class && method && (event.event == :call)
87
116
  end
88
117
 
118
+ # +method+ should be duck-typed to respond to the following:
119
+ # * package
120
+ # * defined_class
121
+ # * name
122
+ # * static
123
+ # * comment
124
+ # * labels
125
+ # * source_location
126
+ def record_method(method)
127
+ @methods << method
128
+ end
129
+
89
130
  # Gets the last package which was observed on the current thread.
90
131
  def last_package_for_current_thread
91
132
  @last_package_for_thread[Thread.current.object_id]