appmap 0.102.2 → 0.103.0

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.
@@ -1,4 +1,4 @@
1
- require_relative './recording_methods'
1
+ require_relative "recording_methods"
2
2
 
3
3
  module AppMap
4
4
  # Detects whether AppMap recording should be enabled. This test can be performed generally, or for
@@ -9,27 +9,29 @@ module AppMap
9
9
  @@detected_for_method = {}
10
10
 
11
11
  class << self
12
+ # rubocop:disable Metrics/MethodLength
12
13
  def discourage_conflicting_recording_methods(recording_method)
13
- return if ENV['APPMAP_DISCOURAGE_CONFLICTING_RECORDING_METHODS'] == 'false'
14
+ return if ENV["APPMAP_DISCOURAGE_CONFLICTING_RECORDING_METHODS"] == "false"
14
15
 
15
16
  return unless enabled?(recording_method.to_sym) && enabled?(:requests)
16
17
 
17
18
  warn Util.color <<~MSG, :yellow
18
- AppMap recording is enabled for both 'requests' and '#{recording_method}'. This is not recommended
19
- because the recordings will contain duplicitive information, and in some case may conflict with each other.
19
+ AppMap recording is enabled for both 'requests' and '#{recording_method}'. This is not recommended
20
+ because the recordings will contain duplicitive information, and in some case may conflict with each other.
20
21
  MSG
21
22
 
22
- return unless ENV['APPMAP'] == 'true'
23
+ return unless ENV["APPMAP"] == "true"
23
24
 
24
25
  warn Util.color <<~MSG, :yellow
25
- The environment contains APPMAP=true, which is not recommended in this application environment because
26
- it enables all recording methods. Consider letting AppMap detect the appropriate recording method,
27
- or explicitly enabling only the recording methods you want to use using environment variables like
28
- APPMAP_RECORD_REQUESTS, APPMAP_RECORD_RSPEC, etc.
29
-
30
- See https://appmap.io/docs/reference/appmap-ruby.html#advanced-runtime-options for more information.
26
+ The environment contains APPMAP=true, which is not recommended in this application environment because
27
+ it enables all recording methods. Consider letting AppMap detect the appropriate recording method,
28
+ or explicitly enabling only the recording methods you want to use using environment variables like
29
+ APPMAP_RECORD_REQUESTS, APPMAP_RECORD_RSPEC, etc.
30
+
31
+ See https://appmap.io/docs/reference/appmap-ruby.html#advanced-runtime-options for more information.
31
32
  MSG
32
33
  end
34
+ # rubocop:enable Metrics/MethodLength
33
35
 
34
36
  def enabled?(recording_method)
35
37
  new(recording_method).enabled?
@@ -57,7 +59,7 @@ See https://appmap.io/docs/reference/appmap-ruby.html#advanced-runtime-options f
57
59
 
58
60
  if @recording_method && (enabled && enabled_by_app_env?)
59
61
  warn AppMap::Util.color(
60
- "AppMap #{@recording_method.nil? ? '' : "#{@recording_method} "}recording is enabled because #{message}", :magenta
62
+ "AppMap #{@recording_method.nil? ? "" : "#{@recording_method} "}recording is enabled because #{message}", :magenta
61
63
  )
62
64
  end
63
65
 
@@ -77,63 +79,63 @@ See https://appmap.io/docs/reference/appmap-ruby.html#advanced-runtime-options f
77
79
  message, enabled = []
78
80
  message, enabled = method(detection_functions.shift).call while enabled.nil? && !detection_functions.empty?
79
81
 
80
- return [ 'it is not enabled by any configuration or framework', false, false ] if enabled.nil?
82
+ return ["it is not enabled by any configuration or framework", false, false] if enabled.nil?
81
83
 
82
84
  _, enabled_by_env = enabled_by_app_env?
83
- [ message, enabled, enabled_by_env ]
85
+ [message, enabled, enabled_by_env]
84
86
  end
85
87
 
