appmap 0.44.0 → 0.47.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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]