Skip to content

Commit 8080222

Browse files
committed
feat: Externalize config of hooks
1 parent 2f94f80 commit 8080222

File tree

12 files changed

+244
-132
lines changed

12 files changed

+244
-132
lines changed

lib/appmap/builtin_hooks/json.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

lib/appmap/builtin_hooks/net/http.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
- method: Net::HTTP#request
2+
label: protocol.http
3+
handler_class: AppMap::Handler::NetHTTP

lib/appmap/builtin_hooks/openssl.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

lib/appmap/builtin_hooks/yaml.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

lib/appmap/config.rb

Lines changed: 82 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,20 @@
1111

1212
module AppMap
1313
class Config
14-
# Specifies a code +path+ to be mapped.
14+
# Specifies a logical code package be mapped.
15+
# This can be a project source folder, a Gem, or a builtin.
1516
#
1617
# Options:
1718
#
19+
# * +path+ indicates a relative path to a code folder.
1820
# * +gem+ may indicate a gem name that "owns" the path
1921
# * +require_name+ can be used to make sure that the code is required so that it can be loaded. This is generally used with
2022
# builtins, or when the path to be required is not automatically required when bundler requires the gem.
2123
# * +exclude+ can be used used to exclude sub-paths. Generally not used with +gem+.
2224
# * +labels+ is used to apply labels to matching code. This is really only useful when the package will be applied to
2325
# specific functions, via TargetMethods.
2426
# * +shallow+ indicates shallow mapping, in which only the entrypoint to a gem is recorded.
25-
Package = Struct.new(:path, :gem, :require_name, :exclude, :labels, :shallow) do
27+
Package = Struct.new(:name, :path, :gem, :require_name, :exclude, :labels, :shallow) do
2628
# This is for internal use only.
2729
private_methods :gem
2830

@@ -45,7 +47,7 @@ class << self
4547
# Builds a package for a path, such as `app/models` in a Rails app. Generally corresponds to a `path:` entry
4648
# in appmap.yml. Also used for mapping specific methods via TargetMethods.
4749
def build_from_path(path, shallow: false, require_name: nil, exclude: [], labels: [])
48-
Package.new(path, nil, require_name, exclude, labels, shallow)
50+
Package.new(path, path, nil, require_name, exclude, labels, shallow)
4951
end
5052