86
88
  def enabled_by_testing?
87
89
  return unless %i[rspec minitest cucumber].member?(@recording_method)
88
90
 
89
- [ "running tests with #{@recording_method}", true ]
91
+ ["running tests with #{@recording_method}", true]
90
92
  end
91
93
 
92
94
  def enabled_by_app_env?
93
95
  env_name, app_env = detect_app_env
94
- return [ "#{env_name} is '#{app_env}'", true ] if @recording_method.nil? && %w[test development].member?(app_env)
96
+ return ["#{env_name} is '#{app_env}'", true] if @recording_method.nil? && %w[test development].member?(app_env)
95
97
 
96
98
  return unless %i[remote requests].member?(@recording_method)
97
- return [ "#{env_name} is '#{app_env}'", true ] if app_env == 'development'
99
+ ["#{env_name} is '#{app_env}'", true] if app_env == "development"
98
100
  end
99
101
 
100
102
  def detect_app_env
101
103
  if rails_env
102
- [ 'RAILS_ENV', rails_env ]
103
- elsif ENV['APP_ENV']
104
- [ 'APP_ENV', ENV['APP_ENV']]
104
+ ["RAILS_ENV", rails_env]
105
+ elsif ENV["APP_ENV"]
106
+ ["APP_ENV", ENV["APP_ENV"]]
105
107
  end
106
108
  end
107
109
 
108
110
  def globally_enabled?
109
111
  # Don't auto-enable request recording in the 'test' environment, because users probably don't want
110
112
  # AppMaps of both test cases and requests. Requests recording can always be enabled by APPMAP_RECORD_REQUESTS=true.
111
- requests_recording_in_test = -> { [ :requests ].member?(@recording_method) && detect_app_env == 'test' }
112
- [ 'APPMAP=true', true ] if ENV['APPMAP'] == 'true' && !requests_recording_in_test.call
113
+ requests_recording_in_test = -> { [:requests].member?(@recording_method) && detect_app_env == "test" }
114
+ ["APPMAP=true", true] if ENV["APPMAP"] == "true" && !requests_recording_in_test.call
113
115
  end
114
116
 
115
117
  def globally_disabled?
116
- [ 'APPMAP=false', false ] if ENV['APPMAP'] == 'false'
118
+ ["APPMAP=false", false] if ENV["APPMAP"] == "false"
117
119
  end
118
120
 
119
121
  def recording_method_disabled?
120
122
  return false unless @recording_method
121
123
 
122
- env_var = [ 'APPMAP', 'RECORD', @recording_method.upcase ].join('_')
123
- [ "#{[ 'APPMAP', 'RECORD', @recording_method.upcase ].join('_')}=false", false ] if ENV[env_var] == 'false'
124
+ env_var = ["APPMAP", "RECORD", @recording_method.upcase].join("_")
125
+ ["#{["APPMAP", "RECORD", @recording_method.upcase].join("_")}=false", false] if ENV[env_var] == "false"
124
126
  end
125
127
 
126
128
  def recording_method_enabled?
127
129
  return false unless @recording_method
128
130
 
129
- env_var = [ 'APPMAP', 'RECORD', @recording_method.upcase ].join('_')
130
- [ "#{[ 'APPMAP', 'RECORD', @recording_method.upcase ].join('_')}=true", true ] if ENV[env_var] == 'true'
131
+ env_var = ["APPMAP", "RECORD", @recording_method.upcase].join("_")
132
+ ["#{["APPMAP", "RECORD", @recording_method.upcase].join("_")}=true", true] if ENV[env_var] == "true"
131
133
  end
132
134
 
133
135
  def rails_env
134
136
  return Rails.env if defined?(::Rails::Railtie)
135
137
 
136
- ENV.fetch('RAILS_ENV', nil)
138
+ ENV.fetch("RAILS_ENV", nil)
137
139
  end
138
140
  end
139
141
  end
@@ -1,6 +1,5 @@
1
1
  - method: ActiveJob::Enqueuing#enqueue
