appmap 0.67.1 → 0.68.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 66a5dd9600679b05f4789e2531ca6151bc46fa5e95a7458d401f67b3b94861e7
4
- data.tar.gz: 89380ec4d7ebfabe615a187a1d0a10ad107d06df4e5bc50c987b62a3f27c4edf
3
+ metadata.gz: 4d54229f9f87639b89c9388722e8e9ee30e327a39f9e4effe40739033229de5f
4
+ data.tar.gz: 2a87b31d06444d8091f78ec00e0751b900f2068ee87003c945b3b6da3e58602d
5
5
  SHA512:
6
- metadata.gz: 89b13f75e8fbe46e5ddf9a5ec817eaebfd0356d124f2a117e74c24a44d07a8d89f9b0e5afcacbe9ea1277f34979955f30d441c5325ce8aa954bfeda08ce2f03f
7
- data.tar.gz: 27c0ba4946df49d998aa6cfa5cfae8cbcc10bed6ec0590ac06c106e99d828f03bfb675b4a67317b1abd1b87ba1c0016699f44470585c0954d431198ee34b6b44
6
+ metadata.gz: 3a6efa5f3fbac309a6df73405f281deaaf4a577de3d22466e04e2a8587af59dc418e99c032387f5cf00d604da2d43cedfaa745f63589ae39f8edba685f9ab5b9
7
+ data.tar.gz: 390d01607fcf6ae24852758a50f4f174b90c4d59f5b008c77c60108540a9d58c65917a36242ce495430fc7bfe38bdd58a4be050d106ebbb4c7ad6a3ea7d2104e
data/CHANGELOG.md CHANGED
@@ -1,3 +1,16 @@
1
+ # [0.68.0](https://github.com/applandinc/appmap-ruby/compare/v0.67.1...v0.68.0) (2021-11-05)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * Require weakref ([2f94f80](https://github.com/applandinc/appmap-ruby/commit/2f94f808bd3327aa3fc7fd8e6a3428a5da3a29bb))
7
+
8
+
9
+ ### Features
10
+
11
+ * Externalize config of hooks ([8080222](https://github.com/applandinc/appmap-ruby/commit/8080222ce5b61d9824eaf20410d7b9b94b679890))
12
+ * Support loading hook config via path env vars ([4856483](https://github.com/applandinc/appmap-ruby/commit/48564837784f8b0e87c4286ad3e2f6cb2d272dcf))
13
+
1
14
  ## [0.67.1](https://github.com/applandinc/appmap-ruby/compare/v0.67.0...v0.67.1) (2021-11-02)
2
15
 
3
16
 
@@ -0,0 +1,4 @@
1
+ - method: JSON::Ext::Parser#parse
2
+ label: format.json.parse
3
+ - method: JSON::Ext::Generator::State#generate
4
+ label: format.json.generate
@@ -0,0 +1,3 @@
1
+ - method: Net::HTTP#request
2
+ label: protocol.http
3
+ handler_class: AppMap::Handler::NetHTTP
@@ -0,0 +1,16 @@
1
+ - method: OpenSSL::PKey::PKey#sign
2
+ label: crypto.pkey
3
+ - methods:
4
+ - OpenSSL::X509::Request#sign
5
+ - OpenSSL::X509::Request#verify
6
+ label: crypto.x509
7
+ - method: OpenSSL::X509::Certificate#sign
8
+ label: crypto.x509
9
+ - methods:
10
+ - OpenSSL::PKCS5#pbkdf2_hmac
11
+ - OpenSSL::PKCS5#pbkdf2_hmac_sha1
12
+ label: crypto.pkcs5
13
+ - method: OpenSSL::Cipher#encrypt
14
+ label: crypto.encrypt
15
+ - method: OpenSSL::Cipher#decrypt
16
+ label: crypto.decrypt
@@ -0,0 +1,10 @@
1
+ - methods:
2
+ - Psych#load
3
+ - Psych#load_stream
4
+ - Psych#parse
5
+ - Psych#parse_stream
6
+ label: format.yaml.parse
7
+ - methods:
8
+ - Psych#dump
9
+ - Psych#dump_stream
10
+ label: format.yaml.generate
data/lib/appmap/config.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'pathname'
3
4
  require 'set'
4
5
  require 'yaml'
5
6
  require 'appmap/util'
@@ -11,17 +12,20 @@ require 'appmap/depends/configuration'
11
12
 
12
13
  module AppMap
13
14
  class Config
14
- # Specifies a code +path+ to be mapped.
15
+ # Specifies a logical code package be mapped.
16
+ # This can be a project source folder, a Gem, or a builtin.
17
+ #
15
18
  # Options:
16
19
  #
20
+ # * +path+ indicates a relative path to a code folder.
17
21
  # * +gem+ may indicate a gem name that "owns" the path
18
- # * +package_name+ can be used to make sure that the code is required so that it can be loaded. This is generally used with
22
+ # * +require_name+ can be used to make sure that the code is required so that it can be loaded. This is generally used with
19
23
  # builtins, or when the path to be required is not automatically required when bundler requires the gem.
20
24
  # * +exclude+ can be used used to exclude sub-paths. Generally not used with +gem+.
21
25
  # * +labels+ is used to apply labels to matching code. This is really only useful when the package will be applied to
22
26
  # specific functions, via TargetMethods.
23
27
  # * +shallow+ indicates shallow mapping, in which only the entrypoint to a gem is recorded.
24
- Package = Struct.new(:path, :gem, :package_name, :exclude, :labels, :shallow) do
28
+ Package = Struct.new(:name, :path, :gem, :require_name, :exclude, :labels, :shallow, :builtin) do
25
29
  # This is for internal use only.
26
30
  private_methods :gem
27
31
 
@@ -43,20 +47,24 @@ module AppMap
43
47
  class << self
44
48
  # Builds a package for a path, such as `app/models` in a Rails app. Generally corresponds to a `path:` entry
45
49
  # in appmap.yml. Also used for mapping specific methods via TargetMethods.
46
- def build_from_path(path, shallow: false, package_name: nil, exclude: [], labels: [])
47
- Package.new(path, nil, package_name, exclude, labels, shallow)
50
+ def build_from_path(path, shallow: false, require_name: nil, exclude: [], labels: [])
51
+ Package.new(path, path, nil, require_name, exclude, labels, shallow)
52
+ end
53
+
54
+ def build_from_builtin(path, shallow: false, require_name: nil, exclude: [], labels: [])
55
+ Package.new(path, path, nil, require_name, exclude, labels, shallow, true)
48
56
  end
49
57
 
50
58
  # Builds a package for gem. Generally corresponds to a `gem:` entry in appmap.yml. Also used when mapping
51
59
  # a builtin.
52
- def build_from_gem(gem, shallow: true, package_name: nil, exclude: [], labels: [], optional: false, force: false)
60
+ def build_from_gem(gem, shallow: true, require_name: nil, exclude: [], labels: [], optional: false, force: false)
53
61
  if !force && %w[method_source activesupport].member?(gem)
54
62
  warn "WARNING: #{gem} cannot be AppMapped because it is a dependency of the appmap gem"
55
63
  return
56
64
  end
57
65
  path = gem_path(gem, optional)
58
66
  if path
59
- Package.new(path, gem, package_name, exclude, labels, shallow)
67
+ Package.new(gem, path, gem, require_name, exclude, labels, shallow)
60
68
  else
61
69
  AppMap::Util.startup_message "#{gem} is not available in the bundle"
62
70
  end
@@ -74,19 +82,16 @@ module AppMap
74
82
  end
75
83
  end
76
84
 
77
- def name
78
- gem || path
79
- end
80
-
81
85
  def to_h
82
86
  {
87
+ name: name,
83
88
  path: path,
84
- package_name: package_name,
85
89
  gem: gem,
86
- handler_class: handler_class.name,
90
+ require_name: require_name,
91
+ handler_class: handler_class ? handler_class.name : nil,
87
92
  exclude: Util.blank?(exclude) ? nil : exclude,
88
93
  labels: Util.blank?(labels) ? nil : labels,
89
- shallow: shallow
94
+ shallow: shallow.nil? ? nil : shallow,
90
95
  }.compact
91
96
  end
92
97
  end
@@ -96,12 +101,12 @@ module AppMap
96
101
  attr_reader :method_names, :package
97
102
 
98
103
  def initialize(method_names, package)
99
- @method_names = method_names
104
+ @method_names = Array(method_names).map(&:to_sym)
100
105
  @package = package
101
106
  end
102
107
 
103
108
  def include_method?(method_name)
104
- Array(method_names).include?(method_name)
109
+ method_names.include?(method_name.to_sym)
105
110
  end
106
111
 
107
112
  def to_h
@@ -110,6 +115,8 @@ module AppMap
110
115
  method_names: method_names
111
116
  }
112
117
  end
118
+
119
+ alias as_json to_h
113
120
  end
114
121
  private_constant :TargetMethods
115
122
 
@@ -117,11 +124,11 @@ module AppMap
117
124
  # entry in appmap.yml. When the Config is initialized, each Function is converted into
118
125
  # a Package and TargetMethods. It's called a Function rather than a Method, because Function
119
126
  # is the AppMap terminology.
120
- Function = Struct.new(:package, :cls, :labels, :function_names, :builtin, :package_name) do # :nodoc:
127
+ Function = Struct.new(:package, :cls, :labels, :function_names, :builtin, :require_name) do # :nodoc:
121
128
  def to_h
122
129
  {
123
130
  package: package,
124
- package_name: package_name,
131
+ require_name: require_name,
125
132
  class: cls,
126
133
  labels: labels,
127
134
  functions: function_names.map(&:to_sym),
@@ -138,9 +145,15 @@ module AppMap
138
145
  private_constant :MethodHook
139
146
 
140
147
  class << self
141
- def package_hooks(gem_name, methods, handler_class: nil, package_name: nil)
148
+ def package_hooks(methods, path: nil, gem: nil, force: false, builtin: false, handler_class: nil, require_name: nil)
142
149
  Array(methods).map do |method|
143
- package = Package.build_from_gem(gem_name, package_name: package_name, labels: method.labels, shallow: false, optional: true)
150
+ package = if builtin
151
+ Package.build_from_builtin(path, require_name: require_name, labels: method.labels, shallow: false)
152
+ elsif gem
153
+ Package.build_from_gem(gem, require_name: require_name, labels: method.labels, shallow: false, force: force, optional: true)
154
+ elsif path
155
+ Package.build_from_path(path, require_name: require_name, labels: method.labels, shallow: false)
156
+ end
144
157
  next unless package
145
158
 
146
159
  package.handler_class = handler_class if handler_class
@@ -151,87 +164,107 @@ module AppMap
151
164
  def method_hook(cls, method_names, labels)
152
165
  MethodHook.new(cls, method_names, labels)
153
166
  end
167
+
168
+ def declare_hook(hook_decl)
169
+ hook_decl = YAML.load(hook_decl) if hook_decl.is_a?(String)
170
+
171
+ methods_decl = hook_decl['methods'] || hook_decl['method']
172
+ methods_decl = Array(methods_decl) unless methods_decl.is_a?(Hash)
173
+ labels_decl = Array(hook_decl['labels'] || hook_decl['label'])
174
+
175
+ methods = methods_decl.map do |name|
176
+ class_name, method_name, static = name.include?('.') ? name.split('.', 2) + [ true ] : name.split('#', 2) + [ false ]
177
+ method_hook class_name, [ method_name ], labels_decl
178
+ end
179
+
180
+ require_name = hook_decl['require_name']
181
+ gem_name = hook_decl['gem']
182
+ path = hook_decl['path']
183
+ builtin = hook_decl['builtin']
184
+
185
+ options = {
186
+ builtin: builtin,
187
+ gem: gem_name,
188
+ path: path,
189
+ require_name: require_name || gem_name || path,
190
+ force: hook_decl['force']
191
+ }.compact
192
+
193
+ handler_class = hook_decl['handler_class']
194
+ options[:handler_class] = Util::class_from_string(handler_class) if handler_class
195
+
196
+ package_hooks(methods, **options)
197
+ end
198
+
199
+ def declare_hook_deprecated(hook_decl)
200
+ function_name = hook_decl['name']
201
+ package, cls, functions = []
202
+ if function_name
203
+ package, cls, _, function = Util.parse_function_name(function_name)
204
+ functions = Array(function)
205
+ else
206
+ package = hook_decl['package']
207
+ cls = hook_decl['class']
208
+ functions = hook_decl['function'] || hook_decl['functions']
209
+ raise %q(AppMap config 'function' element should specify 'package', 'class' and 'function' or 'functions') unless package && cls && functions
210
+ end
211
+
212
+ functions = Array(functions).map(&:to_sym)
213
+ labels = hook_decl['label'] || hook_decl['labels']
214
+ req = hook_decl['require']
215
+ builtin = hook_decl['builtin']
216
+
217
+ package_options = {}
218
+ package_options[:labels] = Array(labels).map(&:to_s) if labels if labels
219
+ package_options[:require_name] = req
220
+ package_options[:require_name] ||= package if builtin
221
+ tm = TargetMethods.new(functions, Package.build_from_path(package, **package_options))
222
+ ClassTargetMethods.new(cls, tm)
223
+ end
224
+
225
+ def builtin_hooks_path
226
+ [ [ __dir__, 'builtin_hooks' ].join('/') ] + ( ENV['APPMAP_BUILTIN_HOOKS_PATH'] || '').split(/[;:]/)
227
+ end
228
+
229
+ def gem_hooks_path
230
+ [ [ __dir__, 'gem_hooks' ].join('/') ] + ( ENV['APPMAP_GEM_HOOKS_PATH'] || '').split(/[;:]/)
231
+ end
232
+
233
+ def load_hooks
234
+ loader = lambda do |dir, &block|
235
+ basename = dir.split('/').compact.join('/')
236
+ [].tap do |hooks|
237
+ Dir.glob(Pathname.new(dir).join('**').join('*.yml').to_s).each do |yaml_file|
238
+ path = yaml_file[basename.length + 1...-4]
239
+ YAML.load(File.read(yaml_file)).map do |config|
240
+ block.call path, config
241
+ config
242
+ end.each do |config|
243
+ hooks << declare_hook(config)
244
+ end
245
+ end
246
+ end.compact
247
+ end
248
+
249
+ builtin_hooks = builtin_hooks_path.map do |path|
250
+ loader.(path) do |path, config|
251
+ config['path'] = path
252
+ config['builtin'] = true
253
+ end
254
+ end
255
+
256
+ gem_hooks = gem_hooks_path.map do |path|
257
+ loader.(path) do |path, config|
258
+ config['gem'] = path
259
+ config['builtin'] = false
260
+ end
261
+ end
262
+
263
+ (builtin_hooks + gem_hooks).flatten
264
+ end
154
265
  end
155
266
 
156
- # Hook well-known functions. When a function configured here is available in the bundle, it will be hooked with the
157
- # predefined labels specified here. If any of these hooks are not desired, they can be disabled in the +exclude+ section
158
- # of appmap.yml.
159
- METHOD_HOOKS = [
160
- package_hooks('actionview',
161
- [
162
- method_hook('ActionView::Renderer', :render, %w[mvc.view]),
163
- method_hook('ActionView::TemplateRenderer', :render, %w[mvc.view]),
164
- method_hook('ActionView::PartialRenderer', :render, %w[mvc.view])
165
- ],
166
- handler_class: AppMap::Handler::Rails::Template::RenderHandler,
167
- package_name: 'action_view'
168
- ),
169
- package_hooks('actionview',
170
- [
171
- method_hook('ActionView::Resolver', %i[find_all find_all_anywhere], %w[mvc.template.resolver])
172
- ],
173
- handler_class: AppMap::Handler::Rails::Template::ResolverHandler,
174
- package_name: 'action_view'
175
- ),
176
- package_hooks('actionpack',
177
- [
178
- method_hook('ActionDispatch::Request::Session', %i[[] dig values fetch], %w[http.session.read]),
179
- method_hook('ActionDispatch::Request::Session', %i[destroy []= clear update delete merge], %w[http.session.write]),
180
- method_hook('ActionDispatch::Cookies::CookieJar', %i[[] fetch], %w[http.session.read]),
181
- method_hook('ActionDispatch::Cookies::CookieJar', %i[[]= clear update delete recycle], %w[http.session.write]),
182
- method_hook('ActionDispatch::Cookies::EncryptedCookieJar', %i[[]= clear update delete recycle], %w[http.cookie crypto.encrypt])
183
- ],
184
- package_name: 'action_dispatch'
185
- ),
186
- package_hooks('cancancan',
187
- [
188
- method_hook('CanCan::ControllerAdditions', %i[authorize! can? cannot?], %w[security.authorization]),
189
- method_hook('CanCan::Ability', %i[authorize?], %w[security.authorization])
190
- ]
191
- ),
192
- package_hooks('actionpack',
193
- [
194
- method_hook('ActionController::Instrumentation', %i[process_action send_file send_data redirect_to], %w[mvc.controller])
195
- ],
196
- package_name: 'action_controller'
197
- )
198
- ].flatten.freeze
199
-
200
- OPENSSL_PACKAGES = ->(labels) { Package.build_from_path('openssl', package_name: 'openssl', labels: labels) }
201
-
202
- # Hook functions which are builtin to Ruby. Because they are builtins, they may be loaded before appmap.
203
- # Therefore, we can't rely on TracePoint to report the loading of this code.
204
- BUILTIN_HOOKS = {
205
- 'OpenSSL::PKey::PKey' => TargetMethods.new(:sign, OPENSSL_PACKAGES.(%w[crypto.pkey])),
206
- 'OpenSSL::X509::Request' => TargetMethods.new(%i[sign verify], OPENSSL_PACKAGES.(%w[crypto.x509])),
207
- 'OpenSSL::PKCS5' => TargetMethods.new(%i[pbkdf2_hmac_sha1 pbkdf2_hmac], OPENSSL_PACKAGES.(%w[crypto.pkcs5])),
208
- 'OpenSSL::Cipher' => [
209
- TargetMethods.new(%i[encrypt], OPENSSL_PACKAGES.(%w[crypto.encrypt])),
210
- TargetMethods.new(%i[decrypt], OPENSSL_PACKAGES.(%w[crypto.decrypt]))
211
- ],
212
- 'ActiveSupport::Callbacks::CallbackSequence' => [
213
- TargetMethods.new(:invoke_before, Package.build_from_gem('activesupport', force: true, package_name: 'active_support', labels: %w[mvc.before_action])),
214
- TargetMethods.new(:invoke_after, Package.build_from_gem('activesupport', force: true, package_name: 'active_support', labels: %w[mvc.after_action])),
215
- ],
216
- 'ActiveSupport::SecurityUtils' => TargetMethods.new(:secure_compare, Package.build_from_gem('activesupport', force: true, package_name: 'active_support/security_utils', labels: %w[crypto.secure_compare])),
217
- 'OpenSSL::X509::Certificate' => TargetMethods.new(:sign, OPENSSL_PACKAGES.(%w[crypto.x509])),
218
- 'Net::HTTP' => TargetMethods.new(:request, Package.build_from_path('net/http', package_name: 'net/http', labels: %w[protocol.http]).tap do |package|
219
- package.handler_class = AppMap::Handler::NetHTTP
220
- end),
221
- 'Net::SMTP' => TargetMethods.new(:send, Package.build_from_path('net/smtp', package_name: 'net/smtp', labels: %w[protocol.email.smtp])),
222
- 'Net::POP3' => TargetMethods.new(:mails, Package.build_from_path('net/pop3', package_name: 'net/pop', labels: %w[protocol.email.pop])),
223
- # This is happening: Method send_command not found on Net::IMAP
224
- # 'Net::IMAP' => TargetMethods.new(:send_command, Package.build_from_path('net/imap', package_name: 'net/imap', labels: %w[protocol.email.imap])),
225
- # 'Marshal' => TargetMethods.new(%i[dump load], Package.build_from_path('marshal', labels: %w[format.marshal])),
226
- 'Psych' => [
227
- TargetMethods.new(%i[load load_stream parse parse_stream], Package.build_from_path('yaml', package_name: 'psych', labels: %w[format.yaml.parse])),
228
- TargetMethods.new(%i[dump dump_stream], Package.build_from_path('yaml', package_name: 'psych', labels: %w[format.yaml.generate])),
229
- ],
230
- 'JSON::Ext::Parser' => TargetMethods.new(:parse, Package.build_from_path('json', package_name: 'json', labels: %w[format.json.parse])),
231
- 'JSON::Ext::Generator::State' => TargetMethods.new(:generate, Package.build_from_path('json', package_name: 'json', labels: %w[format.json.generate])),
232
- }.freeze
233
-
234
- attr_reader :name, :appmap_dir, :packages, :exclude, :swagger_config, :depends_config, :hooked_methods, :builtin_hooks
267
+ attr_reader :name, :appmap_dir, :packages, :exclude, :swagger_config, :depends_config, :gem_hooks, :builtin_hooks
235
268
 
236
269
  def initialize(name,
237
270
  packages: [],
@@ -246,31 +279,22 @@ module AppMap
246
279
  @depends_config = depends_config
247
280
  @hook_paths = Set.new(packages.map(&:path))
248
281
  @exclude = exclude
249
- @builtin_hooks = BUILTIN_HOOKS.dup
250
282
  @functions = functions
251
283
 
252
- @hooked_methods = METHOD_HOOKS.each_with_object(Hash.new { |h,k| h[k] = [] }) do |cls_target_methods, hooked_methods|
253
- hooked_methods[cls_target_methods.cls] << cls_target_methods.target_methods
254
- end
255
-
256
- functions.each do |func|
257
- package_options = {}
258
- package_options[:labels] = func.labels if func.labels
259
- package_options[:package_name] = func.package_name
260
- package_options[:package_name] ||= func.package if func.builtin
261
- hook = TargetMethods.new(func.function_names, Package.build_from_path(func.package, **package_options))
262
- if func.builtin
263
- @builtin_hooks[func.cls] ||= []
264
- @builtin_hooks[func.cls] << hook
284
+ @builtin_hooks = Hash.new { |h,k| h[k] = [] }
285
+ @gem_hooks = Hash.new { |h,k| h[k] = [] }
286
+
287
+ (functions + self.class.load_hooks).each_with_object(Hash.new { |h,k| h[k] = [] }) do |cls_target_methods, gem_hooks|
288
+ hooks = if cls_target_methods.target_methods.package.builtin
289
+ @builtin_hooks
265
290
  else
266
- @hooked_methods[func.cls] << hook
291
+ @gem_hooks
267
292
  end
293
+ hooks[cls_target_methods.cls] << cls_target_methods.target_methods
268
294
  end
269
295
 
270
- @hooked_methods.each_value do |hooks|
271
- Array(hooks).each do |hook|
272
- @hook_paths << hook.package.path
273
- end
296
+ @gem_hooks.each_value do |hooks|
297
+ @hook_paths += Array(hooks).map { |hook| hook.package.path }.compact
274
298
  end
275
299
  end
276
300
 
@@ -334,23 +358,14 @@ module AppMap
334
358
  }.compact
335
359
 
336
360
  if config_data['functions']
337
- config_params[:functions] = config_data['functions'].map do |function_data|
338
- function_name = function_data['name']
339
- package, cls, functions = []
340
- if function_name
341
- package, cls, _, function = Util.parse_function_name(function_name)
342
- functions = Array(function)
361
+ config_params[:functions] = config_data['functions'].map do |hook_decl|
362
+ if hook_decl['name'] || hook_decl['package']
363
+ declare_hook_deprecated(hook_decl)
343
364
  else
344
- package = function_data['package']
345
- cls = function_data['class']
346
- functions = function_data['function'] || function_data['functions']
347
- raise %q(AppMap config 'function' element should specify 'package', 'class' and 'function' or 'functions') unless package && cls && functions
365
+ # Support the same syntax within the 'functions' that's used for externalized
366
+ # hook config.
367
+ declare_hook(hook_decl)
348
368
  end
349
-
350
- functions = Array(functions).map(&:to_sym)
351
- labels = function_data['label'] || function_data['labels']
352
- labels = Array(labels).map(&:to_s) if labels
353
- Function.new(package, cls, labels, functions, function_data['builtin'], function_data['require'])
354
369
  end
355
370
  end
356
371
 
@@ -366,7 +381,11 @@ module AppMap
366
381
  shallow = package['shallow']
367
382
  # shallow is true by default for gems
368
383
  shallow = true if shallow.nil?
369
- Package.build_from_gem(gem, package_name: package['package'], exclude: package['exclude'] || [], shallow: shallow)
384
+
385
+ require_name = \
386
+ package['package'] || #deprecated
387
+ package['require_name']
388
+ Package.build_from_gem(gem, require_name: require_name, exclude: package['exclude'] || [], shallow: shallow)
370
389
  else
371
390
  Package.build_from_path(path, exclude: package['exclude'] || [], shallow: package['shallow'])
372
391
  end
@@ -417,8 +436,8 @@ module AppMap
417
436
 
418
437
  # Hook a method which is specified by class and method name.
419
438
  def package_for_code_object
420
- Array(config.hooked_methods[cls.name])
421
- .compact
439
+ class_name = cls.to_s.index('#<Class:') == 0 ? cls.to_s['#<Class:'.length...-1] : cls.name
440
+ Array(config.gem_hooks[class_name])
422
441
  .find { |hook| hook.include_method?(method.name) }
423
442
  &.package
424
443
  end
data/lib/appmap/event.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'weakref'
4
+
3
5
  module AppMap
4
6
  module Event
5
7
  @@id_counter = 0
@@ -0,0 +1,40 @@
1
+ - methods:
2
+ - ActionDispatch::Request::Session#[]
3
+ - ActionDispatch::Request::Session#dig
4
+ - ActionDispatch::Request::Session#values
5
+ - ActionDispatch::Request::Session#fetch
6
+ - ActionDispatch::Cookies::CookieJar#[]
7
+ - ActionDispatch::Cookies::CookieJar#fetch
8
+ label: http.session.read
9
+ require_name: action_dispatch
10
+ - methods:
11
+ - ActionDispatch::Request::Session#destroy
12
+ - ActionDispatch::Request::Session#[]=
13
+ - ActionDispatch::Request::Session#clear
14
+ - ActionDispatch::Request::Session#update
15
+ - ActionDispatch::Request::Session#delete
16
+ - ActionDispatch::Request::Session#merge
17
+ - ActionDispatch::Cookies::CookieJar#[]=
18
+ - ActionDispatch::Cookies::CookieJar#clear
19
+ - ActionDispatch::Cookies::CookieJar#update
20
+ - ActionDispatch::Cookies::CookieJar#delete
21
+ - ActionDispatch::Cookies::CookieJar#recycle!
22
+ label: http.session.write
23
+ require_name: action_dispatch
24
+ - methods:
25
+ - ActionDispatch::Cookies::EncryptedCookieJar#[]=
26
+ - ActionDispatch::Cookies::EncryptedCookieJar#clear
27
+ - ActionDispatch::Cookies::EncryptedCookieJar#update
28
+ - ActionDispatch::Cookies::EncryptedCookieJar#delete
29
+ - ActionDispatch::Cookies::EncryptedCookieJar#recycle
30
+ labels:
31
+ - http.cookie
32
+ - crypto.encrypt
33
+ require_name: action_dispatch
34
+ - methods:
35
+ - ActionController::Instrumentation#process_action
36
+ - ActionController::Instrumentation#send_file
37
+ - ActionController::Instrumentation#send_data
38
+ - ActionController::Instrumentation#redirect_to
39
+ label: mvc.controller
40
+ require_name: action_controller
@@ -0,0 +1,13 @@
1
+ - methods:
2
+ - ActionView::Renderer#render
3
+ - ActionView::TemplateRenderer#render
4
+ - ActionView::PartialRenderer#render
5
+ label: mvc.view
6
+ handler_class: AppMap::Handler::Rails::Template::RenderHandler
7
+ require_name: action_view
8
+ - methods:
9
+ - ActionView::Resolver#find_all
10
+ - ActionView::Resolver#find_all_anywhere
11
+ label: mvc.template.resolver
12
+ handler_class: AppMap::Handler::Rails::Template::ResolverHandler
13
+ require_name: action_view
@@ -0,0 +1,12 @@
1
+ - method: ActiveSupport::Callbacks::CallbackSequence#invoke_before
2
+ label: mvc.before_action
3
+ require_name: active_support
4
+ force: true
5
+ - method: ActiveSupport::Callbacks::CallbackSequence#invoke_after
6
+ label: mvc.after_action
7
+ require_name: active_support
8
+ force: true
9
+ - method: ActiveSupport::SecurityUtils#secure_compare
10
+ label: crypto.secure_compare
11
+ require_name: active_support/security_utils
12
+ force: true
@@ -0,0 +1,6 @@
1
+ - methods:
2
+ - CanCan::ControllerAdditions#authorize!
3
+ - CanCan::ControllerAdditions#can?
4
+ - CanCan::ControllerAdditions#cannot?
5
+ - CanCan::Ability#authorize?
6
+ label: security.authorization
data/lib/appmap/hook.rb CHANGED
@@ -15,10 +15,23 @@ module AppMap
15
15
  @method_arity = ::Method.instance_method(:arity)
16
16
 
17
17
  class << self
18
- def lock_builtins
19
- return if @builtins_hooked
18
+ def hook_builtins?
19
+ Mutex.new.synchronize do
20
+ @hook_builtins = true if @hook_builtins.nil?
20
21
 
21
- @builtins_hooked = true
22
+ return false unless @hook_builtins
23
+
24
+ @hook_builtins = false
25
+ true
26
+ end
27
+ end
28
+
29
+ def already_hooked?(method)
30
+ # After a method is defined, the statement "module_function <the-method>" can convert that method
31
+ # into a module (class) method. The method is hooked first when it's defined, then AppMap will attempt to
32
+ # hook it again when it's redefined as a module method. So we check the method source location - if it's
33
+ # part of the AppMap source tree, we ignore it.
34
+ method.source_location && method.source_location[0].index(__dir__) == 0
22
35
  end
23
36
 
24
37
  # Return the class, separator ('.' or '#'), and method name for
@@ -79,42 +92,43 @@ module AppMap
79
92
  # hook_builtins builds hooks for code that is built in to the Ruby standard library.
80
93
  # No TracePoint events are emitted for builtins, so a separate hooking mechanism is needed.
81
94
  def hook_builtins
82
- return unless self.class.lock_builtins
95
+ return unless self.class.hook_builtins?
83
96
 
84
- class_from_string = lambda do |fq_class|
85
- fq_class.split('::').inject(Object) do |mod, class_name|
86
- mod.const_get(class_name)
87
- end
88
- end
97
+ hook_loaded_code = lambda do |hooks_by_class, builtin|
98
+ hooks_by_class.each do |class_name, hooks|
99
+ Array(hooks).each do |hook|
100
+ require hook.package.require_name if builtin && hook.package.require_name && hook.package.require_name != 'ruby'
89
101
 
90
- config.builtin_hooks.each do |class_name, hooks|
91
- Array(hooks).each do |hook|
92
- require hook.package.package_name if hook.package.package_name && hook.package.package_name != 'ruby'
93
- Array(hook.method_names).each do |method_name|
94
- method_name = method_name.to_sym
95
- base_cls = class_from_string.(class_name)
102
+ Array(hook.method_names).each do |method_name|
103
+ method_name = method_name.to_sym
104
+ base_cls = Util::class_from_string(class_name, must: false)
105
+ next unless base_cls
96
106
 
97
- hook_method = lambda do |entry|
98
- cls, method = entry
99
- return false if config.never_hook?(cls, method)
107
+ hook_method = lambda do |entry|
108
+ cls, method = entry
109
+ return false if config.never_hook?(cls, method)
100
110
 
101
- Hook::Method.new(hook.package, cls, method).activate
102
- end
111
+ Hook::Method.new(hook.package, cls, method).activate
112
+ end
103
113
 
104
- methods = []
105
- methods << [ base_cls, base_cls.public_instance_method(method_name) ] rescue nil
106
- if base_cls.respond_to?(:singleton_class)
107
- methods << [ base_cls.singleton_class, base_cls.singleton_class.public_instance_method(method_name) ] rescue nil
108
- end
109
- methods.compact!
110
- if methods.empty?
111
- warn "Method #{method_name} not found on #{base_cls.name}"
112
- else
113
- methods.each(&hook_method)
114
+ methods = []
115
+ methods << [ base_cls, base_cls.public_instance_method(method_name) ] rescue nil
116
+ if base_cls.respond_to?(:singleton_class)
117
+ methods << [ base_cls.singleton_class, base_cls.singleton_class.public_instance_method(method_name) ] rescue nil
118
+ end
119
+ methods.compact!
120
+ if methods.empty?
121
+ warn "Method #{method_name} not found on #{base_cls.name}" if LOG
122
+ else
123
+ methods.each(&hook_method)
124
+ end
114
125
  end
115
126
  end
116
127
  end
117
128
  end
129
+
130
+ hook_loaded_code.(config.builtin_hooks, true)
131
+ hook_loaded_code.(config.gem_hooks, false)
118
132
  end
119
133
 
120
134
  protected
@@ -165,6 +179,8 @@ module AppMap
165
179
  next
166
180
  end
167
181
 
182
+ next if self.class.already_hooked?(method)
183
+
168
184
  warn "AppMap: Examining #{hook_cls} #{method.name}" if LOG
169
185
 
170
186
  disasm = RubyVM::InstructionSequence.disasm(method)
data/lib/appmap/util.rb CHANGED
@@ -21,6 +21,14 @@ module AppMap
21
21
  WHITE = "\e[37m"
22
22
 
23
23
  class << self
24
+ def class_from_string(fq_class, must: true)
25
+ fq_class.split('::').inject(Object) do |mod, class_name|
26
+ mod.const_get(class_name)
27
+ end
28
+ rescue NameError
29
+ raise if must
30
+ end
31
+
24
32
  def parse_function_name(name)
25
33
  package_tokens = name.split('/')
26
34
 
@@ -3,7 +3,7 @@
3
3
  module AppMap
4
4
  URL = 'https://github.com/applandinc/appmap-ruby'
5
5
 
6
- VERSION = '0.67.1'
6
+ VERSION = '0.68.0'
7
7
 
8
8
  APPMAP_FORMAT_VERSION = '1.5.1'
9
9
 
data/spec/config_spec.rb CHANGED
@@ -4,58 +4,7 @@ require 'rails_spec_helper'
4
4
  require 'appmap/config'
5
5
 
6
6
  describe AppMap::Config, docker: false do
7
- it 'loads from a Hash' do
8
- config_data = {
9
- exclude: [],
10
- name: 'test',
11
- packages: [
12
- {
13
- path: 'path-1'
14
- },
15
- {
16
- path: 'path-2',
17
- exclude: [ 'exclude-1' ]
18
- }
19
- ],
20
- functions: [
21
- {
22
- package: 'pkg',
23
- class: 'cls',
24
- function: 'fn',
25
- label: 'lbl'
26
- }
27
- ]
28
- }.deep_stringify_keys!
29
- config = AppMap::Config.load(config_data)
30
-
31
- config_expectation = {
32
- exclude: [],
33
- name: 'test',
34
- packages: [
35
- {
36
- path: 'path-1',
37
- handler_class: 'AppMap::Handler::Function'
38
- },
39
- {
40
- path: 'path-2',
41
- handler_class: 'AppMap::Handler::Function',
42
- exclude: [ 'exclude-1' ]
43
- }
44
- ],
45
- functions: [
46
- {
47
- package: 'pkg',
48
- class: 'cls',
49
- functions: [ :fn ],
50
- labels: ['lbl']
51
- }
52
- ]
53
- }.deep_stringify_keys!
54
-
55
- expect(config.to_h.deep_stringify_keys!).to eq(config_expectation)
56
- end
57
-
58
- it 'interprets a function in canonical name format' do
7
+ it 'loads as expected' do
59
8
  config_data = {
60
9
  name: 'test',
61
10
  packages: [],
@@ -67,20 +16,212 @@ describe AppMap::Config, docker: false do
67
16
  }.deep_stringify_keys!
68
17
  config = AppMap::Config.load(config_data)
69
18
 
70
- config_expectation = {
71
- exclude: [],
72
- name: 'test',
73
- packages: [],
74
- functions: [
19
+ expect(JSON.parse(JSON.generate(config.as_json))).to eq(JSON.parse(<<~FIXTURE))
20
+ {
21
+ "name": "test",
22
+ "appmap_dir": "tmp/appmap",
23
+ "packages": [
24
+ ],
25
+ "swagger_config": {
26
+ "project_name": null,
27
+ "project_version": "1.0",
28
+ "output_dir": "swagger",
29
+ "description": "Generate Swagger from AppMaps"
30
+ },
31
+ "depends_config": {
32
+ "base_dir": null,
33
+ "base_branches": [
34
+ "remotes/origin/main",
35
+ "remotes/origin/master"
36
+ ],
37
+ "test_file_patterns": [
38
+ "spec/**/*_spec.rb",
39
+ "test/**/*_test.rb"
40
+ ],
41
+ "dependent_tasks": [
42
+ "swagger"
43
+ ],
44
+ "description": "Bring AppMaps up to date with local file modifications, and updated derived data such as Swagger files",
45
+ "rspec_environment_method": "AppMap::Depends.test_env",
46
+ "minitest_environment_method": "AppMap::Depends.test_env",
47
+ "rspec_select_tests_method": "AppMap::Depends.select_rspec_tests",
48
+ "minitest_select_tests_method": "AppMap::Depends.select_minitest_tests",
49
+ "rspec_test_command_method": "AppMap::Depends.rspec_test_command",
50
+ "minitest_test_command_method": "AppMap::Depends.minitest_test_command"
51
+ },
52
+ "hook_paths": [
53
+ "pkg",
54
+ "#{Gem.loaded_specs['activesupport'].gem_dir}"
55
+ ],
56
+ "exclude": [
57
+ ],
58
+ "functions": [
75
59
  {
76
- package: 'pkg',
77
- class: 'cls',
78
- functions: [ :fn ],
60
+ "cls": "cls",
61
+ "target_methods": {
62
+ "package": "pkg",
63
+ "method_names": [
64
+ "fn"
65
+ ]
66
+ }
79
67
  }
80
- ]
81
- }.deep_stringify_keys!
82
-
83
- expect(config.to_h.deep_stringify_keys!).to eq(config_expectation)
68
+ ],
69
+ "builtin_hooks": {
70
+ "JSON::Ext::Parser": [
71
+ {
72
+ "package": "json",
73
+ "method_names": [
74
+ "parse"
75
+ ]
76
+ }
77
+ ],
78
+ "JSON::Ext::Generator::State": [
79
+ {
80
+ "package": "json",
81
+ "method_names": [
82
+ "generate"
83
+ ]
84
+ }
85
+ ],
86
+ "Net::HTTP": [
87
+ {
88
+ "package": "net/http",
89
+ "method_names": [
90
+ "request"
91
+ ]
92
+ }
93
+ ],
94
+ "OpenSSL::PKey::PKey": [
95
+ {
96
+ "package": "openssl",
97
+ "method_names": [
98
+ "sign"
99
+ ]
100
+ }
101
+ ],
102
+ "OpenSSL::X509::Request": [
103
+ {
104
+ "package": "openssl",
105
+ "method_names": [
106
+ "sign"
107
+ ]
108
+ },
109
+ {
110
+ "package": "openssl",
111
+ "method_names": [
112
+ "verify"
113
+ ]
114
+ }
115
+ ],
116
+ "OpenSSL::X509::Certificate": [
117
+ {
118
+ "package": "openssl",
119
+ "method_names": [
120
+ "sign"
121
+ ]
122
+ }
123
+ ],
124
+ "OpenSSL::PKCS5": [
125
+ {
126
+ "package": "openssl",
127
+ "method_names": [
128
+ "pbkdf2_hmac"
129
+ ]
130
+ },
131
+ {
132
+ "package": "openssl",
133
+ "method_names": [
134
+ "pbkdf2_hmac_sha1"
135
+ ]
136
+ }
137
+ ],
138
+ "OpenSSL::Cipher": [
139
+ {
140
+ "package": "openssl",
141
+ "method_names": [
142
+ "encrypt"
143
+ ]
144
+ },
145
+ {
146
+ "package": "openssl",
147
+ "method_names": [
148
+ "decrypt"
149
+ ]
150
+ }
151
+ ],
152
+ "Psych": [
153
+ {
154
+ "package": "yaml",
155
+ "method_names": [
156
+ "load"
157
+ ]
158
+ },
159
+ {
160
+ "package": "yaml",
161
+ "method_names": [
162
+ "load_stream"
163
+ ]
164
+ },
165
+ {
166
+ "package": "yaml",
167
+ "method_names": [
168
+ "parse"
169
+ ]
170
+ },
171
+ {
172
+ "package": "yaml",
173
+ "method_names": [
174
+ "parse_stream"
175
+ ]
176
+ },
177
+ {
178
+ "package": "yaml",
179
+ "method_names": [
180
+ "dump"
181
+ ]
182
+ },
183
+ {
184
+ "package": "yaml",
185
+ "method_names": [
186
+ "dump_stream"
187
+ ]
188
+ }
189
+ ]
190
+ },
191
+ "gem_hooks": {
192
+ "cls": [
193
+ {
194
+ "package": "pkg",
195
+ "method_names": [
196
+ "fn"
197
+ ]
198
+ }
199
+ ],
200
+ "ActiveSupport::Callbacks::CallbackSequence": [
201
+ {
202
+ "package": "activesupport",
203
+ "method_names": [
204
+ "invoke_before"
205
+ ]
206
+ },
207
+ {
208
+ "package": "activesupport",
209
+ "method_names": [
210
+ "invoke_after"
211
+ ]
212
+ }
213
+ ],
214
+ "ActiveSupport::SecurityUtils": [
215
+ {
216
+ "package": "activesupport",
217
+ "method_names": [
218
+ "secure_compare"
219
+ ]
220
+ }
221
+ ]
222
+ }
223
+ }
224
+ FIXTURE
84
225
  end
85
226
 
86
227
  context do
@@ -94,7 +235,8 @@ describe AppMap::Config, docker: false do
94
235
  expect(config.to_h).to eq(YAML.load(<<~CONFIG))
95
236
  :name: appmap-ruby
96
237
  :packages:
97
- - :path: lib
238
+ - :name: lib
239
+ :path: lib
98
240
  :handler_class: AppMap::Handler::Function
99
241
  :shallow: false
100
242
  :functions: []
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: appmap
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.67.1
4
+ version: 0.68.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kevin Gilpin
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-11-02 00:00:00.000000000 Z
11
+ date: 2021-11-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -3405,6 +3405,10 @@ files:
3405
3405
  - ext/appmap/extconf.rb
3406
3406
  - lib/appmap.rb
3407
3407
  - lib/appmap/agent.rb
3408
+ - lib/appmap/builtin_hooks/json.yml
3409
+ - lib/appmap/builtin_hooks/net/http.yml
3410
+ - lib/appmap/builtin_hooks/openssl.yml
3411
+ - lib/appmap/builtin_hooks/yaml.yml
3408
3412
  - lib/appmap/class_map.rb
3409
3413
  - lib/appmap/command/agent_setup/init.rb
3410
3414
  - lib/appmap/command/agent_setup/status.rb
@@ -3423,6 +3427,10 @@ files:
3423
3427
  - lib/appmap/depends/test_runner.rb
3424
3428
  - lib/appmap/depends/util.rb
3425
3429
  - lib/appmap/event.rb
3430
+ - lib/appmap/gem_hooks/actionpack.yml
3431
+ - lib/appmap/gem_hooks/actionview.yml
3432
+ - lib/appmap/gem_hooks/activesupport.yml
3433
+ - lib/appmap/gem_hooks/cancancan.yml
3426
3434
  - lib/appmap/handler/function.rb
3427
3435
  - lib/appmap/handler/net_http.rb
3428
3436
  - lib/appmap/handler/rails/request_handler.rb