appmap 0.45.0 → 0.48.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -95,7 +95,7 @@ module AppMap
95
95
  def after_hook(_receiver, call_event, start_time, return_value, exception)
96
96
  elapsed = TIME_NOW.call - start_time
97
97
  return_event = hook_package.handler_class.handle_return(call_event.id, elapsed, return_value, exception)
98
- AppMap.tracing.record_event return_event
98
+ AppMap.tracing.record_event(return_event) if return_event
99
99
  nil
100
100
  end
101
101
 
@@ -3,37 +3,15 @@
3
3
  module AppMap
4
4
  # Railtie connects the AppMap recorder to Rails-specific features.
5
5
  class Railtie < ::Rails::Railtie
6
- config.appmap = ActiveSupport::OrderedOptions.new
7
-
8
6
  # appmap.subscribe subscribes to ActiveSupport Notifications so that they can be recorded as
9
7
  # AppMap events.
10
8
  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
15
-
16
- AppMap::Rails::RequestHandler::HookMethod.new.activate
17
- end
18
-
19
- # appmap.trace begins recording an AppMap trace and writes it to appmap.json.
20
- # This behavior is only activated if the configuration setting app.config.appmap.enabled
21
- # is truthy.
22
- initializer 'appmap.trace', after: 'appmap.subscribe' do |app|
23
- lambda do
24
- return unless app.config.appmap.enabled
9
+ require 'appmap/handler/rails/sql_handler'
10
+ require 'appmap/handler/rails/request_handler'
11
+ ActiveSupport::Notifications.subscribe 'sql.sequel', AppMap::Handler::Rails::SQLHandler.new
12
+ ActiveSupport::Notifications.subscribe 'sql.active_record', AppMap::Handler::Rails::SQLHandler.new
25
13
 
26
- require 'appmap/command/record'
27
- require 'json'
28
- AppMap::Command::Record.new(AppMap.configuration).perform do |version, metadata, class_map, events|
29
- appmap = JSON.generate \
30
- version: version,
31
- metadata: metadata,
32
- classMap: class_map,
33
- events: events
34
- File.open('appmap.json', 'w').write(appmap)
35
- end
36
- end.call
14
+ AppMap::Handler::Rails::RequestHandler::HookMethod.new.activate
37
15
  end
38
16
  end
39
- end unless ENV['APPMAP_INITIALIZE'] == 'false'
17
+ end if ENV['APPMAP'] == 'true'
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
 
@@ -83,10 +111,22 @@ module AppMap
83
111
  @last_package_for_thread[Thread.current.object_id] = package if package
84
112
  @events << event
85
113
  static = event.static if event.respond_to?(:static)
86
- @methods << Trace::ScopedMethod.new(package, defined_class, method, static) \
114
+ @methods << Trace::RubyMethod.new(package, defined_class, method, static) \
87
115
  if package && defined_class && method && (event.event == :call)
88
116
  end
89
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
+
90
130
  # Gets the last package which was observed on the current thread.
91
131
  def last_package_for_current_thread
92
132
  @last_package_for_thread[Thread.current.object_id]
data/lib/appmap/util.rb CHANGED
@@ -93,6 +93,24 @@ module AppMap
93
93
  matching_headers.blank? ? nil : matching_headers
94
94
  end
95
95
 
96
+ def normalize_path(path)
97
+ if path.index(Dir.pwd) == 0
98
+ path[Dir.pwd.length + 1..-1]
99
+ else
100
+ path
101
+ end
102
+ end
103
+
104
+ # Convert a Rails-style path from /org/:org_id(.:format)
105
+ # to Swagger-style paths like /org/{org_id}
106
+ def swaggerize_path(path)
107
+ path = path.split('(.')[0]
108
+ tokens = path.split('/')
109
+ tokens.map do |token|
110
+ token.gsub /^:(.*)/, '{\1}'
111
+ end.join('/')
112
+ end
113
+
96
114
  # Atomically writes AppMap data to +filename+.
97
115
  def write_appmap(filename, appmap)
98
116
  require 'fileutils'
@@ -3,7 +3,7 @@
3
3
  module AppMap
4
4
  URL = 'https://github.com/applandinc/appmap-ruby'
5
5
 
6
- VERSION = '0.45.0'
6
+ VERSION = '0.48.0'
7
7
 
8
- APPMAP_FORMAT_VERSION = '1.5.0'
8
+ APPMAP_FORMAT_VERSION = '1.5.1'
9
9
  end
data/package-lock.json CHANGED
@@ -551,9 +551,9 @@
551
551
  }
552
552
  },
