Class: AppMap::Config

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

Defined Under Namespace

Classes: HookConfig, LookupHookConfig, Package

Constant Summary collapse

RECORD_AROUND_LABELS =
%w[job.perform cli.command message.handle].freeze

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.



298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
# File 'lib/appmap/config.rb', line 298

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.



296
297
298
# File 'lib/appmap/config.rb', line 296

def appmap_dir
  @appmap_dir
end

#builtin_hooksObject (readonly)

Returns the value of attribute builtin_hooks.



296
297
298
# File 'lib/appmap/config.rb', line 296

def builtin_hooks
  @builtin_hooks
end

#depends_configObject (readonly)

Returns the value of attribute depends_config.



296
297
298
# File 'lib/appmap/config.rb', line 296

def depends_config
  @depends_config
end

#excludeObject (readonly)

Returns the value of attribute exclude.



296
297
298
# File 'lib/appmap/config.rb', line 296

def exclude
  @exclude
end

#gem_hooksObject (readonly)

Returns the value of attribute gem_hooks.



296
297
298
# File 'lib/appmap/config.rb', line 296

def gem_hooks
  @gem_hooks
end

#nameObject (readonly)

Returns the value of attribute name.



296
297
298
# File 'lib/appmap/config.rb', line 296

def name
  @name
end

#packagesObject (readonly)

Returns the value of attribute packages.



296
297
298
# File 'lib/appmap/config.rb', line 296

def packages
  @packages
end

#swagger_configObject (readonly)

Returns the value of attribute swagger_config.



296
297
298
# File 'lib/appmap/config.rb', line 296

def swagger_config
  @swagger_config
end

Class Method Details

.builtin_hooks_pathObject



254
255
256
# File 'lib/appmap/config.rb', line 254

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

.declare_hook(hook_decl) ⇒ Object



197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
# File 'lib/appmap/config.rb', line 197

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



228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
# File 'lib/appmap/config.rb', line 228

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 "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



258
259
260
# File 'lib/appmap/config.rb', line 258

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

.load(config_data) ⇒ Object

Loads configuration from a Hash.



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
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
# File 'lib/appmap/config.rb', line 391

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 "AppMap config 'package' element should specify 'gem' or 'path', not both" if gem && path
        raise "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.



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
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
# File 'lib/appmap/config.rb', line 332

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 .call
    warn ""
    warn Util.color(%(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.exist?(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 .call
      end
    end
  end
end

.load_hooksObject



262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
# File 'lib/appmap/config.rb', line 262

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(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.call(path) do |path, config|
      config["path"] = path
      config["builtin"] = true
    end
  end

  gem_hooks = gem_hooks_path.map do |path|
    loader.call(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



193
194
195
# File 'lib/appmap/config.rb', line 193

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



177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
# File 'lib/appmap/config.rb', line 177

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_hook_config(cls, method) ⇒ Object



519
520
521
# File 'lib/appmap/config.rb', line 519

def lookup_hook_config(cls, method)
  LookupHookConfig.new(self, cls, method).hook_config
end

#never_hook?(cls, method) ⇒ Boolean

Returns:

  • (Boolean)


523
524
525
526
527
528
529
530
531
532
533
534
535
# File 'lib/appmap/config.rb', line 523

def never_hook?(cls, method)
  unless method
    HookLog.log "method is nil" if HookLog.enabled?
    return true
  end

  _, separator, = ::AppMap::Hook.qualify_method_name(method)
  if exclude.member?(cls.name) || exclude.member?([cls.name, separator, method.name].join)
    HookLog.log "Hooking of #{method} disabled by configuration" if HookLog.enabled?
    return true
  end
  false
end

#path_enabled?(path) ⇒ Boolean

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

Returns:

  • (Boolean)


459
460
461
462
# File 'lib/appmap/config.rb', line 459

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

#to_hObject



449
450
451
452
453
454
455
456
# File 'lib/appmap/config.rb', line 449

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