Skip to content

Commit 4856483

Browse files
committed
feat: Support loading hook config via path env vars
APPMAP_BUILTIN_HOOKS_PATH APPMAP_GEM_HOOKS_PATH Path-like environment variables for loading hook config from individual YMAL files.
1 parent 8080222 commit 4856483

File tree

3 files changed

+295
-124
lines changed

3 files changed

+295
-124
lines changed

lib/appmap/config.rb

+89-57
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# frozen_string_literal: true
22

3+
require 'pathname'
34
require 'set'
45
require 'yaml'
56
require 'appmap/util'
@@ -24,7 +25,7 @@ class Config
2425
# * +labels+ is used to apply labels to matching code. This is really only useful when the package will be applied to
2526
# specific functions, via TargetMethods.
2627
# * +shallow+ indicates shallow mapping, in which only the entrypoint to a gem is recorded.
27-
Package = Struct.new(:name, :path, :gem, :require_name, :exclude, :labels, :shallow) do
28+
Package = Struct.new(:name, :path, :gem, :require_name, :exclude, :labels, :shallow, :builtin) do
2829
# This is for internal use only.
2930
private_methods :gem
3031

@@ -50,6 +51,10 @@ def build_from_path(path, shallow: false, require_name: nil, exclude: [], labels
5051
Package.new(path, path, nil, require_name, exclude, labels, shallow)
5152
end
5253

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)
56+
end
57+
5358
# Builds a package for gem. Generally corresponds to a `gem:` entry in appmap.yml. Also used when mapping
5459
# a builtin.
5560
def build_from_gem(gem, shallow: true, require_name: nil, exclude: [], labels: [], optional: false, force: false)
@@ -110,6 +115,8 @@ def to_h
110115
method_names: method_names
111116
}
112117
end
118+
119+
alias as_json to_h
113120
end
114121
private_constant :TargetMethods
115122

@@ -138,9 +145,11 @@ def to_h
138145
private_constant :MethodHook
139146

140147
class << self
141-
def package_hooks(methods, path: nil, gem: nil, force: false, handler_class: nil, require_name: nil)
148+
def package_hooks(methods, path: nil, gem: nil, force: false, builtin: false, handler_class: nil, require_name: nil)
142149
Array(methods).map do |method|
143-
package = if gem
150+
package = if builtin
151+
Package.build_from_builtin(path, require_name: require_name, labels: method.labels, shallow: false)
152+
elsif gem
144153
Package.build_from_gem(gem, require_name: require_name, labels: method.labels, shallow: false, force: force, optional: true)
145154
elsif path
146155
Package.build_from_path(path, require_name: require_name, labels: method.labels, shallow: false)
@@ -171,8 +180,10 @@ def declare_hook(hook_decl)
171180
require_name = hook_decl['require_name']
172181
gem_name = hook_decl['gem']
173182
path = hook_decl['path']
183+
builtin = hook_decl['builtin']
174184

175185
options = {
186+
builtin: builtin,
176187
gem: gem_name,
177188
path: path,
178189
require_name: require_name || gem_name || path,
@@ -185,35 +196,75 @@ def declare_hook(hook_decl)
185196
package_hooks(methods, **options)
186197
end
187198

188-
def load_builtin_hooks
189-
load_hooks('builtin_hooks') do |path, config|
190-
config['path'] = path
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
191210
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)
192223
end
193224

194-
def load_gem_hooks
195-
load_hooks('gem_hooks') do |path, config|
196-
config['gem'] = path
197-
end
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(/[;:]/)
198231
end
199232

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)
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
210245
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
211260
end
212-
end.compact.flatten
261+
end
262+
263+
(builtin_hooks + gem_hooks).flatten
213264
end
214265
end
215266

216-
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
217268

218269
def initialize(name,
219270
packages: [],
@@ -230,29 +281,19 @@ def initialize(name,
230281
@exclude = exclude
231282
@functions = functions
232283

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|
238-
hooked_methods[cls_target_methods.cls] << cls_target_methods.target_methods
239-
end
240-
241-
functions.each do |func|
242-
package_options = {}
243-
package_options[:labels] = func.labels if func.labels
244-
package_options[:require_name] = func.require_name
245-
package_options[:require_name] ||= func.package if func.builtin
246-
hook = TargetMethods.new(func.function_names, Package.build_from_path(func.package, **package_options))
247-
if func.builtin
248-
@builtin_hooks[func.cls] ||= []
249-
@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
250290
else
251-
@hooked_methods[func.cls] << hook
291+
@gem_hooks
252292
end
293+
hooks[cls_target_methods.cls] << cls_target_methods.target_methods
253294
end
254295

255-
@hooked_methods.each_value do |hooks|
296+
@gem_hooks.each_value do |hooks|
256297
@hook_paths += Array(hooks).map { |hook| hook.package.path }.compact
257298
end
258299
end
@@ -317,23 +358,14 @@ def load(config_data)
317358
}.compact
318359

319360
if config_data['functions']
320-
config_params[:functions] = config_data['functions'].map do |function_data|
321-
function_name = function_data['name']
322-
package, cls, functions = []
323-
if function_name
324-
package, cls, _, function = Util.parse_function_name(function_name)
325-
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)
326364
else
327-
package = function_data['package']
328-
cls = function_data['class']
329-
functions = function_data['function'] || function_data['functions']
330-
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)
331368
end
332-
333-
functions = Array(functions).map(&:to_sym)
334-
labels = function_data['label'] || function_data['labels']
335-
labels = Array(labels).map(&:to_s) if labels
336-
Function.new(package, cls, labels, functions, function_data['builtin'], function_data['require'])
337369
end
338370
end
339371

@@ -405,7 +437,7 @@ def package
405437
# Hook a method which is specified by class and method name.
406438
def package_for_code_object
407439
class_name = cls.to_s.index('#<Class:') == 0 ? cls.to_s['#<Class:'.length...-1] : cls.name
408-
Array(config.hooked_methods[class_name])
440+
Array(config.gem_hooks[class_name])
409441
.find { |hook| hook.include_method?(method.name) }
410442
&.package
411443
end

lib/appmap/hook.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ def hook_builtins
128128
end
129129

130130
hook_loaded_code.(config.builtin_hooks, true)
131-
hook_loaded_code.(config.hooked_methods, false)
131+
hook_loaded_code.(config.gem_hooks, false)
132132
end
133133

134134
protected

0 commit comments

Comments
 (0)