appmap 0.66.1 → 0.68.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +2 -0
  3. data/CHANGELOG.md +45 -0
  4. data/lib/appmap/builtin_hooks/json.yml +4 -0
  5. data/lib/appmap/builtin_hooks/net/http.yml +3 -0
  6. data/lib/appmap/builtin_hooks/openssl.yml +16 -0
  7. data/lib/appmap/builtin_hooks/yaml.yml +10 -0
  8. data/lib/appmap/command/agent_setup/validate.rb +8 -1
  9. data/lib/appmap/command/inspect.rb +0 -1
  10. data/lib/appmap/config.rb +157 -137
  11. data/lib/appmap/depends/api.rb +1 -1
  12. data/lib/appmap/depends/configuration.rb +0 -2
  13. data/lib/appmap/depends/util.rb +1 -1
  14. data/lib/appmap/event.rb +6 -2
  15. data/lib/appmap/gem_hooks/actionpack.yml +40 -0
  16. data/lib/appmap/gem_hooks/actionview.yml +13 -0
  17. data/lib/appmap/gem_hooks/activesupport.yml +12 -0
  18. data/lib/appmap/gem_hooks/cancancan.yml +6 -0
  19. data/lib/appmap/handler/net_http.rb +14 -12
  20. data/lib/appmap/handler/rails/request_handler.rb +4 -10
  21. data/lib/appmap/hook.rb +46 -30
  22. data/lib/appmap/minitest.rb +1 -0
  23. data/lib/appmap/rspec.rb +1 -0
  24. data/lib/appmap/swagger/configuration.rb +2 -0
  25. data/lib/appmap/util.rb +20 -2
  26. data/lib/appmap/version.rb +1 -1
  27. data/lib/appmap.rb +2 -2
  28. data/spec/config_spec.rb +207 -65
  29. data/spec/depends/api_spec.rb +3 -3
  30. data/spec/fixtures/config/missing_path_or_gem.yml +3 -0
  31. data/spec/rails_recording_spec.rb +1 -1
  32. data/spec/rails_spec_helper.rb +6 -1
  33. data/test/agent_setup_validate_test.rb +35 -8
  34. data/test/fixtures/mocha_mock_app/Gemfile +5 -0
  35. data/test/fixtures/mocha_mock_app/appmap.yml +5 -0
  36. data/test/fixtures/mocha_mock_app/lib/sheep.rb +5 -0
  37. data/test/fixtures/mocha_mock_app/test/sheep_test.rb +18 -0
  38. data/test/mock_compatibility_test.rb +45 -0
  39. metadata +17 -3
@@ -0,0 +1,40 @@
1
+ - methods:
2
+ - ActionDispatch::Request::Session#[]
3
+ - ActionDispatch::Request::Session#dig
4
+ - ActionDispatch::Request::Session#values
5
+ - ActionDispatch::Request::Session#fetch
6
+ - ActionDispatch::Cookies::CookieJar#[]
7
+ - ActionDispatch::Cookies::CookieJar#fetch
8
+ label: http.session.read
9
+ require_name: action_dispatch
10
+ - methods:
11
+ - ActionDispatch::Request::Session#destroy
12
+ - ActionDispatch::Request::Session#[]=
13
+ - ActionDispatch::Request::Session#clear
14
+ - ActionDispatch::Request::Session#update
15
+ - ActionDispatch::Request::Session#delete
16
+ - ActionDispatch::Request::Session#merge
17
+ - ActionDispatch::Cookies::CookieJar#[]=
18
+ - ActionDispatch::Cookies::CookieJar#clear
19
+ - ActionDispatch::Cookies::CookieJar#update
20
+ - ActionDispatch::Cookies::CookieJar#delete
21
+ - ActionDispatch::Cookies::CookieJar#recycle!
22
+ label: http.session.write
23
+ require_name: action_dispatch
24
+ - methods:
25
+ - ActionDispatch::Cookies::EncryptedCookieJar#[]=
26
+ - ActionDispatch::Cookies::EncryptedCookieJar#clear
27
+ - ActionDispatch::Cookies::EncryptedCookieJar#update
28
+ - ActionDispatch::Cookies::EncryptedCookieJar#delete
29
+ - ActionDispatch::Cookies::EncryptedCookieJar#recycle
30
+ labels:
31
+ - http.cookie
32
+ - crypto.encrypt
33
+ require_name: action_dispatch
34
+ - methods:
35
+ - ActionController::Instrumentation#process_action
36
+ - ActionController::Instrumentation#send_file
37
+ - ActionController::Instrumentation#send_data
38
+ - ActionController::Instrumentation#redirect_to
39
+ label: mvc.controller
40
+ require_name: action_controller
@@ -0,0 +1,13 @@
1
+ - methods:
2
+ - ActionView::Renderer#render
3
+ - ActionView::TemplateRenderer#render
4
+ - ActionView::PartialRenderer#render
5
+ label: mvc.view
6
+ handler_class: AppMap::Handler::Rails::Template::RenderHandler
7
+ require_name: action_view
8
+ - methods:
9
+ - ActionView::Resolver#find_all
10
+ - ActionView::Resolver#find_all_anywhere
11
+ label: mvc.template.resolver
12
+ handler_class: AppMap::Handler::Rails::Template::ResolverHandler
13
+ require_name: action_view
@@ -0,0 +1,12 @@
1
+ - method: ActiveSupport::Callbacks::CallbackSequence#invoke_before
2
+ label: mvc.before_action
3
+ require_name: active_support
4
+ force: true
5
+ - method: ActiveSupport::Callbacks::CallbackSequence#invoke_after
6
+ label: mvc.after_action
7
+ require_name: active_support
8
+ force: true
9
+ - method: ActiveSupport::SecurityUtils#secure_compare
10
+ label: crypto.secure_compare
11
+ require_name: active_support/security_utils
12
+ force: true
@@ -0,0 +1,6 @@
1
+ - methods:
2
+ - CanCan::ControllerAdditions#authorize!
3
+ - CanCan::ControllerAdditions#can?
4
+ - CanCan::ControllerAdditions#cannot?
5
+ - CanCan::Ability#authorize?
6
+ label: security.authorization
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'appmap/event'
4
4
  require 'appmap/util'