5153
# Builds a package for gem. Generally corresponds to a `gem:` entry in appmap.yml. Also used when mapping
@@ -57,7 +59,7 @@ def build_from_gem(gem, shallow: true, require_name: nil, exclude: [], labels: [
5759
end
5860
path = gem_path(gem, optional)
5961
if path
60-
Package.new(path, gem, require_name, exclude, labels, shallow)
62+
Package.new(gem, path, gem, require_name, exclude, labels, shallow)
6163
else
6264
AppMap::Util.startup_message "#{gem} is not available in the bundle"
6365
end
@@ -75,19 +77,16 @@ def gem_path(gem, optional)
7577
end
7678
end
7779

78-
def name
79-
gem || path
80-
end
81-
8280
def to_h
8381
{
82+
name: name,
8483
path: path,
85-
require_name: require_name,
8684
gem: gem,
87-
handler_class: handler_class.name,
85+
require_name: require_name,
86+
handler_class: handler_class ? handler_class.name : nil,
8887
exclude: Util.blank?(exclude) ? nil : exclude,
8988
labels: Util.blank?(labels) ? nil : labels,
90-
shallow: shallow
89+
shallow: shallow.nil? ? nil : shallow,
9190
}.compact
9291
end
9392
end
@@ -97,12 +96,12 @@ class TargetMethods # :nodoc:
9796
attr_reader :method_names, :package
9897

9998
def initialize(method_names, package)
100-
@method_names = method_names
99+
@method_names = Array(method_names).map(&:to_sym)
101100
@package = package
102101
end
103102

104103
def include_method?(method_name)
105-
Array(method_names).include?(method_name)
104+
method_names.include?(method_name.to_sym)
106105
end
107106

108107
def to_h
@@ -139,9 +138,13 @@ def to_h
139138
private_constant :MethodHook
140139

141140
class << self
142-
def package_hooks(gem_name, methods, handler_class: nil, require_name: nil)
141+
def package_hooks(methods, path: nil, gem: nil, force: false, handler_class: nil, require_name: nil)
143142
Array(methods).map do |method|
144-
package = Package.build_from_gem(gem_name, require_name: require_name, labels: method.labels, shallow: false, optional: true)
143+
package = if gem
144+
Package.build_from_gem(gem, require_name: require_name, labels: method.labels, shallow: false, force: force, optional: true)
145+
elsif path
146+
Package.build_from_path(path, require_name: require_name, labels: method.labels, shallow: false)
147+
end
145148
next unless package
146149

147150
package.handler_class = handler_class if handler_class
@@ -152,85 +155,63 @@ def package_hooks(gem_name, methods, handler_class: nil, require_name: nil)
152155
def method_hook(cls, method_names, labels)
153156
MethodHook.new(cls, method_names, labels)
154157
end
155-
end
156158

157-
# Hook well-known functions. When a function configured here is available in the bundle, it will be hooked with the
158-
# predefined labels specified here. If any of these hooks are not desired, they can be disabled in the +exclude+ section
159-
# of appmap.yml.
160-
METHOD_HOOKS = [
161-
package_hooks('actionview',
162-
[
163-
method_hook('ActionView::Renderer', :render, %w[mvc.view]),
164-
method_hook('ActionView::TemplateRenderer', :render, %w[mvc.view]),
165-
method_hook('ActionView::PartialRenderer', :render, %w[mvc.view])
166-
],
167-
handler_class: AppMap::Handler::Rails::Template::RenderHandler,
168-
require_name: 'action_view'
169-
),
170-
package_hooks('actionview',
171-
[
172-
method_hook('ActionView::Resolver', %i[find_all find_all_anywhere], %w[mvc.template.resolver])
173-
],
174-
handler_class: AppMap::Handler::Rails::Template::ResolverHandler,
175-
require_name: 'action_view'
176-
),
177-
package_hooks('actionpack',
178-
[
179-
method_hook('ActionDispatch::Request::Session', %i[[] dig values fetch], %w[http.session.read]),
180-
method_hook('ActionDispatch::Request::Session', %i[destroy []= clear update delete merge], %w[http.session.write]),
181-
method_hook('ActionDispatch::Cookies::CookieJar', %i[[] fetch], %w[http.session.read]),
182-
method_hook('ActionDispatch::Cookies::CookieJar', %i[[]= clear update delete recycle], %w[http.session.write]),
183-
method_hook('ActionDispatch::Cookies::EncryptedCookieJar', %i[[]= clear update delete recycle], %w[http.cookie crypto.encrypt])
184-
],
185-
require_name: 'action_dispatch'
186-
),
187-
package_hooks('cancancan',
188-
[
189-
method_hook('CanCan::ControllerAdditions', %i[authorize! can? cannot?], %w[security.authorization]),
190-
method_hook('CanCan::Ability', %i[authorize?], %w[security.authorization])
191-
]
192-
),
193-
package_hooks('actionpack',
194-
[
195-
method_hook('ActionController::Instrumentation', %i[process_action send_file send_data redirect_to], %w[mvc.controller])
196-
],
197-
require_name: 'action_controller'
198-
)
199-
].flatten.freeze
200-
201-
OPENSSL_PACKAGES = ->(labels) { Package.build_from_path('openssl', require_name: 'openssl', labels: labels) }
202-
203-
# Hook functions which are builtin to Ruby. Because they are builtins, they may be loaded before appmap.
204-
# Therefore, we can't rely on TracePoint to report the loading of this code.
205-
BUILTIN_HOOKS = {
206-
'OpenSSL::PKey::PKey' => TargetMethods.new(:sign, OPENSSL_PACKAGES.(%w[crypto.pkey])),
207-
'OpenSSL::X509::Request' => TargetMethods.new(%i[sign verify], OPENSSL_PACKAGES.(%w[crypto.x509])),
208-
'OpenSSL::PKCS5' => TargetMethods.new(%i[pbkdf2_hmac_sha1 pbkdf2_hmac], OPENSSL_PACKAGES.(%w[crypto.pkcs5])),
209-
'OpenSSL::Cipher' => [
210-
TargetMethods.new(%i[encrypt], OPENSSL_PACKAGES.(%w[crypto.encrypt])),
211-
TargetMethods.new(%i[decrypt], OPENSSL_PACKAGES.(%w[crypto.decrypt]))
212-
],
213-
'ActiveSupport::Callbacks::CallbackSequence' => [
214-
TargetMethods.new(:invoke_before, Package.build_from_gem('activesupport', force: true, require_name: 'active_support', labels: %w[mvc.before_action])),
215-
TargetMethods.new(:invoke_after, Package.build_from_gem('activesupport', force: true, require_name: 'active_support', labels: %w[mvc.after_action])),
216-
],
217-
'ActiveSupport::SecurityUtils' => TargetMethods.new(:secure_compare, Package.build_from_gem('activesupport', force: true, require_name: 'active_support/security_utils', labels: %w[crypto.secure_compare])),
218-
'OpenSSL::X509::Certificate' => TargetMethods.new(:sign, OPENSSL_PACKAGES.(%w[crypto.x509])),
219-
'Net::HTTP' => TargetMethods.new(:request, Package.build_from_path('net/http', require_name: 'net/http', labels: %w[protocol.http]).tap do |package|
220-
package.handler_class = AppMap::Handler::NetHTTP
221-
end),
222-
'Net::SMTP' => TargetMethods.new(:send, Package.build_from_path('net/smtp', require_name: 'net/smtp', labels: %w[protocol.email.smtp])),
223-
'Net::POP3' => TargetMethods.new(:mails, Package.build_from_path('net/pop3', require_name: 'net/pop', labels: %w[protocol.email.pop])),
224-
# This is happening: Method send_command not found on Net::IMAP
225-
# 'Net::IMAP' => TargetMethods.new(:send_command, Package.build_from_path('net/imap', require_name: 'net/imap', labels: %w[protocol.email.imap])),
226-
# 'Marshal' => TargetMethods.new(%i[dump load], Package.build_from_path('marshal', labels: %w[format.marshal])),
227-
'Psych' => [
228-
TargetMethods.new(%i[load load_stream parse parse_stream], Package.build_from_path('yaml', require_name: 'psych', labels: %w[format.yaml.parse])),
229-
TargetMethods.new(%i[dump dump_stream], Package.build_from_path('yaml', require_name: 'psych', labels: %w[format.yaml.generate])),
230-
],
231-
'JSON::Ext::Parser' => TargetMethods.new(:parse, Package.build_from_path('json', require_name: 'json', labels: %w[format.json.parse])),
232-
'JSON::Ext::Generator::State' => TargetMethods.new(:generate, Package.build_from_path('json', require_name: 'json', labels: %w[format.json.generate])),
233-
}.freeze
159+
def declare_hook(hook_decl)
160+
hook_decl = YAML.load(hook_decl) if hook_decl.is_a?(String)
161+
162+
methods_decl = hook_decl['methods'] || hook_decl['method']
163+
methods_decl = Array(methods_decl) unless methods_decl.is_a?(Hash)
164+
labels_decl = Array(hook_decl['labels'] || hook_decl['label'])
165+
166+
methods = methods_decl.map do |name|
167+
class_name, method_name, static = name.include?('.') ? name.split('.', 2) + [ true ] : name.split('#', 2) + [ false ]
168+
method_hook class_name, [ method_name ], labels_decl
169+
end
170+
171+
require_name = hook_decl['require_name']
172+
gem_name = hook_decl['gem']
173+
path = hook_decl['path']
174+
175+
options = {
176+
gem: gem_name,
177+
path: path,
178+
require_name: require_name || gem_name || path,
179+
force: hook_decl['force']
180+
}.compact
181+
182+
handler_class = hook_decl['handler_class']
183+
options[:handler_class] = Util::class_from_string(handler_class) if handler_class
184+
185+
package_hooks(methods, **options)
186+
end
187+
188+
def load_builtin_hooks
189+
load_hooks('builtin_hooks') do |path, config|
190+
config['path'] = path
191+
end
192+
end
193+
194+
def load_gem_hooks
195+
load_hooks('gem_hooks') do |path, config|
196+
config['gem'] = path
197+
end
198+
end
199+
200+
def load_hooks(dir, &block)
201+
basedir = [ __dir__, dir ].join('/')
202+
[].tap do |hooks|
203+
Dir.glob("#{basedir}/**/*.yml").each do |yaml_file|
204+
path = yaml_file[basedir.length + 1...-4]
205+
YAML.load(File.read(yaml_file)).map do |config|
206+
yield path, config
207+
config
208+
end.each do |config|
209+
hooks << declare_hook(config)
210+
end
211+
end
212+
end.compact.flatten
213+
end
214+
end
234215

235216
attr_reader :name, :appmap_dir, :packages, :exclude, :swagger_config, :depends_config, :hooked_methods, :builtin_hooks
236217

@@ -247,10 +228,13 @@ def initialize(name,
247228
@depends_config = depends_config
248229
@hook_paths = Set.new(packages.map(&:path))
249230
@exclude = exclude
250-
@builtin_hooks = BUILTIN_HOOKS.dup
251231
@functions = functions
252232

253-
@hooked_methods = METHOD_HOOKS.each_with_object(Hash.new { |h,k| h[k] = [] }) do |cls_target_methods, hooked_methods|
233+
@builtin_hooks = self.class.load_builtin_hooks.each_with_object(Hash.new { |h,k| h[k] = [] }) do |cls_target_methods, hooked_methods|
234+
hooked_methods[cls_target_methods.cls] << cls_target_methods.target_methods
235+
end
236+
237+
@hooked_methods = self.class.load_gem_hooks.each_with_object(Hash.new { |h,k| h[k] = [] }) do |cls_target_methods, hooked_methods|
254238
hooked_methods[cls_target_methods.cls] << cls_target_methods.target_methods
255239
end
256240

@@ -269,9 +253,7 @@ def initialize(name,
269253
end
270254

271255
@hooked_methods.each_value do |hooks|
272-
Array(hooks).each do |hook|
273-
@hook_paths << hook.package.path
274-
end
256+
@hook_paths += Array(hooks).map { |hook| hook.package.path }.compact
275257
end
276258
end
277259

@@ -422,8 +404,8 @@ def package
422404

423405
# Hook a method which is specified by class and method name.
424406
def package_for_code_object
425-
Array(config.hooked_methods[cls.name])
426-
.compact
407+
class_name = cls.to_s.index('#<Class:') == 0 ? cls.to_s['#<Class:'.length...-1] : cls.name
408+
Array(config.hooked_methods[class_name])
427409
.find { |hook| hook.include_method?(method.name) }
428410
&.package
429411
end

lib/appmap/gem_hooks/actionpack.yml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

lib/appmap/gem_hooks/actionview.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

lib/appmap/gem_hooks/cancancan.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

0 commit comments

Comments
 (0)