appmap 0.102.2 → 0.103.0

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