Class: AppMap::Config

Inherits:
Object show all
Defined in:
lib/appmap/config.rb

Defined Under Namespace

Classes: LookupPackage, Package

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(name, packages: [], swagger_config: Swagger::Configuration.new, depends_config: Depends::Configuration.new, exclude: [], functions: []) ⇒ Config

Returns a new instance of Config.



283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
# File 'lib/appmap/config.rb', line 283

def initialize(name,
  packages: [],
  swagger_config: Swagger::Configuration.new,
  depends_config: Depends::Configuration.new,
  exclude: [],
  functions: [])
  @name = name
  @appmap_dir = AppMap::DEFAULT_APPMAP_DIR
  @packages = packages
  @swagger_config = swagger_config
  @depends_config = depends_config
  @hook_paths = Set.new(packages.map(&:path))
  @exclude = exclude
  @functions = functions

  @builtin_hooks = Hash.new { |h,k| h[k] = [] }
  @gem_hooks = Hash.new { |h,k| h[k] = [] }
  
  (functions + self.class.load_hooks).each_with_object(Hash.new { |h,k| h[k] = [] }) do |cls_target_methods, gem_hooks|
    hooks = if cls_target_methods.target_methods.package.builtin
      @builtin_hooks
    else
      @gem_hooks
    end
    hooks[cls_target_methods.cls] << cls_target_methods.target_methods
  end

  @gem_hooks.each_value do |hooks|
    @hook_paths += Array(hooks).map { |hook| hook.package.path }.compact
  end
end

Instance Attribute Details

#appmap_dirObject (readonly)

Returns the value of attribute appmap_dir.



281
282
283
# File 'lib/appmap/config.rb', line 281

def appmap_dir
  @appmap_dir
end

#builtin_hooksObject (readonly)

Returns the value of attribute builtin_hooks.



281
282
283
# File 'lib/appmap/config.rb', line 281

def builtin_hooks
  @builtin_hooks
end

#depends_configObject (readonly)

Returns the value of attribute depends_config.



281
282
283
# File 'lib/appmap/config.rb', line 281

def depends_config
  @depends_config
end

#excludeObject (readonly)

Returns the value of attribute exclude.



281
282
283
# File 'lib/appmap/config.rb', line 281

def exclude
  @exclude
end

#gem_hooksObject (readonly)

Returns the value of attribute gem_hooks.



281
282
283
# File 'lib/appmap/config.rb', line 281

def gem_hooks
  @gem_hooks
end

#nameObject (readonly)

Returns the value of attribute name.



281
282
283
# File 'lib/appmap/config.rb', line 281

def name
  @name
end

#packagesObject (readonly)

Returns the value of attribute packages.



281
282
283
# File 'lib/appmap/config.rb', line 281

def packages
  @packages
end

#swagger_configObject (readonly)

Returns the value of attribute swagger_config.



281
282
283
# File 'lib/appmap/config.rb', line 281

def swagger_config
  @swagger_config
end

Class Method Details

.builtin_hooks_pathObject



239
240
241
# File 'lib/appmap/config.rb', line 239

def builtin_hooks_path
  [ [ __dir__, 'builtin_hooks' ].join('/') ] + ( ENV['APPMAP_BUILTIN_HOOKS_PATH'] || '').split(/[;:]/)
end

.declare_hook(hook_decl) ⇒ Object



182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
# File 'lib/appmap/config.rb', line 182

def declare_hook(hook_decl)
  hook_decl = YAML.load(hook_decl) if hook_decl.is_a?(String)
  
  methods_decl = hook_decl['methods'] || hook_decl['method']
  methods_decl = Array(methods_decl) unless methods_decl.is_a?(Hash)
  labels_decl = Array(hook_decl['labels'] || hook_decl['label'])

  methods = methods_decl.map do |name|
    class_name, method_name, static = name.include?('.') ? name.split('.', 2) + [ true ] : name.split('#', 2) + [ false ]
    method_hook class_name, [ method_name ], labels_decl
  end

  require_name = hook_decl['require_name']
  gem_name = hook_decl['gem']
  path = hook_decl['path']
  builtin = hook_decl['builtin']

  options = {
    builtin: builtin,
    gem: gem_name,
    path: path,
    require_name: require_name || gem_name || path,
    force: hook_decl['force']
  }.compact

  handler_class = hook_decl['handler_class']
  options[:handler_class] = Handler.find(handler_class) if handler_class

  package_hooks(methods, **options)