2
2
  label: job.create
3
3
  - methods:
4
- - ActiveJob::Execution#perform
5
4
  - ActiveJob::Execution#perform_now
6
5
  label: job.perform
@@ -1,16 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'appmap/util'
3
+ require "appmap/util"
4
+ require_relative "../record_around"
4
5
 
5
- def ruby2_keywords(*); end unless respond_to?(:ruby2_keywords, true)
6
+ unless respond_to?(:ruby2_keywords, true)
7
+ def ruby2_keywords(*)
8
+ end
9
+ end
6
10
 
7
11
  module AppMap
8
12
  class Hook
9
13
  # Delegation methods for Ruby 2.
10
14
  # cf. https://eregon.me/blog/2019/11/10/the-delegation-challenge-of-ruby27.html
11
15
  class Method
16
+ include RecordAround
17
+
12
18
  ruby2_keywords def call(receiver, *args, &block)
13
19
  call_event = false
20
+ record_around_before
14
21
  if trace?
15
22
  call_event, elapsed_before = with_disabled_hook { before_hook receiver, *args }
16
23
  end
@@ -22,7 +29,7 @@ module AppMap
22
29
  protected
23
30
 
24
31
  def before_hook(receiver, *args)
25
- before_hook_start_time = AppMap::Util.gettime()
32
+ before_hook_start_time = AppMap::Util.gettime
26
33
  call_event = handle_call(receiver, args)
27
34
  if call_event
28
35
  AppMap.tracing.record_event \
@@ -31,10 +38,11 @@ module AppMap
31
38
  defined_class: defined_class,
32
39
  method: hook_method
33
40
  end
34
- [call_event, AppMap::Util.gettime() - before_hook_start_time]
41
+ [call_event, AppMap::Util.gettime - before_hook_start_time]
35
42
  end
36
43
 
37
44
  ruby2_keywords def do_call(receiver, *args, &block)
45
+ # Do not allow this to change to bind_call, it's not defined for Ruby 2.
38
46
  hook_method.bind(receiver).call(*args, &block)
39
47
  end
40
48
 
@@ -42,16 +50,19 @@ module AppMap
42
50
  ruby2_keywords def trace_call(call_event, elapsed_before, receiver, *args, &block)
43
51
  return do_call(receiver, *args, &block) unless call_event
44
52
 
45
- start_time = AppMap::Util.gettime()
53
+ start_time = AppMap::Util.gettime
46
54
  begin
47
55
  return_value = do_call(receiver, *args, &block)
48
56
  rescue # rubocop:disable Style/RescueStandardError
49
57
  exception = $ERROR_INFO
50
58
  raise
51
59
  ensure
52
- after_start_time = AppMap::Util.gettime()
53
- with_disabled_hook { after_hook receiver, call_event, elapsed_before, after_start_time - start_time, after_start_time, return_value, exception } \
54
- if call_event
60
+ after_start_time = AppMap::Util.gettime
61
+ begin
62
+ with_disabled_hook { after_hook receiver, call_event, elapsed_before, after_start_time - start_time, after_start_time, return_value, exception }
63
+ ensure
64
+ record_around_after
65
+ end
55
66
  end
56
67
  end
57
68
  # rubocop:enable Metrics/MethodLength
@@ -1,17 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'appmap/util'
3
+ require "appmap/util"
4
+ require_relative "../record_around"
4
5
 
5
6
  module AppMap
6
7
  class Hook
7
8
  # Delegation methods for Ruby 3.
8
9
  class Method
10
+ include RecordAround
11
+
9
12
  def call(receiver, *args, **kwargs, &block)
10
13
  call_event = false
14
+ record_around_before
11
15
  if trace?
12
16
  call_event, elapsed_before = with_disabled_hook { before_hook receiver, *args, **kwargs }
13
17
  end
