Class: AppMap::Config

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

Defined Under Namespace

Classes: LookupPackage, Package

Constant Summary collapse

METHOD_HOOKS =

Hook well-known functions. When a function configured here is available in the bundle, it will be hooked with the predefined labels specified here. If any of these hooks are not desired, they can be disabled in the exclude section of appmap.yml.

[
  package_hooks('actionview',
    [
      method_hook('ActionView::Renderer', :render, %w[mvc.view]),
      method_hook('ActionView::TemplateRenderer', :render, %w[mvc.view]),
      method_hook('ActionView::PartialRenderer', :render, %w[mvc.view])
    ],
    handler_class: AppMap::Handler::Rails::Template::RenderHandler,
    package_name: 'action_view'
  ),
  package_hooks('actionview',
    [
      method_hook('ActionView::Resolver', %i[find_all find_all_anywhere], %w[mvc.template.resolver])
    ],
    handler_class: AppMap::Handler::Rails::Template::ResolverHandler,
    package_name: 'action_view'
  ),
  package_hooks('actionpack',
    [
      method_hook('ActionDispatch::Request::Session', %i[[] dig values fetch], %w[http.session.read]),
      method_hook('ActionDispatch::Request::Session', %i[destroy[]= clear update delete merge], %w[http.session.write]),
      method_hook('ActionDispatch::Cookies::CookieJar', %i[[]= clear update delete recycle], %w[http.session.read]),
      method_hook('ActionDispatch::Cookies::CookieJar', %i[[]= clear update delete recycle], %w[http.session.write]),
      method_hook('ActionDispatch::Cookies::EncryptedCookieJar', %i[[]= clear update delete recycle], %w[http.cookie crypto.encrypt])
    ],
    package_name: 'action_dispatch'
  ),
  package_hooks('cancancan',
    [
      method_hook('CanCan::ControllerAdditions', %i[authorize! can? cannot?], %w[security.authorization]),
      method_hook('CanCan::Ability', %i[authorize?], %w[security.authorization])
    ]
  ),
  package_hooks('actionpack',
    [
      method_hook('ActionController::Instrumentation', %i[process_action send_file send_data redirect_to], %w[mvc.controller])
    ],
    package_name: 'action_controller'
  )
].flatten.freeze
OPENSSL_PACKAGES =
->(labels) { Package.build_from_path('openssl', package_name: 'openssl', labels: labels) }
BUILTIN_HOOKS =

Hook functions which are builtin to Ruby. Because they are builtins, they may be loaded before appmap. Therefore, we can't rely on TracePoint to report the loading of this code.

{
  'OpenSSL::PKey::PKey' => TargetMethods.new(:sign, OPENSSL_PACKAGES.(%w[crypto.pkey])),
  'OpenSSL::X509::Request' => TargetMethods.new(%i[sign verify], OPENSSL_PACKAGES.(%w[crypto.x509])),
  'OpenSSL::PKCS5' => TargetMethods.new(%i[pbkdf2_hmac_sha1 pbkdf2_hmac], OPENSSL_PACKAGES.(%w[crypto.pkcs5])),
  'OpenSSL::Cipher' => [
    TargetMethods.new(%i[encrypt], OPENSSL_PACKAGES.(%w[crypto.encrypt])),
    TargetMethods.new(%i[decrypt], OPENSSL_PACKAGES.(%w[crypto.decrypt]))
  ],
  'ActiveSupport::Callbacks::CallbackSequence' => [
    TargetMethods.new(:invoke_before, Package.build_from_gem('activesupport', force: true, package_name: 'active_support', labels: %w[mvc.before_action])),
    TargetMethods.new(:invoke_after, Package.build_from_gem('activesupport', force: true, package_name: 'active_support', labels: %w[mvc.after_action])),
  ],
  'ActiveSupport::SecurityUtils' => TargetMethods.new(:secure_compare, Package.build_from_gem('activesupport', force: true, package_name: 'active_support/security_utils', labels: %w[crypto.secure_compare])),
  'OpenSSL::X509::Certificate' => TargetMethods.new(:sign, OPENSSL_PACKAGES.(%w[crypto.x509])),
  'Net::HTTP' => TargetMethods.new(:request, Package.build_from_path('net/http', package_name: 'net/http', labels: %w[protocol.http]).tap do |package|
    package.handler_class = AppMap::Handler::NetHTTP
  end),
  'Net::SMTP' => TargetMethods.new(:send, Package.build_from_path('net/smtp', package_name: 'net/smtp', labels: %w[protocol.email.smtp])),
  'Net::POP3' => TargetMethods.new(:mails, Package.build_from_path('net/pop3', package_name: 'net/pop', labels: %w[protocol.email.pop])),
  # This is happening: Method send_command not found on Net::IMAP
  # 'Net::IMAP' => TargetMethods.new(:send_command, Package.build_from_path('net/imap', package_name: 'net/imap', labels: %w[protocol.email.imap])),
  # 'Marshal' => TargetMethods.new(%i[dump load], Package.build_from_path('marshal', labels: %w[format.marshal])),
  'Psych' => [
    TargetMethods.new(%i[load load_stream parse parse_stream], Package.build_from_path('yaml', package_name: 'psych', labels: %w[format.yaml.parse])),
    TargetMethods.new(%i[dump dump_stream], Package.build_from_path('yaml', package_name: 'psych', labels: %w[format.yaml.generate])),
  ],
  'JSON::Ext::Parser' => TargetMethods.new(:parse, Package.build_from_path('json', package_name: 'json', labels: %w[format.json.parse])),
  'JSON::Ext::Generator::State' => TargetMethods.new(:generate, Package.build_from_path('json', package_name: 'json', labels: %w[format.json.generate])),
}.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.



