appmap 0.67.1 → 0.68.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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