end

.declare_hook_deprecated(hook_decl) ⇒ Object



213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
# File 'lib/appmap/config.rb', line 213

def declare_hook_deprecated(hook_decl)
  function_name = hook_decl['name']
  package, cls, functions = []
  if function_name
    package, cls, _, function = Util.parse_function_name(function_name)
    functions = Array(function)
  else
    package = hook_decl['package']
    cls = hook_decl['class']
    functions = hook_decl['function'] || hook_decl['functions']
    raise %q(AppMap config 'function' element should specify 'package', 'class' and 'function' or 'functions') unless package && cls && functions
  end

  functions = Array(functions).map(&:to_sym)
  labels = hook_decl['label'] || hook_decl['labels']
  req = hook_decl['require']
  builtin = hook_decl['builtin']

  package_options = {}
  package_options[:labels] = Array(labels).map(&:to_s) if labels
  package_options[:require_name] = req
  package_options[:require_name] ||= package if builtin
  tm = TargetMethods.new(functions, Package.build_from_path(package, **package_options))
  ClassTargetMethods.new(cls, tm)
end

.gem_hooks_pathObject



243
244
245
# File 'lib/appmap/config.rb', line 243

def gem_hooks_path
  [ [ __dir__, 'gem_hooks' ].join('/') ] + ( ENV['APPMAP_GEM_HOOKS_PATH'] || '').split(/[;:]/)
end

.load(config_data) ⇒ Object

Loads configuration from a Hash.



376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
# File 'lib/appmap/config.rb', line 376

def load(config_data)
  name = config_data['name'] || Service::Guesser.guess_name
  config_params = {
    exclude: config_data['exclude']
  }.compact

  if config_data['functions']
    config_params[:functions] = config_data['functions'].map do |hook_decl|
      if hook_decl['name'] || hook_decl['package']
        declare_hook_deprecated(hook_decl)
      else
        # Support the same syntax within the 'functions' that's used for externalized
        # hook config.
        declare_hook(hook_decl)
      end
    end.flatten
  end

  config_params[:packages] = \
    if config_data['packages']
      config_data['packages'].map do |package|
        gem = package['gem']
        path = package['path']
        raise %q(AppMap config 'package' element should specify 'gem' or 'path', not both) if gem && path
        raise %q(AppMap config 'package' element should specify 'gem' or 'path') unless gem || path

        if gem
          shallow = package['shallow']
          # shallow is true by default for gems
          shallow = true if shallow.nil?

          require_name = \
            package['package'] || #deprecated
            package['require_name']
          Package.build_from_gem(gem, require_name: require_name, exclude: package['exclude'] || [], shallow: shallow)
        else
          Package.build_from_path(path, exclude: package['exclude'] || [], shallow: package['shallow'])
        end
      end.compact
    else
      Array(Service::Guesser.guess_paths).map do |path|
        Package.build_from_path(path)
      end
    end

  if config_data['swagger']
    swagger_config = Swagger::Configuration.load(config_data['swagger'])
    config_params[:swagger_config] = swagger_config
  end
  if config_data['depends']
    depends_config = Depends::Configuration.load(config_data['depends'])
    config_params[:depends_config] = depends_config
  end

  Config.new name, **config_params
end

.load_from_file(config_file_name) ⇒ Object

Loads configuration data from a file, specified by the file name.



317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
# File 'lib/appmap/config.rb', line 317