5
+ require 'rack'
5
6
 
6
7
  module AppMap
7
8
  module Handler
@@ -27,7 +28,7 @@ module AppMap
27
28
 
28
29
  self.request_method = request.method
29
30
  self.url = url
30
- self.headers = AppMap::Util.select_headers(NetHTTP.request_headers(request))
31
+ self.headers = NetHTTP.copy_headers(request)
31
32
  self.params = Rack::Utils.parse_nested_query(query)
32
33
  end
33
34
 
@@ -55,22 +56,25 @@ module AppMap
55
56
  end
56
57
 
57
58
  class HTTPClientResponse < AppMap::Event::MethodReturnIgnoreValue
58
- attr_accessor :status, :mime_type, :headers
59
+ attr_accessor :status, :headers
59
60
 
60
61
  def initialize(response, parent_id, elapsed)
61
62
  super AppMap::Event.next_id_counter, :return, Thread.current.object_id
62
63
 
63
- self.status = response.code.to_i
64
+ if response
65
+ self.status = response.code.to_i
66
+ self.headers = NetHTTP.copy_headers(response)
67
+ else
68
+ self.headers = {}
69
+ end
64
70
  self.parent_id = parent_id
65
71
  self.elapsed = elapsed
66
- self.headers = AppMap::Util.select_headers(NetHTTP.response_headers(response))
67
72
  end
68
73
 
69
74
  def to_h
70
75
  super.tap do |h|
71
76
  h[:http_client_response] = {
72
77
  status_code: status,
73
- mime_type: mime_type,
74
78
  headers: headers
75
79
  }.compact
76
80
  end
@@ -79,17 +83,15 @@ module AppMap
79
83
 
80
84
  class NetHTTP
81
85
  class << self
82
- def request_headers(request)
86
+ def copy_headers(obj)
83
87
  {}.tap do |headers|
84
- request.each_header do |k,v|
85
- key = [ 'HTTP', Util.underscore(k).upcase ].join('_')
86
- headers[key] = v
88
+ obj.each_header do |key, value|
89
+ key = key.split('-').map(&:capitalize).join('-')
90
+ headers[key] = value
87
91
  end
88
92
  end
89
93
  end
90
-
91
- alias response_headers request_headers
92
-
94
+
93
95
  def handle_call(defined_class, hook_method, receiver, args)
94
96
  # request will call itself again in a start block if it's not already started.
95
97
  return unless receiver.started?
@@ -9,16 +9,14 @@ module AppMap
9
9
  module Rails
10
10
  module RequestHandler
11
11
  class HTTPServerRequest < AppMap::Event::MethodEvent
12
- attr_accessor :normalized_path_info, :request_method, :path_info, :params, :mime_type, :headers, :authorization
12
+ attr_accessor :normalized_path_info, :request_method, :path_info, :params, :headers
13
13
 
14
14
  def initialize(request)
15
15
  super AppMap::Event.next_id_counter, :call, Thread.current.object_id
16
16
 
17
17
  self.request_method = request.request_method
18
18
  self.normalized_path_info = normalized_path(request)