553
553
  "lodash": {
554
- "version": "4.17.19",
555
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz",
556
- "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ=="
554
+ "version": "4.17.21",
555
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
556
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
557
557
  },
558
558
  "longest": {
559
559
  "version": "1.0.1",
@@ -42,7 +42,7 @@ describe 'Rails' do
42
42
  hash_including(
43
43
  'http_server_request' => hash_including(
44
44
  'request_method' => 'POST',
45
- 'normalized_path_info' => '/api/users(.:format)',
45
+ 'normalized_path_info' => '/api/users',
46
46
  'path_info' => '/api/users'
47
47
  ),
48
48
  'message' => include(
@@ -144,7 +144,49 @@ describe 'Rails' do
144
144
  end
145
145
 
146
146
  describe 'a UI route' do
147
- describe 'rendering a page' do
147
+ describe 'rendering a page using a template file' do
148
+ let(:appmap_json_file) do
149
+ 'UsersController_GET_users_lists_the_users.appmap.json'
150
+ end
151
+
152
+ it 'records the template file' do
153
+ expect(events).to include hash_including(
154
+ 'event' => 'call',
155
+ 'defined_class' => 'app_views_users_index_html_haml',
156
+ 'method_id' => 'render',
157
+ 'path' => 'app/views/users/index.html.haml'
158
+ )
159
+
160
+ expect(appmap['classMap']).to include hash_including(
161
+ 'name' => 'app/views',
162
+ 'children' => include(hash_including(
163
+ 'name' => 'app_views_users_index_html_haml',
164
+ 'children' => include(hash_including(
165
+ 'name' => 'render',
166
+ 'type' => 'function',
167
+ 'location' => 'app/views/users/index.html.haml',
168
+ 'static' => true,
169
+ 'labels' => [ 'mvc.template' ]
170
+ ))
171
+ ))
172
+ )
173
+ expect(appmap['classMap']).to include hash_including(
174
+ 'name' => 'app/views',
175
+ 'children' => include(hash_including(
176
+ 'name' => 'app_views_layouts_application_html_haml',
177
+ 'children' => include(hash_including(
178
+ 'name' => 'render',
179
+ 'type' => 'function',
180
+ 'location' => 'app/views/layouts/application.html.haml',
181
+ 'static' => true,
182
+ 'labels' => [ 'mvc.template' ]
183
+ ))
184
+ ))
185
+ )
186
+ end
187
+ end
188
+
189
+ describe 'rendering a page using a text template' do
148
190
  let(:appmap_json_file) do
149
191
  'UsersController_GET_users_login_shows_the_user.appmap.json'
150
192
  end
@@ -155,7 +197,7 @@ describe 'Rails' do
155
197
  'http_server_request' => {
156
198
  'request_method' => 'GET',
157
199
  'path_info' => '/users/alice',
158
- 'normalized_path_info' => '/users/:id(.:format)',
200
+ 'normalized_path_info' => '/users/{id}',
159
201
  'headers' => {
160
202
  'Host' => 'test.host',
161
203
  'User-Agent' => 'Rails Testing'
@@ -165,19 +207,36 @@ describe 'Rails' do
165
207
  )
166
208
  end
167
209
 
210
+ it 'ignores the text template' do
211
+ expect(events).to_not include hash_including(
212
+ 'event' => 'call',
213
+ 'method_id' => 'render',
214
+ 'render_template' => anything
215
+ )
216
+
217
+ expect(appmap['classMap']).to_not include hash_including(
218
+ 'name' => 'views',
219
+ 'children' => include(hash_including(
220
+ 'name' => 'ViewTemplate',
221
+ 'children' => include(hash_including(
222
+ 'name' => 'render',
223
+ 'type' => 'function',
224
+ 'location' => 'text template'
225
+ ))
226
+ ))
227
+ )
228
+ end
229
+
168
230
  it 'records and labels view rendering' do
169
231
  expect(events).to include hash_including(
170
232
  'event' => 'call',
171
233
  'thread_id' => Numeric,
172
- 'defined_class' => 'ActionView::Renderer',
173
- 'method_id' => 'render',
174
- 'path' => String,
175
- 'lineno' => Integer,
176
- 'static' => false
234
+ 'defined_class' => 'inline_template',
235
+ 'method_id' => 'render'
177
236
  )
178
237
 
179
238
  expect(appmap['classMap']).to include hash_including(
180
- 'name' => 'action_view',
239
+ 'name' => 'actionview',
181
240
  'children' => include(hash_including(
182
241
  'name' => 'ActionView',
183
242
  'children' => include(hash_including(