appmap 0.48.2 → 0.51.2

Sign up to get free protection for your applications and to get access to all the features.
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