19
- self.mime_type = request.headers['Content-Type']
20
- self.headers = AppMap::Util.select_headers(request.env)
21
- self.authorization = request.headers['Authorization']
19
+ self.headers = AppMap::Util.select_rack_headers(request.env)
22
20
  self.path_info = request.path_info.split('?')[0]
23
21
  # ActionDispatch::Http::ParameterFilter is deprecated
24
22
  parameter_filter_cls = \
@@ -35,9 +33,7 @@ module AppMap
35
33
  h[:http_server_request] = {
36
34
  request_method: request_method,
37
35
  path_info: path_info,
38
- mime_type: mime_type,
39
36
  normalized_path_info: normalized_path_info,
40
- authorization: authorization,
41
37
  headers: headers,
42
38
  }.compact
43
39
 
@@ -72,23 +68,21 @@ module AppMap
72
68
  end
73
69
 
74
70
  class HTTPServerResponse < AppMap::Event::MethodReturnIgnoreValue
75
- attr_accessor :status, :mime_type, :headers
71
+ attr_accessor :status, :headers
76
72
 
77
73
  def initialize(response, parent_id, elapsed)
78
74
  super AppMap::Event.next_id_counter, :return, Thread.current.object_id
79
75
 
80
76
  self.status = response.status
81
- self.mime_type = response.headers['Content-Type']
82
77
  self.parent_id = parent_id
83
78
  self.elapsed = elapsed
84
- self.headers = AppMap::Util.select_headers(response.headers)
79
+ self.headers = response.headers.dup
85
80
  end
86
81
 
87
82
  def to_h
88
83
  super.tap do |h|
89
84
  h[:http_server_response] = {
90
85
  status_code: status,
91
- mime_type: mime_type,
92
86
  headers: headers
93
87
  }.compact
94
88
  end
data/lib/appmap/hook.rb CHANGED
@@ -15,10 +15,23 @@ module AppMap
15
15
  @method_arity = ::Method.instance_method(:arity)
16
16
 
17
17
  class << self
18
- def lock_builtins
19
- return if @builtins_hooked
18
+ def hook_builtins?
19
+ Mutex.new.synchronize do
20
+ @hook_builtins = true if @hook_builtins.nil?
20
21
 
21
- @builtins_hooked = true
22
+ return false unless @hook_builtins
23
+
24
+ @hook_builtins = false
25
+ true
26
+ end
27
+ end
28
+
29
+ def already_hooked?(method)
30
+ # After a method is defined, the statement "module_function <the-method>" can convert that method
31
+ # into a module (class) method. The method is hooked first when it's defined, then AppMap will attempt to
32
+ # hook it again when it's redefined as a module method. So we check the method source location - if it's
33
+ # part of the AppMap source tree, we ignore it.
34
+ method.source_location && method.source_location[0].index(__dir__) == 0
22
35
  end
23
36
 
24
37
  # Return the class, separator ('.' or '#'), and method name for
@@ -79,42 +92,43 @@ module AppMap
79
92
  # hook_builtins builds hooks for code that is built in to the Ruby standard library.
80
93
  # No TracePoint events are emitted for builtins, so a separate hooking mechanism is needed.
81
94
  def hook_builtins
82
- return unless self.class.lock_builtins
95
+ return unless self.class.hook_builtins?
83
96
 
84
- class_from_string = lambda do |fq_class|
85
- fq_class.split('::').inject(Object) do |mod, class_name|
86
- mod.const_get(class_name)
87
- end
88
- end
97
+ hook_loaded_code = lambda do |hooks_by_class, builtin|
98
+ hooks_by_class.each do |class_name, hooks|
99
+ Array(hooks).each do |hook|
100
+ require hook.package.require_name if builtin && hook.package.require_name && hook.package.require_name != 'ruby'
89
101
 
90
- config.builtin_hooks.each do |class_name, hooks|
91
- Array(hooks).each do |hook|
92
- require hook.package.package_name if hook.package.package_name && hook.package.package_name != 'ruby'
93
- Array(hook.method_names).each do |method_name|
94
- method_name = method_name.to_sym
95
- base_cls = class_from_string.(class_name)
102
+ Array(hook.method_names).each do |method_name|
103
+ method_name = method_name.to_sym
104
+ base_cls = Util::class_from_string(class_name, must: false)
105
+ next unless base_cls
96
106
 
97
- hook_method = lambda do |entry|
98
- cls, method = entry
99
- return false if config.never_hook?(cls, method)
107
+ hook_method = lambda do |entry|
108
+ cls, method = entry
109
+ return false if config.never_hook?(cls, method)
100
110
 
101
- Hook::Method.new(hook.package, cls, method).activate
102
- end
111
+ Hook::Method.new(hook.package, cls, method).activate
112
+ end
103
113
 