14
- # note we can't short-circuit directly to do_call because then the call stack
18
+ # NOTE: we can't short-circuit directly to do_call because then the call stack
15
19
  # depth changes and eval handler doesn't work correctly
16
20
  trace_call call_event, elapsed_before, receiver, *args, **kwargs, &block
17
21
  end
@@ -19,7 +23,7 @@ module AppMap
19
23
  protected
20
24
 
21
25
  def before_hook(receiver, *args, **kwargs)
22
- before_hook_start_time = AppMap::Util.gettime()
26
+ before_hook_start_time = AppMap::Util.gettime
23
27
  args = [*args, kwargs] if !kwargs.empty? || keyrest?
24
28
  call_event = handle_call(receiver, args)
25
29
  if call_event
@@ -29,31 +33,34 @@ module AppMap
29
33
  defined_class: defined_class,
30
34
  method: hook_method
31
35
  end
32
- [call_event, AppMap::Util.gettime() - before_hook_start_time]
36
+ [call_event, AppMap::Util.gettime - before_hook_start_time]
33
37
  end
34
38
 
35
39
  def keyrest?
36
40
  @keyrest ||= parameters.map(&:last).include? :keyrest
37
41
  end
38
42
 
39
- def do_call(receiver, *args, **kwargs, &block)
40
- hook_method.bind_call(receiver, *args, **kwargs, &block)
43
+ def do_call(receiver, ...)
44
+ hook_method.bind_call(receiver, ...)
41
45
  end
42
46
 
43
47
  # rubocop:disable Metrics/MethodLength
44
- def trace_call(call_event, elapsed_before, receiver, *args, **kwargs, &block)
45
- return do_call(receiver, *args, **kwargs, &block) unless call_event
48
+ def trace_call(call_event, elapsed_before, receiver, ...)
49
+ return do_call(receiver, ...) unless call_event
46
50
 
47
- start_time = AppMap::Util.gettime()
51
+ start_time = AppMap::Util.gettime
48
52
  begin
49
- return_value = do_call(receiver, *args, **kwargs, &block)
53
+ return_value = do_call(receiver, ...)
50
54
  rescue # rubocop:disable Style/RescueStandardError
51
55
  exception = $ERROR_INFO
52
56
  raise
53
57
  ensure
54
- after_start_time = AppMap::Util.gettime()
55
- with_disabled_hook { after_hook receiver, call_event, elapsed_before, after_start_time - start_time, after_start_time, return_value, exception } \
56
- if call_event
58
+ after_start_time = AppMap::Util.gettime
59
+ begin
60
+ with_disabled_hook { after_hook receiver, call_event, elapsed_before, after_start_time - start_time, after_start_time, return_value, exception }
61
+ ensure
62
+ record_around_after
63
+ end
57
64
  end
58
65
  end
59
66
  # rubocop:enable Metrics/MethodLength
@@ -1,17 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'appmap/util'
3
+ require "appmap/util"
4
4
 
5
5
  module AppMap
6
6
  class Hook
7
7
  class << self
8
8
  def method_hash_key(cls, method)
9
- [ cls, method.name ].hash
9
+ [cls, method.name].hash
10
10
  rescue TypeError => e
11
11
  warn "Error building hash key for #{cls}, #{method}: #{e}"
12
12
  end
13
13
  end
14
-
15
14
 
16
15
  SIGNATURES = {}
17
16
  LOOKUP_SIGNATURE = lambda do |id|
@@ -32,19 +31,20 @@ module AppMap
32
31
  method
33
32
  end
34
33
 
35
- RUBY_MAJOR_VERSION, RUBY_MINOR_VERSION, _ = RUBY_VERSION.split('.').map(&:to_i)
34
+ RUBY_MAJOR_VERSION, RUBY_MINOR_VERSION, _ = RUBY_VERSION.split(".").map(&:to_i)
36
35
 
37
36
  # Single hooked method.
38
37
  # Call #activate to override the original.
39
38
  class Method