236
237
238
239
240
241
242
243
244
245
246
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
# File 'lib/appmap/config.rb', line 236

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
  @builtin_hooks = BUILTIN_HOOKS.dup
  @functions = functions

  @hooked_methods = METHOD_HOOKS.each_with_object(Hash.new { |h,k| h[k] = [] }) do |cls_target_methods, hooked_methods|
    hooked_methods[cls_target_methods.cls] << cls_target_methods.target_methods
  end

  functions.each do |func|
    package_options = {}
    package_options[:labels] = func.labels if func.labels
    package_options[:package_name] = func.package_name
    package_options[:package_name] ||= func.package if func.builtin
    hook = TargetMethods.new(func.function_names, Package.build_from_path(func.package, **package_options))
    if func.builtin
      @builtin_hooks[func.cls] ||= []
      @builtin_hooks[func.cls] << hook
    else
      @hooked_methods[func.cls] << hook
    end
  end

  @hooked_methods.each_value do |hooks|
    Array(hooks).each do |hook|
      @hook_paths << hook.package.path
    end
  end
end

Instance Attribute Details

#appmap_dir ⇒ Object (readonly)

Returns the value of attribute appmap_dir.



234
235
236
# File 'lib/appmap/config.rb', line 234

def appmap_dir
  @appmap_dir
end

#builtin_hooks ⇒ Object (readonly)

Returns the value of attribute builtin_hooks.



234
235
236
# File 'lib/appmap/config.rb', line 234

def builtin_hooks
  @builtin_hooks
end

#depends_config ⇒ Object (readonly)

Returns the value of attribute depends_config.



234
235
236
# File 'lib/appmap/config.rb', line 234

def depends_config
  @depends_config
end

#exclude ⇒ Object (readonly)

Returns the value of attribute exclude.



234
235
236
# File 'lib/appmap/config.rb', line 234

def exclude
  @exclude
end

#hooked_methods ⇒ Object (readonly)

Returns the value of attribute hooked_methods.



234
235
236
# File 'lib/appmap/config.rb', line 234

def hooked_methods
  @hooked_methods
end

#name ⇒ Object (readonly)

Returns the value of attribute name.



234
235
236
# File 'lib/appmap/config.rb', line 234

def name
  @name
end

#packages ⇒ Object (readonly)

Returns the value of attribute packages.



234
235
236
# File 'lib/appmap/config.rb', line 234

def packages
  @packages
end

#swagger_config ⇒ Object (readonly)

Returns the value of attribute swagger_config.



234
235
236
# File 'lib/appmap/config.rb', line 234

def swagger_config
  @swagger_config
end

Class Method Details

.load(config_data) ⇒ Object

Loads configuration from a Hash.



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

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 |function_data|
      function_name = function_data['name']
      package, cls, functions = []
      if function_name
        package, cls, _, function = Util.parse_function_name(function_name)
        functions = Array(function)
      else
        package = function_data['package']
        cls = function_data['class']
        functions = function_data['function'] || function_data['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 = function_data['label'] || function_data['labels']
      labels = Array(labels).map(&:to_s) if labels
      Function.new(package, cls, labels, functions, function_data['builtin'], function_data['require'])
    end
  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

        if gem
          shallow = package['shallow']
          # shallow is true by default for gems
          shallow = true if shallow.nil?
          Package.build_from_gem(gem, package_name: package['package'], 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.



279
280
281
282
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
314
315
316
317
318
319
320
321
322
323
324
325
326
327
# File 'lib/appmap/config.rb', line 279

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

  config_present = true if File.exists?(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 try and guess some reasonable defaults.
    To suppress this message, create the file:
    
    #{Pathname.new(config_file_name).expand_path}
    
    Here are the default settings that will be used in the meantime. You can
    copy and paste this example to start your appmap.yml.
    MISSING_FILE_MSG
    {}
  end

  load(config_data).tap do |config|
    config_yaml = {
      'name' => config.name,
      'packages' => config.packages.select{|p| p.path}.map do |pkg|
        { 'path' => pkg.path }
      end,
      'exclude' => []
    }.compact
    unless config_present
      warn Util.color(YAML.dump(config_yaml), :magenta)
      warn .()
    end
  end
end

.method_hook(cls, method_names, labels) ⇒ Object



151
152
153
# File 'lib/appmap/config.rb', line 151

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

.package_hooks(gem_name, methods, handler_class: nil, package_name: nil) ⇒ Object



141
142
143
144
145
146
147
148
149
# File 'lib/appmap/config.rb', line 141

def package_hooks(gem_name, methods, handler_class: nil, package_name: nil)
  Array(methods).map do |method|
    package = Package.build_from_gem(gem_name, package_name: package_name, labels: method.labels, shallow: false, optional: true)
    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



442
443
444
# File 'lib/appmap/config.rb', line 442

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

#never_hook?(cls, method) ⇒ Boolean

Returns:

  • (Boolean)


446
447
448
449
# File 'lib/appmap/config.rb', line 446

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)


402
403
404
405
# File 'lib/appmap/config.rb', line 402

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

#to_h ⇒ Object



392
393
394
395
396
397
398
399
# File 'lib/appmap/config.rb', line 392

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