def load_from_file(config_file_name)
   = lambda do
    Util.color(<<~LOGO, :magenta)
       ___             __  ___
      / _ | ___  ___  /  |/  /__ ____
     / __ |/ _ \\/ _ \\/ /|_/ / _ `/ _ \\
    /_/ |_/ .__/ .__/_/  /_/\\_,_/ .__/
         /_/  /_/              /_/
    LOGO
  end

  config_present = true if File.exist?(config_file_name)

  config_data = if config_present
    YAML.safe_load(::File.read(config_file_name))
  else
    warn .()
    warn ''
    warn Util.color(%Q|NOTICE: The AppMap config file #{config_file_name} was not found!|, :magenta, bold: true)
    warn ''
    warn Util.color(<<~MISSING_FILE_MSG, :magenta)
    AppMap uses this file to customize its behavior. For example, you can use
    the 'packages' setting to indicate which local file paths and dependency
    gems you want to include in the AppMap. Since you haven't provided specific
    settings, the appmap gem will use these default options:
    MISSING_FILE_MSG
    {}
  end

  load(config_data).tap do |config|
    {
      'name' => config.name,
      'language' => 'ruby',
      'appmap_dir' => AppMap::DEFAULT_APPMAP_DIR,
      'packages' => config.packages.select{|p| p.path}.map do |pkg|
        { 'path' => pkg.path }
      end,
      'exclude' => []
    }.compact.tap do |config_yaml|
      unless config_present
        warn Util.color(YAML.dump(config_yaml), :magenta)
        dirname = Pathname.new(config_file_name).dirname.expand_path
        if Dir.exists?(dirname) && File.writable?(dirname)
          warn Util.color(<<~CONFIG_FILE_MSG, :magenta)
          This file will be saved to #{Pathname.new(config_file_name).expand_path},
          where you can customize it.
          CONFIG_FILE_MSG
          File.write(config_file_name, YAML.dump(config_yaml))
        end
        warn Util.color(<<~CONFIG_FILE_MSG, :magenta)
        For more information, see https://appmap.io/docs/reference/appmap-ruby.html#configuration
        CONFIG_FILE_MSG
        warn .()
      end
    end
  end
end

.load_hooksObject



247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
# File 'lib/appmap/config.rb', line 247

def load_hooks
  loader = lambda do |dir, &block|
    basename = dir.split('/').compact.join('/')
    [].tap do |hooks|
      Dir.glob(Pathname.new(dir).join('**').join('*.yml').to_s).each do |yaml_file|
        path = yaml_file[basename.length + 1...-4]
        YAML.load(File.read(yaml_file)).map do |config|
          block.call path, config
          config
        end.each do |config|
          hooks << declare_hook(config)
        end
      end
    end.compact
  end

  builtin_hooks = builtin_hooks_path.map do |path|
    loader.(path) do |path, config|
      config['path'] = path
      config['builtin'] = true
    end
  end

  gem_hooks = gem_hooks_path.map do |path|
    loader.(path) do |path, config|
      config['gem'] = path
      config['builtin'] = false
    end
  end

  (builtin_hooks + gem_hooks).flatten
end

.method_hook(cls, method_names, labels) ⇒ Object



178
179
180
# File 'lib/appmap/config.rb', line 178

def method_hook(cls, method_names, labels)
  MethodHook.new(cls, method_names, labels)
end

.package_hooks(methods, path: nil, gem: nil, force: false, builtin: false, handler_class: nil, require_name: nil) ⇒ Object



162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'lib/appmap/config.rb', line 162

def package_hooks(methods, path: nil, gem: nil, force: false, builtin: false, handler_class: nil, require_name: nil)
  Array(methods).map do |method|
    package = if builtin
      Package.build_from_builtin(path || require_name, require_name: require_name, labels: method.labels, shallow: false)
    elsif gem
      Package.build_from_gem(gem, require_name: require_name, labels: method.labels, shallow: false, force: force, optional: true)
    elsif path
      Package.build_from_path(path, require_name: require_name, labels: method.labels, shallow: false)
    end
    next unless package

    package.handler_class = handler_class if handler_class
    ClassTargetMethods.new(method.cls, TargetMethods.new(Array(method.method_names), package))
  end.compact
end

Instance Method Details

#lookup_package(cls, method) ⇒ Object



496
497
498
# File 'lib/appmap/config.rb', line 496

def lookup_package(cls, method)
  LookupPackage.new(self, cls, method).package
end

#never_hook?(cls, method) ⇒ Boolean

Returns:

  • (Boolean)


500
501
502
503
# File 'lib/appmap/config.rb', line 500

def never_hook?(cls, method)
  _, separator, = ::AppMap::Hook.qualify_method_name(method)
  return true if exclude.member?(cls.name) || exclude.member?([ cls.name, separator, method.name ].join)
end

#path_enabled?(path) ⇒ Boolean

Determines if methods defined in a file path should possibly be hooked.

Returns:

  • (Boolean)


444
445
446
447
# File 'lib/appmap/config.rb', line 444

def path_enabled?(path)
  path = AppMap::Util.normalize_path(path)
  @hook_paths.find { |hook_path| path.index(hook_path) == 0 }
end

#to_hObject



434
435
436
437
438
439
440
441
# File 'lib/appmap/config.rb', line 434

def to_h
  {
    name: name,
    packages: packages.map(&:to_h),
    functions: @functions.map(&:to_h),
    exclude: exclude
  }.compact
end