appmap 0.48.2 → 0.51.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/appmap.rb CHANGED
@@ -27,7 +27,7 @@ module AppMap
27
27
  # Gets the configuration. If there is no configuration, the default
28
28
  # configuration is initialized.
29
29
  def configuration
30
- @configuration ||= initialize
30
+ @configuration ||= initialize_configuration
31
31
  end
32
32
 
33
33
  # Sets the configuration. This is only expected to happen once per
@@ -38,19 +38,34 @@ module AppMap
38
38
  @configuration = config
39
39
  end
40
40
 
41
- # Configures AppMap for recording. Default behavior is to configure from "appmap.yml".
41
+ def default_config_file_path
42
+ ENV['APPMAP_CONFIG_FILE'] || 'appmap.yml'
43
+ end
44
+
45
+ # Configures AppMap for recording. Default behavior is to configure from
46
+ # APPMAP_CONFIG_FILE, or 'appmap.yml'. If no config file is available, a
47
+ # configuration will be automatically generated and used - and the user is prompted
48
+ # to create the config file.
49
+ #
42
50
  # This method also activates the code hooks which record function calls as trace events.
43
51
  # Call this function before the program code is loaded by the Ruby VM, otherwise
44
52
  # the load events won't be seen and the hooks won't activate.
45
- def initialize(config_file_path = 'appmap.yml')
46
- raise "AppMap configuration file #{config_file_path} does not exist" unless ::File.exists?(config_file_path)
47
- warn "Configuring AppMap from path #{config_file_path}"
53
+ def initialize_configuration(config_file_path = default_config_file_path)
54
+ startup_message "Configuring AppMap from path #{config_file_path}"
48
55
  Config.load_from_file(config_file_path).tap do |configuration|
49
56
  self.configuration = configuration
50
57
  Hook.new(configuration).enable
51
58
  end
52
59
  end
53
60
 
61
+ def info(msg)
62
+ if defined?(::Rails) && defined?(::Rails.logger)
63
+ ::Rails.logger.info msg
64
+ else
65
+ warn msg
66
+ end
67
+ end
68
+
54
69
  # Used to start tracing, stop tracing, and record events.
55
70
  def tracing
56
71
  @tracing ||= Trace::Tracing.new
@@ -94,8 +109,60 @@ module AppMap
94
109
  @metadata ||= Metadata.detect.freeze
95
110
  @metadata.deep_dup
96
111
  end
112
+
113
+ def startup_message(msg)
114
+ if defined?(::Rails) && defined?(::Rails.logger) && ::Rails.logger
115
+ ::Rails.logger.debug msg
116
+ elsif ENV['DEBUG'] == 'true'
117
+ warn msg
118
+ end
119
+ end
97
120
  end
98
121
  end
99
122
 