104
- methods = []
105
- methods << [ base_cls, base_cls.public_instance_method(method_name) ] rescue nil
106
- if base_cls.respond_to?(:singleton_class)
107
- methods << [ base_cls.singleton_class, base_cls.singleton_class.public_instance_method(method_name) ] rescue nil
108
- end
109
- methods.compact!
110
- if methods.empty?
111
- warn "Method #{method_name} not found on #{base_cls.name}"
112
- else
113
- methods.each(&hook_method)
114
+ methods = []
115
+ methods << [ base_cls, base_cls.public_instance_method(method_name) ] rescue nil
116
+ if base_cls.respond_to?(:singleton_class)
117
+ methods << [ base_cls.singleton_class, base_cls.singleton_class.public_instance_method(method_name) ] rescue nil
118
+ end
119
+ methods.compact!
120
+ if methods.empty?
121
+ warn "Method #{method_name} not found on #{base_cls.name}" if LOG
122
+ else
123
+ methods.each(&hook_method)
124
+ end
114
125
  end
115
126
  end
116
127
  end
117
128
  end
129
+
130
+ hook_loaded_code.(config.builtin_hooks, true)
131
+ hook_loaded_code.(config.gem_hooks, false)
118
132
  end
119
133
 
120
134
  protected
@@ -165,6 +179,8 @@ module AppMap
165
179
  next
166
180
  end
167
181
 
182
+ next if self.class.already_hooked?(method)
183
+
168
184
  warn "AppMap: Examining #{hook_cls} #{method.name}" if LOG
169
185
 
170
186
  disasm = RubyVM::InstructionSequence.disasm(method)
@@ -142,6 +142,7 @@ if AppMap::Minitest.enabled?
142
142
  alias run_without_hook run
143
143
 
144
144
  def run
145
+ GC.start
145
146
  AppMap::Minitest.begin_test self, name
146
147
  begin
147
148
  run_without_hook
data/lib/appmap/rspec.rb CHANGED
@@ -87,6 +87,7 @@ module AppMap
87
87
  end
88
88
 
89
89
  warn "Starting recording of example #{example}@#{source_location}" if AppMap::RSpec::LOG
90
+ GC.start
90
91
  @trace = AppMap.tracing.trace
91
92
  @webdriver_port = webdriver_port.()
92
93
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'yaml'
2
4
 
3
5
  module AppMap
data/lib/appmap/util.rb CHANGED
@@ -21,6 +21,14 @@ module AppMap
21
21
  WHITE = "\e[37m"
22
22
 
23
23
  class << self
24
+ def class_from_string(fq_class, must: true)
25
+ fq_class.split('::').inject(Object) do |mod, class_name|
26
+ mod.const_get(class_name)
27
+ end
28
+ rescue NameError
29
+ raise if must
30
+ end
31
+
24
32
  def parse_function_name(name)
25
33
  package_tokens = name.split('/')
26
34
 
@@ -108,8 +116,18 @@ module AppMap
108
116
  event
109
117
  end
110
118
 
111
- def select_headers(env)
119
+ def select_rack_headers(env)
120
+ finalize_headers = lambda do |headers|
121
+ blank?(headers) ? nil : headers
122
+ end
123
+
112
124
  # Rack prepends HTTP_ to all client-sent headers.
125
+
126
+ if !env['rack.version']
127
+ warn "Request headers does not contain rack.version. HTTP_ prefix is not expected."
128
+ return finalize_headers.call(env.dup)
129
+ end
130
+
113
131
  matching_headers = env
114
132
  .select { |k,v| k.start_with? 'HTTP_'}
115
133
  .reject { |k,v| blank?(v) }
@@ -118,7 +136,7 @@ module AppMap
118
136
  value = kv[1]
119
137
  memo[key] = value
120
138
  end
121
- blank?(matching_headers) ? nil : matching_headers
139
+ finalize_headers.call(matching_headers)
122
140
  end
123
141
 
124
142
  def normalize_path(path)
@@ -3,7 +3,7 @@
3
3
  module AppMap
4
4
  URL = 'https://github.com/applandinc/appmap-ruby'
5
5
 
6
- VERSION = '0.66.1'
6
+ VERSION = '0.68.0'
7
7
 
8
8
  APPMAP_FORMAT_VERSION = '1.5.1'
9
9
 
data/lib/appmap.rb CHANGED
@@ -74,6 +74,6 @@ lambda do
74
74
  require 'appmap/depends'
75
75
  end
76
76
 
77
- end.call
77
+ end.call unless ENV['APPMAP_AUTOREQUIRE'] == 'false'
78
78
 
79
- AppMap.initialize_configuration if ENV['APPMAP'] == 'true'
79
+ AppMap.initialize_configuration if ENV['APPMAP'] == 'true' && ENV['APPMAP_INITIALIZE'] != 'false'