40
- attr_reader :hook_package, :hook_class, :hook_method, :parameters, :arity
39
+ attr_reader :hook_package, :hook_class, :hook_method, :record_around, :parameters, :arity
41
40
 
42
- HOOK_DISABLE_KEY = 'AppMap::Hook.disable'
41
+ HOOK_DISABLE_KEY = "AppMap::Hook.disable"
43
42
 
44
- def initialize(hook_package, hook_class, hook_method)
43
+ def initialize(hook_package, hook_class, hook_method, record_around: false)
45
44
  @hook_package = hook_package
46
45
  @hook_class = hook_class
47
46
  @hook_method = hook_method
47
+ @record_around = record_around
48
48
  @parameters = hook_method.parameters
49
49
  @arity = hook_method.arity
50
50
  end
@@ -52,11 +52,11 @@ module AppMap
52
52
  def activate
53
53
  if HookLog.enabled?
54
54
  msg = if method_display_name
55
- "#{method_display_name}"
56
- else
57
- "#{hook_method.name} (class resolution deferred)"
58
- end
59
- HookLog.log "Hooking #{msg} at line #{(hook_method.source_location || []).join(':')}"
55
+ "#{method_display_name}"
56
+ else
57
+ "#{hook_method.name} (class resolution deferred)"
58
+ end
59
+ HookLog.log "Hooking #{msg} at line #{(hook_method.source_location || []).join(":")}"
60
60
  end
61
61
 
62
62
  hook_method_parameters = hook_method.parameters.dup.freeze
@@ -79,13 +79,13 @@ module AppMap
79
79
 
80
80
  def defining_class(hook_class)
81
81
  cls = if RUBY_MAJOR_VERSION == 2 && RUBY_MINOR_VERSION <= 5
82
- hook_class
83
- .ancestors
84
- .select { |cls| cls.method_defined?(hook_method.name) }
85
- .find { |cls| cls.instance_method(hook_method.name).owner == cls }
86
- else
87
- hook_class.ancestors.find { |cls| cls.method_defined?(hook_method.name, false) }
88
- end
82
+ hook_class
83
+ .ancestors
84
+ .select { |cls| cls.method_defined?(hook_method.name) }
85
+ .find { |cls| cls.instance_method(hook_method.name).owner == cls }
86
+ else
87
+ hook_class.ancestors.find { |cls| cls.method_defined?(hook_method.name, false) }
88
+ end
89
89
 
90
90
  return cls if cls
91
91
 
@@ -93,7 +93,7 @@ module AppMap
93
93
  end
94
94
 
95
95
  def trace?
96
- return false unless AppMap.tracing_enabled?
96
+ return false unless AppMap.tracing_enabled?(thread: Thread.current)
97
97
  return false if Thread.current[HOOK_DISABLE_KEY]
98
98
  return false if hook_package&.shallow? && AppMap.tracing.last_package_for_current_thread == hook_package
99
99
 
@@ -103,7 +103,7 @@ module AppMap
103
103
  def method_display_name
104
104
  return @method_display_name if @method_display_name
105
105
 
106
- return @method_display_name = [defined_class, '#', hook_method.name].join if defined_class
106
+ return @method_display_name = [defined_class, "#", hook_method.name].join if defined_class
107
107
 
108
108
  "#{hook_method.name} (class resolution deferred)"
109
109
  end
@@ -114,7 +114,7 @@ module AppMap
114
114
 
115
115
  def after_hook(_receiver, call_event, elapsed_before, elapsed, after_start_time, return_value, exception)
116
116
  return_event = handle_return(call_event.id, elapsed, return_value, exception)
117
- return_event.elapsed_instrumentation = elapsed_before + (AppMap::Util.gettime() - after_start_time)
117
+ return_event.elapsed_instrumentation = elapsed_before + (AppMap::Util.gettime - after_start_time)
118
118
  AppMap.tracing.record_event(return_event) if return_event
119
119
  end
120
120
 
@@ -141,20 +141,20 @@ module AppMap
141
141
  end
142
142
  end