100
- require 'appmap/railtie' if defined?(::Rails::Railtie)
101
- AppMap.initialize if ENV['APPMAP'] == 'true'
123
+ lambda do
124
+ Initializer = Struct.new(:class_name, :module_name, :gem_module_name)
125
+
126
+ INITIALIZERS = {
127
+ 'Rails::Railtie' => Initializer.new('AppMap::Railtie', 'appmap/railtie', 'railtie'),
128
+ 'RSpec' => Initializer.new('AppMap::RSpec', 'appmap/rspec', 'rspec-core'),
129
+ 'Minitest::Unit::TestCase' => Initializer.new('AppMap::Minitest', 'appmap/minitest', 'minitest')
130
+ }
131
+
132
+ TracePoint.new(:class) do |tp|
133
+ cls_name = tp.self.name
134
+ initializers = INITIALIZERS.delete(cls_name)
135
+ if initializers
136
+ initializers = [ initializers ] unless initializers.is_a?(Array)
137
+ next if Object.const_defined?(initializers.first.class_name)
138
+
139
+ gem_module_name = initializers.first.gem_module_name
140
+
141
+ AppMap.startup_message AppMap::Util.color(<<~LOAD_MSG, :magenta)
142
+ When 'appmap' was loaded, '#{gem_module_name}' had not been loaded yet. Now '#{gem_module_name}' has
143
+ just been loaded, so the following AppMap modules will be automatically required:
144
+
145
+ #{initializers.map(&:module_name).join("\n")}
146
+
147
+ To suppress this message, ensure '#{gem_module_name}' appears before 'appmap' in your Gemfile.
148
+ LOAD_MSG
149
+ initializers.each do |init|
150
+ require init.module_name
151
+ end
152
+ end
153
+ end.enable
154
+
155
+ if defined?(::Rails::Railtie)
156
+ require 'appmap/railtie'
157
+ end
158
+
159
+ if defined?(::RSpec)
160
+ require 'appmap/rspec'
161
+ end
162
+
163
+ if defined?(::Minitest)
164
+ require 'appmap/minitest'
165
+ end
166
+ end.call
167
+
168
+ AppMap.initialize_configuration if ENV['APPMAP'] == 'true'
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'appmap/service/guesser'
5
+ require 'appmap/util'
6
+
7
+ module AppMap
8
+ module Command
9
+ InitStruct = Struct.new(:config_file)
10
+
11
+ class Init < InitStruct
12
+ def perform
13
+ if File.exist?(config_file)
14
+ puts AppMap::Util.color(%(The AppMap config file #{config_file} already exists.), :magenta)
15
+ return
16
+ end
17
+
18
+ ensure_directory_exists
19
+
20
+ config = {
21
+ 'name' => Service::Guesser.guess_name,
22
+ 'packages' => Service::Guesser.guess_paths.map { |path| { 'path' => path } }
23
+ }
24
+ content = YAML.dump(config).gsub("---\n", '')
25
+
26
+ File.write(config_file, content)
27
+ puts AppMap::Util.color(
28
+ %(The following AppMap config file #{config_file} has been created:),
29
+ :green
30
+ )
31
+ puts content
32
+ end
33
+
34
+ private
35
+
36
+ def ensure_directory_exists
37
+ dirname = File.dirname(config_file)
38
+ FileUtils.mkdir_p(dirname) unless File.directory?(dirname)
39
+ end
40
+ end
41
+ end
42
+ end
data/lib/appmap/config.rb CHANGED
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'yaml'
3
4
  require 'appmap/handler/net_http'
4
5
  require 'appmap/handler/rails/template'
6
+ require 'appmap/service/guesser'
5
7
 
6
8
  module AppMap
7
9
  class Config
@@ -167,8 +169,10 @@ module AppMap
167
169
  ),
168
170
  package_hooks('actionpack',
169
171
  [
170
- method_hook('ActionDispatch::Request::Session', %i[destroy [] dig values []= clear update delete fetch merge], %w[http.session]),
171
- method_hook('ActionDispatch::Cookies::CookieJar', %i[[]= clear update delete recycle], %w[http.session]),
172
+ method_hook('ActionDispatch::Request::Session', %i[[] dig values fetch], %w[http.session.read]),
173
+ method_hook('ActionDispatch::Request::Session', %i[destroy[]= clear update delete merge], %w[http.session.write]),
174
+ method_hook('ActionDispatch::Cookies::CookieJar', %i[[]= clear update delete recycle], %w[http.session.read]),
175
+ method_hook('ActionDispatch::Cookies::CookieJar', %i[[]= clear update delete recycle], %w[http.session.write]),
172
176
  method_hook('ActionDispatch::Cookies::EncryptedCookieJar', %i[[]= clear update delete recycle], %w[http.cookie crypto.encrypt])
173
177
  ],
174
178
  package_name: 'action_dispatch'
@@ -213,15 +217,22 @@ module AppMap
213
217
  # This is happening: Method send_command not found on Net::IMAP
214
218
  # 'Net::IMAP' => TargetMethods.new(:send_command, Package.build_from_path('net/imap', package_name: 'net/imap', labels: %w[protocol.email.imap])),
215
219
  # 'Marshal' => TargetMethods.new(%i[dump load], Package.build_from_path('marshal', labels: %w[format.marshal])),
216
- 'Psych' => TargetMethods.new(%i[dump dump_stream load load_stream parse parse_stream], Package.build_from_path('yaml', package_name: 'psych', labels: %w[format.yaml])),
217
- 'JSON::Ext::Parser' => TargetMethods.new(:parse, Package.build_from_path('json', package_name: 'json', labels: %w[format.json])),
218
- 'JSON::Ext::Generator::State' => TargetMethods.new(:generate, Package.build_from_path('json', package_name: 'json', labels: %w[format.json])),
220
+ 'Psych' => [
221
+ TargetMethods.new(%i[load load_stream parse parse_stream], Package.build_from_path('yaml', package_name: 'psych', labels: %w[format.yaml.parse])),
222
+ TargetMethods.new(%i[dump dump_stream], Package.build_from_path('yaml', package_name: 'psych', labels: %w[format.yaml.generate])),
223
+ ],
224
+ 'JSON::Ext::Parser' => TargetMethods.new(:parse, Package.build_from_path('json', package_name: 'json', labels: %w[format.json.parse])),
225
+ 'JSON::Ext::Generator::State' => TargetMethods.new(:generate, Package.build_from_path('json', package_name: 'json', labels: %w[format.json.generate])),
219
226
  }.freeze
220
227
 
221
- attr_reader :name, :packages, :exclude, :hooked_methods, :builtin_hooks
228
+ attr_reader :name, :appmap_dir, :packages, :exclude, :hooked_methods, :builtin_hooks
222
229
 
223
- def initialize(name, packages, exclude: [], functions: [])
230
+ def initialize(name,
231
+ packages: [],
232
+ exclude: [],
233
+ functions: [])
224
234
  @name = name
235
+ @appmap_dir = AppMap::DEFAULT_APPMAP_DIR
225
236
  @packages = packages
226
237
  @hook_paths = Set.new(packages.map(&:path))
227
238
  @exclude = exclude
@@ -248,38 +259,98 @@ module AppMap
248
259
  class << self
249
260
  # Loads configuration data from a file, specified by the file name.
250
261
  def load_from_file(config_file_name)
251
- require 'yaml'
252
- load YAML.safe_load(::File.read(config_file_name))
262
+ logo = lambda do
263
+ Util.color(<<~LOGO, :magenta)
264
+ ___ __ ___
265
+ / _ | ___ ___ / |/ /__ ____
266
+ / __ |/ _ \\/ _ \\/ /|_/ / _ `/ _ \\
267
+ /_/ |_/ .__/ .__/_/ /_/\\_,_/ .__/
268
+ /_/ /_/ /_/
269
+ LOGO
270
+ end
271
+
272
+ config_present = true if File.exists?(config_file_name)
273
+
274
+ config_data = if config_present
275
+ YAML.safe_load(::File.read(config_file_name))
276
+ else
277
+ warn logo.()
278
+ warn ''
279
+ warn Util.color(%Q|NOTICE: The AppMap config file #{config_file_name} was not found!|, :magenta, bold: true)
280
+ warn ''
281
+ warn Util.color(<<~MISSING_FILE_MSG, :magenta)
282
+ AppMap uses this file to customize its behavior. For example, you can use
283
+ the 'packages' setting to indicate which local file paths and dependency
284
+ gems you want to include in the AppMap. Since you haven't provided specific
285
+ settings, the appmap gem will try and guess some reasonable defaults.
286
+ To suppress this message, create the file:
287
+
288
+ #{Pathname.new(config_file_name).expand_path}
289
+
290
+ Here are the default settings that will be used in the meantime. You can
291
+ copy and paste this example to start your appmap.yml.
292
+ MISSING_FILE_MSG
293
+ {}
294
+ end
295
+ load(config_data).tap do |config|
296
+ config_yaml = {
297
+ 'name' => config.name,
298
+ 'packages' => config.packages.select{|p| p.path}.map do |pkg|
299
+ { 'path' => pkg.path }
300
+ end,
301
+ 'exclude' => []
302
+ }.compact
303
+ unless config_present
304
+ warn Util.color(YAML.dump(config_yaml), :magenta)
305
+ warn logo.()
306
+ end
307
+ end
253
308
  end
254
309
 
255
310
  # Loads configuration from a Hash.
256
311
  def load(config_data)
257
- functions = (config_data['functions'] || []).map do |function_data|
258
- package = function_data['package']
259
- cls = function_data['class']
260
- functions = function_data['function'] || function_data['functions']
261
- raise 'AppMap class configuration should specify package, class and function(s)' unless package && cls && functions
262
- functions = Array(functions).map(&:to_sym)
263
- labels = function_data['label'] || function_data['labels']
264
- labels = Array(labels).map(&:to_s) if labels
265
- Function.new(package, cls, labels, functions)
312
+ name = config_data['name'] || Service::Guesser.guess_name
313
+ config_params = {
314
+ exclude: config_data['exclude']
315
+ }.compact
316
+
317
+ if config_data['functions']
318
+ config_params[:functions] = config_data['functions'].map do |function_data|
319
+ package = function_data['package']
320
+ cls = function_data['class']
321
+ functions = function_data['function'] || function_data['functions']
322
+ raise %q(AppMap config 'function' element should specify 'package', 'class' and 'function' or 'functions') unless package && cls && functions
323
+
324
+ functions = Array(functions).map(&:to_sym)
325
+ labels = function_data['label'] || function_data['labels']
326
+ labels = Array(labels).map(&:to_s) if labels
327
+ Function.new(package, cls, labels, functions)
328
+ end
266
329
  end
267
- packages = (config_data['packages'] || []).map do |package|
268
- gem = package['gem']
269
- path = package['path']
270
- raise 'AppMap package configuration should specify gem or path, not both' if gem && path
271
-
272
- if gem
273
- shallow = package['shallow']
274
- # shallow is true by default for gems
275
- shallow = true if shallow.nil?
276
- Package.build_from_gem(gem, exclude: package['exclude'] || [], shallow: shallow)
330
+
331
+ config_params[:packages] = \
332
+ if config_data['packages']
333
+ config_data['packages'].map do |package|
334
+ gem = package['gem']
335
+ path = package['path']
336
+ raise %q(AppMap config 'package' element should specify 'gem' or 'path', not both) if gem && path
337
+
338
+ if gem
339
+ shallow = package['shallow']
340
+ # shallow is true by default for gems
341
+ shallow = true if shallow.nil?
342
+ Package.build_from_gem(gem, exclude: package['exclude'] || [], shallow: shallow)
343
+ else
344
+ Package.build_from_path(path, exclude: package['exclude'] || [], shallow: package['shallow'])
345
+ end
346
+ end.compact
277
347
  else
278
- Package.build_from_path(path, exclude: package['exclude'] || [], shallow: package['shallow'])
348
+ Array(Service::Guesser.guess_paths).map do |path|
349
+ Package.build_from_path(path)
350
+ end
279
351
  end
280
- end.compact
281
- exclude = config_data['exclude'] || []
282
- Config.new config_data['name'], packages, exclude: exclude, functions: functions
352
+
353
+ Config.new name, config_params
283
354
  end
284
355
  end
285
356
 
@@ -289,7 +360,7 @@ module AppMap
289
360
  packages: packages.map(&:to_h),
290
361
  functions: @functions.map(&:to_h),
291
362
  exclude: exclude
292
- }
363
+ }.compact
293
364
  end
294
365
 
295
366
  # Determines if methods defined in a file path should possibly be hooked.
@@ -14,16 +14,30 @@ module AppMap
14
14
  # The class name is generated from the template path. The package name is
15
15
  # 'app/views', and the method name is 'render'. The source location of the method
16
16
  # is, of course, the path to the view template.
17
- TemplateMethod = Struct.new(:path) do
18
- private_instance_methods :path
17
+ class TemplateMethod
19
18
  attr_reader :class_name
20
-
19
+
20
+ attr_reader :path
21
+ private_instance_methods :path
22
+
21
23
  def initialize(path)
22
- super
24
+ @path = path
23
25
 
24
26
  @class_name = path.parameterize.underscore
25
27
  end
26
-
28
+
29
+ def id
30
+ [ package, path, name ]
31
+ end
32
+
33
+ def hash
34
+ id.hash
35
+ end
36
+
37
+ def eql?(other)
38
+ other.is_a?(TemplateMethod) && id.eql?(other.id)
39
+ end
40
+
27
41
  def package
28
42
  'app/views'
29
43
  end
@@ -54,15 +54,21 @@ module AppMap
54
54
 
55
55
  @recordings_by_test = {}
56
56
  @event_methods = Set.new
57
+ @recording_count = 0
57
58
 
58
59
  class << self
59
60
  def init
60
- warn 'Configuring AppMap recorder for Minitest'
61
-
62
61
  FileUtils.mkdir_p APPMAP_OUTPUT_DIR
63
62
  end
64
63
 
64
+ def first_recording?
65
+ @recording_count == 0
66
+ end
67
+
65
68
  def begin_test(test, name)
69
+ AppMap.info 'Configuring AppMap recorder for Minitest' if first_recording?
70
+ @recording_count += 1
71
+
66
72
  @recordings_by_test[test.object_id] = Recording.new(test, name)
67
73
  end
68
74
 
@@ -3,6 +3,13 @@
3
3
  module AppMap
4
4
  # Railtie connects the AppMap recorder to Rails-specific features.
5
5
  class Railtie < ::Rails::Railtie
6
+ initializer 'appmap.remote_recording' do
7
+ require 'appmap/middleware/remote_recording'
8
+ Rails.application.config.middleware.insert_after \
9
+ Rails::Rack::Logger,
10
+ AppMap::Middleware::RemoteRecording
11
+ end
12
+
6
13
  # appmap.subscribe subscribes to ActiveSupport Notifications so that they can be recorded as
7
14
  # AppMap events.
8
15
  initializer 'appmap.subscribe' do |_| # params: app
data/lib/appmap/rspec.rb CHANGED
@@ -139,15 +139,21 @@ module AppMap
139
139
 
140
140
  @recordings_by_example = {}
141
141
  @event_methods = Set.new
142
+ @recording_count = 0
142
143
 
143
144
  class << self
144
145
  def init
145
- warn 'Configuring AppMap recorder for RSpec'
146
-
147
146
  FileUtils.mkdir_p APPMAP_OUTPUT_DIR
148
147
  end
149
148
 
149
+ def first_recording?
150
+ @recording_count == 0
151
+ end
152
+
150
153
  def begin_spec(example)
154
+ AppMap.info 'Configuring AppMap recorder for RSpec' if first_recording?
155
+ @recording_count += 1
156
+
151
157
  @recordings_by_example[example.object_id] = Recording.new(example)
152
158
  end
153
159
 
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AppMap
4
+ module Service
5
+ class Guesser
6
+ POSSIBLE_PATHS = %w[app/controllers app/models lib]
7
+ class << self
8
+ def guess_name
9
+ reponame = lambda do
10
+ next unless File.directory?('.git')
11
+
12
+ repo_name = `git config --get remote.origin.url`.strip
13
+ repo_name.split('/').last.split('.').first unless repo_name == ''
14
+ end
15
+ dirname = -> { Dir.pwd.split('/').last }
16
+
17
+ reponame.() || dirname.()
18
+ end
19
+
20
+ def guess_paths
21
+ POSSIBLE_PATHS.select { |path| File.directory?(path) }
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end