143
143
 
144
- unless ENV['APPMAP_NO_PATCH_OBJECT'] == 'true'
144
+ unless ENV["APPMAP_NO_PATCH_OBJECT"] == "true"
145
145
  class Object
146
146
  prepend AppMap::ObjectMethods
147
147
  end
148
148
  end
149
149
 
150
- unless ENV['APPMAP_NO_PATCH_MODULE'] == 'true'
150
+ unless ENV["APPMAP_NO_PATCH_MODULE"] == "true"
151
151
  class Module
152
152
  prepend AppMap::ModuleMethods
153
153
  end
154
154
  end
155
155
 
156
- if RUBY_VERSION < '3'
157
- require 'appmap/hook/method/ruby2'
156
+ if RUBY_VERSION < "3"
157
+ require "appmap/hook/method/ruby2"
158
158
  else
159
- require 'appmap/hook/method/ruby3'
159
+ require "appmap/hook/method/ruby3"
160
160
  end
@@ -0,0 +1,77 @@
1
+ module AppMap
2
+ class Hook
3
+ # Start and stop a recording around a Hook::Method.
4
+ module RecordAround
5
+ APPMAP_OUTPUT_DIR = File.join(AppMap.output_dir, "requests")
6
+
7
+ # Context for a recording.
8
+ class Context
9
+ attr_reader :hook_method
10
+
11
+ def initialize(hook_method)
12
+ @hook_method = hook_method
13
+ @start_time = DateTime.now
14
+ @tracer = AppMap.tracing.trace(thread: Thread.current)
15
+ end
16
+
17
+ # Finish recording the AppMap by collecting the events and classMap and writing the output file.
18
+ # rubocop:disable Metrics/MethodLength
19
+ # rubocop:disable Metrics/AbcSize
20
+ def finish
21
+ return unless @tracer
22
+
23
+ tracer = @tracer
24
+ @tracer = nil
25
+ AppMap.tracing.delete(tracer)
26
+
27
+ events = tracer.events.map(&:to_h)
28
+
29
+ timestamp = DateTime.now
30
+ appmap_name = "#{hook_method.name} (#{Thread.current.object_id}) - #{timestamp.strftime("%T.%L")}"
31
+ appmap_file_name = AppMap::Util.scenario_filename([timestamp.to_f, hook_method.name, Thread.current.object_id].join("_"))
32
+
33
+ metadata = AppMap.detect_metadata.tap do |metadata|
34
+ metadata[:name] = appmap_name
35
+ metadata[:source_location] = hook_method.source_location
36
+ metadata[:recorder] = {
37
+ name: "command",
38
+ type: "requests"
39
+ }
40
+ end
41
+
42
+ appmap = {
43
+ version: AppMap::APPMAP_FORMAT_VERSION,
44
+ classMap: AppMap.class_map(tracer.event_methods),
45
+ metadata: metadata,
46
+ events: events
47
+ }
48
+
49
+ AppMap::Util.write_appmap(File.join(APPMAP_OUTPUT_DIR, appmap_file_name), appmap)
50
+ end
51
+ # rubocop:enable Metrics/MethodLength
52
+ # rubocop:enable Metrics/AbcSize
53
+ end
54
+
55
+ # If requests recording is enabled, and we encounter a method which should always be recorded
56
+ # when requests recording is on, and there is no other recording in progress, then start a
57
+ # new recording and end it when the method returns.
58
+ def record_around?
59
+ (record_around && AppMap.recording_enabled?(:requests) && !AppMap.tracing_enabled?(thread: Thread.current))
60
+ end
61
+
62
+ def record_around_before
63
+ return unless record_around?
64
+
65
+ @record_around_context = Context.new(hook_method)
66
+ end
67
+
68
+ def record_around_after
69
+ return unless @record_around_context
70
+
71
+ context = @record_around_context
72
+ @record_around_context = nil
73
+ context.finish
74
+ end
75
+ end
76
+ end
77
+ end