appmap 0.37.2 → 0.41.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.
- checksums.yaml +4 -4
- data/.rubocop.yml +5 -1
- data/.travis.yml +2 -23
- data/CHANGELOG.md +23 -0
- data/CONTRIBUTING.md +22 -0
- data/README.md +102 -54
- data/Rakefile +3 -3
- data/lib/appmap/class_map.rb +25 -8
- data/lib/appmap/config.rb +54 -26
- data/lib/appmap/hook.rb +18 -3
- data/lib/appmap/hook/method.rb +18 -12
- data/lib/appmap/rails/request_handler.rb +17 -3
- data/lib/appmap/railtie.rb +1 -5
- data/lib/appmap/trace.rb +18 -7
- data/lib/appmap/version.rb +2 -2
- data/spec/abstract_controller_base_spec.rb +125 -64
- data/spec/config_spec.rb +1 -0
- data/spec/fixtures/hook/exclude.rb +15 -0
- data/spec/fixtures/hook/labels.rb +6 -0
- data/spec/fixtures/rails5_users_app/Gemfile +3 -4
- data/spec/fixtures/rails5_users_app/app/controllers/users_controller.rb +8 -0
- data/spec/fixtures/rails5_users_app/appmap.yml +5 -1
- data/spec/fixtures/rails5_users_app/config/application.rb +2 -0
- data/spec/fixtures/rails5_users_app/config/routes.rb +1 -1
- data/spec/fixtures/rails5_users_app/spec/controllers/users_controller_api_spec.rb +1 -1
- data/spec/fixtures/rails5_users_app/spec/controllers/users_controller_spec.rb +27 -0
- data/spec/fixtures/rails6_users_app/Gemfile +3 -4
- data/spec/fixtures/rails6_users_app/app/controllers/users_controller.rb +8 -0
- data/spec/fixtures/rails6_users_app/appmap.yml +6 -1
- data/spec/fixtures/rails6_users_app/config/application.rb +2 -0
- data/spec/fixtures/rails6_users_app/config/routes.rb +1 -1
- data/spec/fixtures/rails6_users_app/spec/controllers/users_controller_api_spec.rb +1 -1
- data/spec/fixtures/rails6_users_app/spec/controllers/users_controller_spec.rb +27 -0
- data/spec/hook_spec.rb +69 -47
- data/spec/rails_spec_helper.rb +2 -2
- data/spec/record_sql_rails_pg_spec.rb +1 -1
- data/spec/rspec_feature_metadata_spec.rb +1 -1
- data/spec/spec_helper.rb +1 -0
- metadata +7 -68
- data/spec/abstract_controller4_base_spec.rb +0 -66
- data/spec/fixtures/rails4_users_app/.gitignore +0 -13
- data/spec/fixtures/rails4_users_app/.rbenv-gemsets +0 -2
- data/spec/fixtures/rails4_users_app/.ruby-version +0 -1
- data/spec/fixtures/rails4_users_app/Dockerfile +0 -30
- data/spec/fixtures/rails4_users_app/Dockerfile.pg +0 -3
- data/spec/fixtures/rails4_users_app/Gemfile +0 -77
- data/spec/fixtures/rails4_users_app/README.rdoc +0 -28
- data/spec/fixtures/rails4_users_app/Rakefile +0 -6
- data/spec/fixtures/rails4_users_app/app/assets/images/.keep +0 -0
- data/spec/fixtures/rails4_users_app/app/assets/javascripts/application.js +0 -16
- data/spec/fixtures/rails4_users_app/app/assets/stylesheets/application.css +0 -15
- data/spec/fixtures/rails4_users_app/app/controllers/api/users_controller.rb +0 -27
- data/spec/fixtures/rails4_users_app/app/controllers/application_controller.rb +0 -5
- data/spec/fixtures/rails4_users_app/app/controllers/concerns/.keep +0 -0
- data/spec/fixtures/rails4_users_app/app/controllers/health_controller.rb +0 -5
- data/spec/fixtures/rails4_users_app/app/controllers/users_controller.rb +0 -5
- data/spec/fixtures/rails4_users_app/app/helpers/application_helper.rb +0 -2
- data/spec/fixtures/rails4_users_app/app/mailers/.keep +0 -0
- data/spec/fixtures/rails4_users_app/app/models/.keep +0 -0
- data/spec/fixtures/rails4_users_app/app/models/concerns/.keep +0 -0
- data/spec/fixtures/rails4_users_app/app/models/user.rb +0 -18
- data/spec/fixtures/rails4_users_app/app/views/layouts/application.html.haml +0 -7
- data/spec/fixtures/rails4_users_app/app/views/users/index.html.haml +0 -7
- data/spec/fixtures/rails4_users_app/appmap.yml +0 -3
- data/spec/fixtures/rails4_users_app/bin/rails +0 -9
- data/spec/fixtures/rails4_users_app/bin/setup +0 -29
- data/spec/fixtures/rails4_users_app/bin/spring +0 -17
- data/spec/fixtures/rails4_users_app/config.ru +0 -4
- data/spec/fixtures/rails4_users_app/config/application.rb +0 -26
- data/spec/fixtures/rails4_users_app/config/boot.rb +0 -3
- data/spec/fixtures/rails4_users_app/config/database.yml +0 -18
- data/spec/fixtures/rails4_users_app/config/environment.rb +0 -5
- data/spec/fixtures/rails4_users_app/config/environments/development.rb +0 -41
- data/spec/fixtures/rails4_users_app/config/environments/production.rb +0 -79
- data/spec/fixtures/rails4_users_app/config/environments/test.rb +0 -42
- data/spec/fixtures/rails4_users_app/config/initializers/assets.rb +0 -11
- data/spec/fixtures/rails4_users_app/config/initializers/backtrace_silencers.rb +0 -7
- data/spec/fixtures/rails4_users_app/config/initializers/cookies_serializer.rb +0 -3
- data/spec/fixtures/rails4_users_app/config/initializers/filter_parameter_logging.rb +0 -4
- data/spec/fixtures/rails4_users_app/config/initializers/inflections.rb +0 -16
- data/spec/fixtures/rails4_users_app/config/initializers/mime_types.rb +0 -4
- data/spec/fixtures/rails4_users_app/config/initializers/session_store.rb +0 -3
- data/spec/fixtures/rails4_users_app/config/initializers/to_time_preserves_timezone.rb +0 -10
- data/spec/fixtures/rails4_users_app/config/initializers/wrap_parameters.rb +0 -14
- data/spec/fixtures/rails4_users_app/config/locales/en.yml +0 -23
- data/spec/fixtures/rails4_users_app/config/routes.rb +0 -12
- data/spec/fixtures/rails4_users_app/config/secrets.yml +0 -22
- data/spec/fixtures/rails4_users_app/create_app +0 -23
- data/spec/fixtures/rails4_users_app/db/migrate/20191127112304_create_users.rb +0 -10
- data/spec/fixtures/rails4_users_app/db/schema.rb +0 -26
- data/spec/fixtures/rails4_users_app/db/seeds.rb +0 -7
- data/spec/fixtures/rails4_users_app/docker-compose.yml +0 -26
- data/spec/fixtures/rails4_users_app/lib/assets/.keep +0 -0
- data/spec/fixtures/rails4_users_app/lib/tasks/.keep +0 -0
- data/spec/fixtures/rails4_users_app/log/.keep +0 -0
- data/spec/fixtures/rails4_users_app/public/404.html +0 -67
- data/spec/fixtures/rails4_users_app/public/422.html +0 -67
- data/spec/fixtures/rails4_users_app/public/500.html +0 -66
- data/spec/fixtures/rails4_users_app/public/favicon.ico +0 -0
- data/spec/fixtures/rails4_users_app/public/robots.txt +0 -5
- data/spec/fixtures/rails4_users_app/spec/controllers/users_controller_api_spec.rb +0 -49
- data/spec/fixtures/rails4_users_app/spec/rails_helper.rb +0 -95
- data/spec/fixtures/rails4_users_app/spec/spec_helper.rb +0 -96
- data/spec/fixtures/rails4_users_app/test/fixtures/users.yml +0 -9
- data/spec/record_sql_rails4_pg_spec.rb +0 -75
data/lib/appmap/config.rb
CHANGED
|
@@ -2,15 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
module AppMap
|
|
4
4
|
class Config
|
|
5
|
-
Package = Struct.new(:path, :gem, :package_name, :exclude, :labels) do
|
|
5
|
+
Package = Struct.new(:path, :gem, :package_name, :exclude, :labels, :shallow) do
|
|
6
|
+
# Indicates that only the entry points to a package will be recorded.
|
|
7
|
+
# Once the code has entered a package, subsequent calls within the package will not be
|
|
8
|
+
# recorded unless the code leaves the package and re-enters it.
|
|
9
|
+
def shallow?
|
|
10
|
+
shallow
|
|
11
|
+
end
|
|
12
|
+
|
|
6
13
|
class << self
|
|
7
|
-
def build_from_path(path, package_name: nil, exclude: [], labels: [])
|
|
8
|
-
Package.new(path, nil, package_name, exclude, labels)
|
|
14
|
+
def build_from_path(path, shallow: false, package_name: nil, exclude: [], labels: [])
|
|
15
|
+
Package.new(path, nil, package_name, exclude, labels, shallow)
|
|
9
16
|
end
|
|
10
17
|
|
|
11
|
-
def build_from_gem(gem, package_name: nil, exclude: [], labels: [])
|
|
18
|
+
def build_from_gem(gem, shallow: true, package_name: nil, exclude: [], labels: [])
|
|
12
19
|
gem_paths(gem).map do |gem_path|
|
|
13
|
-
Package.new(gem_path, gem, package_name, exclude, labels)
|
|
20
|
+
Package.new(gem_path, gem, package_name, exclude, labels, shallow)
|
|
14
21
|
end
|
|
15
22
|
end
|
|
16
23
|
|
|
@@ -36,7 +43,8 @@ module AppMap
|
|
|
36
43
|
package_name: package_name,
|
|
37
44
|
gem: gem,
|
|
38
45
|
exclude: exclude.blank? ? nil : exclude,
|
|
39
|
-
labels: labels.blank? ? nil : labels
|
|
46
|
+
labels: labels.blank? ? nil : labels,
|
|
47
|
+
shallow: shallow
|
|
40
48
|
}.compact
|
|
41
49
|
end
|
|
42
50
|
end
|
|
@@ -49,31 +57,32 @@ module AppMap
|
|
|
49
57
|
# Methods that should always be hooked, with their containing
|
|
50
58
|
# package and labels that should be applied to them.
|
|
51
59
|
HOOKED_METHODS = {
|
|
52
|
-
'ActiveSupport::SecurityUtils' => Hook.new(:secure_compare, Package.build_from_path('active_support', package_name: 'active_support', labels: %w[
|
|
60
|
+
'ActiveSupport::SecurityUtils' => Hook.new(:secure_compare, Package.build_from_path('active_support', package_name: 'active_support', labels: %w[provider.secure_compare])),
|
|
61
|
+
'ActionView::Renderer' => Hook.new(:render, Package.build_from_path('action_view', package_name: 'action_view', labels: %w[mvc.view]))
|
|
53
62
|
}.freeze
|
|
54
63
|
|
|
55
64
|
BUILTIN_METHODS = {
|
|
56
65
|
'OpenSSL::PKey::PKey' => Hook.new(:sign, OPENSSL_PACKAGES),
|
|
57
|
-
'Digest::Instance' => Hook.new(:digest, OPENSSL_PACKAGES),
|
|
58
66
|
'OpenSSL::X509::Request' => Hook.new(%i[sign verify], OPENSSL_PACKAGES),
|
|
59
67
|
'OpenSSL::PKCS5' => Hook.new(%i[pbkdf2_hmac_sha1 pbkdf2_hmac], OPENSSL_PACKAGES),
|
|
60
68
|
'OpenSSL::Cipher' => Hook.new(%i[encrypt decrypt final], OPENSSL_PACKAGES),
|
|
61
69
|
'OpenSSL::X509::Certificate' => Hook.new(:sign, OPENSSL_PACKAGES),
|
|
62
|
-
'Net::HTTP' => Hook.new(:request, Package.build_from_path('net/http', package_name: 'net/http', labels: %w[http io])),
|
|
63
|
-
'Net::SMTP' => Hook.new(:send, Package.build_from_path('net/smtp', package_name: 'net/smtp', labels: %w[smtp email io])),
|
|
64
|
-
'Net::POP3' => Hook.new(:mails, Package.build_from_path('net/pop3', package_name: 'net/pop', labels: %w[pop
|
|
65
|
-
'Net::IMAP' => Hook.new(:send_command, Package.build_from_path('net/imap', package_name: 'net/imap', labels: %w[imap email io])),
|
|
66
|
-
'Marshal' => Hook.new(%i[dump load], Package.build_from_path('marshal', labels: %w[serialization marshal])),
|
|
67
|
-
'Psych' => Hook.new(%i[dump dump_stream load load_stream parse parse_stream], Package.build_from_path('yaml', package_name: 'psych', labels: %w[serialization
|
|
68
|
-
'JSON::Ext::Parser' => Hook.new(:parse, Package.build_from_path('json', package_name: 'json', labels: %w[serialization
|
|
69
|
-
'JSON::Ext::Generator::State' => Hook.new(:generate, Package.build_from_path('json', package_name: 'json', labels: %w[serialization
|
|
70
|
+
'Net::HTTP' => Hook.new(:request, Package.build_from_path('net/http', package_name: 'net/http', labels: %w[protocol.http io])),
|
|
71
|
+
'Net::SMTP' => Hook.new(:send, Package.build_from_path('net/smtp', package_name: 'net/smtp', labels: %w[protocol.smtp protocol.email io])),
|
|
72
|
+
'Net::POP3' => Hook.new(:mails, Package.build_from_path('net/pop3', package_name: 'net/pop', labels: %w[protocol.pop protocol.email io])),
|
|
73
|
+
'Net::IMAP' => Hook.new(:send_command, Package.build_from_path('net/imap', package_name: 'net/imap', labels: %w[protocol.imap protocol.email io])),
|
|
74
|
+
'Marshal' => Hook.new(%i[dump load], Package.build_from_path('marshal', labels: %w[format.marshal provider.serialization marshal])),
|
|
75
|
+
'Psych' => Hook.new(%i[dump dump_stream load load_stream parse parse_stream], Package.build_from_path('yaml', package_name: 'psych', labels: %w[format.yaml provider.serialization])),
|
|
76
|
+
'JSON::Ext::Parser' => Hook.new(:parse, Package.build_from_path('json', package_name: 'json', labels: %w[format.json provider.serialization])),
|
|
77
|
+
'JSON::Ext::Generator::State' => Hook.new(:generate, Package.build_from_path('json', package_name: 'json', labels: %w[format.json provider.serialization])),
|
|
70
78
|
}.freeze
|
|
71
79
|
|
|
72
|
-
attr_reader :name, :packages
|
|
80
|
+
attr_reader :name, :packages, :exclude
|
|
73
81
|
|
|
74
|
-
def initialize(name, packages = [])
|
|
82
|
+
def initialize(name, packages = [], exclude = [])
|
|
75
83
|
@name = name
|
|
76
84
|
@packages = packages
|
|
85
|
+
@exclude = exclude
|
|
77
86
|
end
|
|
78
87
|
|
|
79
88
|
class << self
|
|
@@ -91,27 +100,38 @@ module AppMap
|
|
|
91
100
|
raise 'AppMap package configuration should specify gem or path, not both' if gem && path
|
|
92
101
|
|
|
93
102
|
if gem
|
|
94
|
-
|
|
103
|
+
shallow = package['shallow']
|
|
104
|
+
# shallow is true by default for gems
|
|
105
|
+
shallow = true if shallow.nil?
|
|
106
|
+
Package.build_from_gem(gem, exclude: package['exclude'] || [], shallow: shallow)
|
|
95
107
|
else
|
|
96
|
-
[ Package.build_from_path(path, exclude: package['exclude'] || []) ]
|
|
108
|
+
[ Package.build_from_path(path, exclude: package['exclude'] || [], shallow: package['shallow']) ]
|
|
97
109
|
end
|
|
98
110
|
end.flatten
|
|
99
|
-
Config.new config_data['name'], packages
|
|
111
|
+
Config.new config_data['name'], packages, config_data['exclude'] || []
|
|
100
112
|
end
|
|
101
113
|
end
|
|
102
114
|
|
|
103
115
|
def to_h
|
|
104
116
|
{
|
|
105
117
|
name: name,
|
|
106
|
-
packages: packages.map(&:to_h)
|
|
118
|
+
packages: packages.map(&:to_h),
|
|
119
|
+
exclude: exclude
|
|
107
120
|
}
|
|
108
121
|
end
|
|
109
122
|
|
|
123
|
+
# package_for_method finds the Package, if any, which configures the hook
|
|
124
|
+
# for a method.
|
|
110
125
|
def package_for_method(method)
|
|
126
|
+
package_hooked_by_class(method) || package_hooked_by_source_location(method)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def package_hooked_by_class(method)
|
|
111
130
|
defined_class, _, method_name = ::AppMap::Hook.qualify_method_name(method)
|
|
112
|
-
|
|
113
|
-
|
|
131
|
+
return find_package(defined_class, method_name)
|
|
132
|
+
end
|
|
114
133
|
|
|
134
|
+
def package_hooked_by_source_location(method)
|
|
115
135
|
location = method.source_location
|
|
116
136
|
location_file, = location
|
|
117
137
|
return unless location_file
|
|
@@ -123,14 +143,22 @@ module AppMap
|
|
|
123
143
|
end
|
|
124
144
|
end
|
|
125
145
|
|
|
126
|
-
def
|
|
127
|
-
|
|
146
|
+
def never_hook?(method)
|
|
147
|
+
defined_class, separator, method_name = ::AppMap::Hook.qualify_method_name(method)
|
|
148
|
+
return true if exclude.member?(defined_class) || exclude.member?([ defined_class, separator, method_name ].join)
|
|
128
149
|
end
|
|
129
150
|
|
|
151
|
+
# always_hook? indicates a method that should always be hooked.
|
|
130
152
|
def always_hook?(defined_class, method_name)
|
|
131
153
|
!!find_package(defined_class, method_name)
|
|
132
154
|
end
|
|
133
155
|
|
|
156
|
+
# included_by_location? indicates a method whose source location matches a method definition that has been
|
|
157
|
+
# configured for inclusion.
|
|
158
|
+
def included_by_location?(method)
|
|
159
|
+
!!package_for_method(method)
|
|
160
|
+
end
|
|
161
|
+
|
|
134
162
|
def find_package(defined_class, method_name)
|
|
135
163
|
hook = find_hook(defined_class)
|
|
136
164
|
return nil unless hook
|
data/lib/appmap/hook.rb
CHANGED
|
@@ -6,6 +6,9 @@ module AppMap
|
|
|
6
6
|
class Hook
|
|
7
7
|
LOG = (ENV['APPMAP_DEBUG'] == 'true' || ENV['DEBUG'] == 'true')
|
|
8
8
|
|
|
9
|
+
OBJECT_INSTANCE_METHODS = %i[! != !~ <=> == === =~ __id__ __send__ class clone define_singleton_method display dup enum_for eql? equal? extend freeze frozen? hash inspect instance_eval instance_exec instance_of? instance_variable_defined? instance_variable_get instance_variable_set instance_variables is_a? itself kind_of? method methods nil? object_id private_methods protected_methods public_method public_methods public_send remove_instance_variable respond_to? send singleton_class singleton_method singleton_methods taint tainted? tap then to_enum to_s to_h to_a trust untaint untrust untrusted? yield_self].freeze
|
|
10
|
+
OBJECT_STATIC_METHODS = %i[! != !~ < <= <=> == === =~ > >= __id__ __send__ alias_method allocate ancestors attr attr_accessor attr_reader attr_writer autoload autoload? class class_eval class_exec class_variable_defined? class_variable_get class_variable_set class_variables clone const_defined? const_get const_missing const_set constants define_method define_singleton_method deprecate_constant display dup enum_for eql? equal? extend freeze frozen? hash include include? included_modules inspect instance_eval instance_exec instance_method instance_methods instance_of? instance_variable_defined? instance_variable_get instance_variable_set instance_variables is_a? itself kind_of? method method_defined? methods module_eval module_exec name new nil? object_id prepend private_class_method private_constant private_instance_methods private_method_defined? private_methods protected_instance_methods protected_method_defined? protected_methods public_class_method public_constant public_instance_method public_instance_methods public_method public_method_defined? public_methods public_send remove_class_variable remove_instance_variable remove_method respond_to? send singleton_class singleton_class? singleton_method singleton_methods superclass taint tainted? tap then to_enum to_s trust undef_method untaint untrust untrusted? yield_self].freeze
|
|
11
|
+
|
|
9
12
|
@unbound_method_arity = ::UnboundMethod.instance_method(:arity)
|
|
10
13
|
@method_arity = ::Method.instance_method(:arity)
|
|
11
14
|
|
|
@@ -42,12 +45,17 @@ module AppMap
|
|
|
42
45
|
tp = TracePoint.new(:end) do |trace_point|
|
|
43
46
|
cls = trace_point.self
|
|
44
47
|
|
|
45
|
-
instance_methods = cls.public_instance_methods(false)
|
|
46
|
-
class_methods = cls.singleton_class.public_instance_methods(false) - instance_methods
|
|
48
|
+
instance_methods = cls.public_instance_methods(false) - OBJECT_INSTANCE_METHODS
|
|
49
|
+
class_methods = cls.singleton_class.public_instance_methods(false) - instance_methods - OBJECT_STATIC_METHODS
|
|
47
50
|
|
|
48
51
|
hook = lambda do |hook_cls|
|
|
49
52
|
lambda do |method_id|
|
|
50
|
-
method =
|
|
53
|
+
method = begin
|
|
54
|
+
hook_cls.public_instance_method(method_id)
|
|
55
|
+
rescue NameError
|
|
56
|
+
warn "AppMap: Method #{hook_cls} #{method.name} is not accessible" if LOG
|
|
57
|
+
return
|
|
58
|
+
end
|
|
51
59
|
|
|
52
60
|
warn "AppMap: Examining #{hook_cls} #{method.name}" if LOG
|
|
53
61
|
|
|
@@ -55,6 +63,8 @@ module AppMap
|
|
|
55
63
|
# Skip methods that have no instruction sequence, as they are obviously trivial.
|
|
56
64
|
next unless disasm
|
|
57
65
|
|
|
66
|
+
next if config.never_hook?(method)
|
|
67
|
+
|
|
58
68
|
next unless \
|
|
59
69
|
config.always_hook?(hook_cls, method.name) ||
|
|
60
70
|
config.included_by_location?(method)
|
|
@@ -76,6 +86,8 @@ module AppMap
|
|
|
76
86
|
tp.enable(&block)
|
|
77
87
|
end
|
|
78
88
|
|
|
89
|
+
# hook_builtins builds hooks for code that is built in to the Ruby standard library.
|
|
90
|
+
# No TracePoint events are emitted for builtins, so a separate hooking mechanism is needed.
|
|
79
91
|
def hook_builtins
|
|
80
92
|
return unless self.class.lock_builtins
|
|
81
93
|
|
|
@@ -89,6 +101,7 @@ module AppMap
|
|
|
89
101
|
require hook.package.package_name if hook.package.package_name
|
|
90
102
|
Array(hook.method_names).each do |method_name|
|
|
91
103
|
method_name = method_name.to_sym
|
|
104
|
+
|
|
92
105
|
cls = class_from_string.(class_name)
|
|
93
106
|
method = \
|
|
94
107
|
begin
|
|
@@ -97,6 +110,8 @@ module AppMap
|
|
|
97
110
|
cls.method(method_name) rescue nil
|
|
98
111
|
end
|
|
99
112
|
|
|
113
|
+
next if config.never_hook?(method)
|
|
114
|
+
|
|
100
115
|
if method
|
|
101
116
|
Hook::Method.new(hook.package, cls, method).activate
|
|
102
117
|
else
|
data/lib/appmap/hook/method.rb
CHANGED
|
@@ -39,6 +39,7 @@ module AppMap
|
|
|
39
39
|
end
|
|
40
40
|
|
|
41
41
|
defined_class = @defined_class
|
|
42
|
+
hook_package = self.hook_package
|
|
42
43
|
hook_method = self.hook_method
|
|
43
44
|
before_hook = self.method(:before_hook)
|
|
44
45
|
after_hook = self.method(:after_hook)
|
|
@@ -48,29 +49,34 @@ module AppMap
|
|
|
48
49
|
hook_class.instance_eval do
|
|
49
50
|
hook_method_def = Proc.new do |*args, &block|
|
|
50
51
|
instance_method = hook_method.bind(self).to_proc
|
|
52
|
+
call_instance_method = -> { instance_method.call(*args, &block) }
|
|
51
53
|
|
|
52
54
|
# We may not have gotten the class for the method during
|
|
53
55
|
# initialization (e.g. for a singleton method on an embedded
|
|
54
56
|
# struct), so make sure we have it now.
|
|
55
|
-
defined_class,
|
|
57
|
+
defined_class, = Hook.qualify_method_name(hook_method) unless defined_class
|
|
56
58
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
reentrant = Thread.current[HOOK_DISABLE_KEY]
|
|
60
|
+
disabled_by_shallow_flag = \
|
|
61
|
+
-> { hook_package&.shallow? && AppMap.tracing.last_package_for_current_thread == hook_package }
|
|
60
62
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
+
enabled = true if AppMap.tracing.enabled? && !reentrant && !disabled_by_shallow_flag.call
|
|
64
|
+
|
|
65
|
+
return call_instance_method.call unless enabled
|
|
66
|
+
|
|
67
|
+
call_event, start_time = with_disabled_hook.call do
|
|
68
|
+
before_hook.call(self, defined_class, args)
|
|
63
69
|
end
|
|
64
70
|
return_value = nil
|
|
65
71
|
exception = nil
|
|
66
72
|
begin
|
|
67
|
-
return_value =
|
|
73
|
+
return_value = call_instance_method.call
|
|
68
74
|
rescue
|
|
69
75
|
exception = $ERROR_INFO
|
|
70
76
|
raise
|
|
71
77
|
ensure
|
|
72
|
-
with_disabled_hook.
|
|
73
|
-
after_hook.(self, call_event, start_time, return_value, exception)
|
|
78
|
+
with_disabled_hook.call do
|
|
79
|
+
after_hook.call(self, call_event, start_time, return_value, exception)
|
|
74
80
|
end
|
|
75
81
|
end
|
|
76
82
|
end
|
|
@@ -87,7 +93,7 @@ module AppMap
|
|
|
87
93
|
[ call_event, TIME_NOW.call ]
|
|
88
94
|
end
|
|
89
95
|
|
|
90
|
-
def after_hook(
|
|
96
|
+
def after_hook(_receiver, call_event, start_time, return_value, exception)
|
|
91
97
|
require 'appmap/event'
|
|
92
98
|
elapsed = TIME_NOW.call - start_time
|
|
93
99
|
return_event = \
|
|
@@ -95,12 +101,12 @@ module AppMap
|
|
|
95
101
|
AppMap.tracing.record_event return_event
|
|
96
102
|
end
|
|
97
103
|
|
|
98
|
-
def with_disabled_hook(&
|
|
104
|
+
def with_disabled_hook(&function)
|
|
99
105
|
# Don't record functions, such as to_s and inspect, that might be called
|
|
100
106
|
# by the fn. Otherwise there can be a stack overflow.
|
|
101
107
|
Thread.current[HOOK_DISABLE_KEY] = true
|
|
102
108
|
begin
|
|
103
|
-
|
|
109
|
+
function.call
|
|
104
110
|
ensure
|
|
105
111
|
Thread.current[HOOK_DISABLE_KEY] = false
|
|
106
112
|
end
|
|
@@ -7,12 +7,13 @@ module AppMap
|
|
|
7
7
|
module Rails
|
|
8
8
|
module RequestHandler
|
|
9
9
|
class HTTPServerRequest < AppMap::Event::MethodEvent
|
|
10
|
-
attr_accessor :request_method, :path_info, :params
|
|
10
|
+
attr_accessor :normalized_path_info, :request_method, :path_info, :params
|
|
11
11
|
|
|
12
12
|
def initialize(request)
|
|
13
13
|
super AppMap::Event.next_id_counter, :call, Thread.current.object_id
|
|
14
14
|
|
|
15
15
|
@request_method = request.request_method
|
|
16
|
+
@normalized_path_info = normalized_path request
|
|
16
17
|
@path_info = request.path_info.split('?')[0]
|
|
17
18
|
# ActionDispatch::Http::ParameterFilter is deprecated
|
|
18
19
|
parameter_filter_cls = \
|
|
@@ -28,8 +29,9 @@ module AppMap
|
|
|
28
29
|
super.tap do |h|
|
|
29
30
|
h[:http_server_request] = {
|
|
30
31
|
request_method: request_method,
|
|
31
|
-
path_info: path_info
|
|
32
|
-
|
|
32
|
+
path_info: path_info,
|
|
33
|
+
normalized_path_info: normalized_path_info
|
|
34
|
+
}.compact
|
|
33
35
|
|
|
34
36
|
h[:message] = params.keys.map do |key|
|
|
35
37
|
val = params[key]
|
|
@@ -42,6 +44,18 @@ module AppMap
|
|
|
42
44
|
end
|
|
43
45
|
end
|
|
44
46
|
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def normalized_path(request, router = ::Rails.application.routes.router)
|
|
51
|
+
router.recognize request do |route, _|
|
|
52
|
+
app = route.app
|
|
53
|
+
next unless app.matches? request
|
|
54
|
+
return normalized_path request, app.rack_app.routes.router if app.engine?
|
|
55
|
+
|
|
56
|
+
return route.path.spec.to_s
|
|
57
|
+
end
|
|
58
|
+
end
|
|
45
59
|
end
|
|
46
60
|
|
|
47
61
|
class HTTPServerResponse < AppMap::Event::MethodReturnIgnoreValue
|
data/lib/appmap/railtie.rb
CHANGED
|
@@ -5,13 +5,9 @@ module AppMap
|
|
|
5
5
|
class Railtie < ::Rails::Railtie
|
|
6
6
|
config.appmap = ActiveSupport::OrderedOptions.new
|
|
7
7
|
|
|
8
|
-
initializer 'appmap.init' do |_| # params: app
|
|
9
|
-
require 'appmap'
|
|
10
|
-
end
|
|
11
|
-
|
|
12
8
|
# appmap.subscribe subscribes to ActiveSupport Notifications so that they can be recorded as
|
|
13
9
|
# AppMap events.
|
|
14
|
-
initializer 'appmap.subscribe'
|
|
10
|
+
initializer 'appmap.subscribe' do |_| # params: app
|
|
15
11
|
require 'appmap/rails/sql_handler'
|
|
16
12
|
require 'appmap/rails/request_handler'
|
|
17
13
|
ActiveSupport::Notifications.subscribe 'sql.sequel', AppMap::Rails::SQLHandler.new
|
data/lib/appmap/trace.rb
CHANGED
|
@@ -15,34 +15,38 @@ module AppMap
|
|
|
15
15
|
|
|
16
16
|
class Tracing
|
|
17
17
|
def initialize
|
|
18
|
-
@
|
|
18
|
+
@tracers = []
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def empty?
|
|
22
|
-
@
|
|
22
|
+
@tracers.empty?
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
def trace(enable: true)
|
|
26
26
|
Tracer.new.tap do |tracer|
|
|
27
|
-
@
|
|
27
|
+
@tracers << tracer
|
|
28
28
|
tracer.enable if enable
|
|
29
29
|
end
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
def enabled?
|
|
33
|
-
@
|
|
33
|
+
@tracers.any?(&:enabled?)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def last_package_for_current_thread
|
|
37
|
+
@tracers.first&.last_package_for_current_thread
|
|
34
38
|
end
|
|
35
39
|
|
|
36
40
|
def record_event(event, package: nil, defined_class: nil, method: nil)
|
|
37
|
-
@
|
|
41
|
+
@tracers.each do |tracer|
|
|
38
42
|
tracer.record_event(event, package: package, defined_class: defined_class, method: method)
|
|
39
43
|
end
|
|
40
44
|
end
|
|
41
45
|
|
|
42
46
|
def delete(tracer)
|
|
43
|
-
return unless @
|
|
47
|
+
return unless @tracers.member?(tracer)
|
|
44
48
|
|
|
45
|
-
@
|
|
49
|
+
@tracers.delete(tracer)
|
|
46
50
|
tracer.disable
|
|
47
51
|
end
|
|
48
52
|
end
|
|
@@ -52,6 +56,7 @@ module AppMap
|
|
|
52
56
|
# Records the events which happen in a program.
|
|
53
57
|
def initialize
|
|
54
58
|
@events = []
|
|
59
|
+
@last_package_for_thread = {}
|
|
55
60
|
@methods = Set.new
|
|
56
61
|
@enabled = false
|
|
57
62
|
end
|
|
@@ -75,11 +80,17 @@ module AppMap
|
|
|
75
80
|
def record_event(event, package: nil, defined_class: nil, method: nil)
|
|
76
81
|
return unless @enabled
|
|
77
82
|
|
|
83
|
+
@last_package_for_thread[Thread.current.object_id] = package if package
|
|
78
84
|
@events << event
|
|
79
85
|
@methods << Trace::ScopedMethod.new(package, defined_class, method, event.static) \
|
|
80
86
|
if package && defined_class && method && (event.event == :call)
|
|
81
87
|
end
|
|
82
88
|
|
|
89
|
+
# Gets the last package which was observed on the current thread.
|
|
90
|
+
def last_package_for_current_thread
|
|
91
|
+
@last_package_for_thread[Thread.current.object_id]
|
|
92
|
+
end
|
|
93
|
+
|
|
83
94
|
# Gets a unique list of the methods that were invoked by the program.
|
|
84
95
|
def event_methods
|
|
85
96
|
@methods.to_a
|
data/lib/appmap/version.rb
CHANGED
|
@@ -1,89 +1,150 @@
|
|
|
1
1
|
require 'rails_spec_helper'
|
|
2
2
|
|
|
3
3
|
describe 'AbstractControllerBase' do
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
%w[5 6].each do |rails_major_version| # rubocop:disable Metrics/BlockLength
|
|
5
|
+
context "in Rails #{rails_major_version}" do
|
|
6
|
+
include_context 'Rails app pg database', "spec/fixtures/rails#{rails_major_version}_users_app"
|
|
7
|
+
def run_spec(spec_name)
|
|
7
8
|
FileUtils.rm_rf tmpdir
|
|
8
9
|
FileUtils.mkdir_p tmpdir
|
|
9
|
-
cmd =
|
|
10
|
+
cmd = <<~CMD.gsub "\n", ' '
|
|
11
|
+
docker-compose run --rm -e RAILS_ENV=test -e APPMAP=true
|
|
12
|
+
-v #{File.absolute_path tmpdir}:/app/tmp app ./bin/rspec #{spec_name}
|
|
13
|
+
CMD
|
|
10
14
|
run_cmd cmd, chdir: fixture_dir
|
|
15
|
+
end
|
|
11
16
|
|
|
12
|
-
|
|
17
|
+
def tmpdir
|
|
18
|
+
'tmp/spec/AbstractControllerBase'
|
|
13
19
|
end
|
|
14
20
|
|
|
15
|
-
let(:
|
|
16
|
-
let(:
|
|
21
|
+
let(:appmap) { JSON.parse File.read File.join tmpdir, 'appmap/rspec', appmap_json_file }
|
|
22
|
+
let(:events) { appmap['events'] }
|
|
17
23
|
|
|
18
24
|
describe 'testing with rspec' do
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
expect(File).to exist(appmap_json)
|
|
25
|
-
appmap = JSON.parse(File.read(appmap_json)).to_yaml
|
|
25
|
+
describe 'creating a user' do
|
|
26
|
+
before(:all) { run_spec 'spec/controllers/users_controller_api_spec.rb:8' }
|
|
27
|
+
let(:appmap_json_file) do
|
|
28
|
+
'Api_UsersController_POST_api_users_with_required_parameters_creates_a_user.appmap.json'
|
|
29
|
+
end
|
|
26
30
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
class: String
|
|
31
|
-
value: alice
|
|
32
|
-
object_id:
|
|
33
|
-
MESSAGE
|
|
31
|
+
it 'inventory file is printed' do
|
|
32
|
+
expect(File).to exist(File.join(tmpdir, 'appmap/rspec/Inventory.appmap.json'))
|
|
33
|
+
end
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
35
|
+
it 'message fields are recorded in the appmap' do
|
|
36
|
+
expect(events).to include(
|
|
37
|
+
hash_including(
|
|
38
|
+
'http_server_request' => hash_including(
|
|
39
|
+
'request_method' => 'POST',
|
|
40
|
+
'path_info' => '/api/users'
|
|
41
|
+
),
|
|
42
|
+
'message' => include(
|
|
43
|
+
hash_including(
|
|
44
|
+
'name' => 'login',
|
|
45
|
+
'class' => 'String',
|
|
46
|
+
'value' => 'alice',
|
|
47
|
+
'object_id' => Integer
|
|
48
|
+
),
|
|
49
|
+
hash_including(
|
|
50
|
+
'name' => 'password',
|
|
51
|
+
'class' => 'String',
|
|
52
|
+
'value' => '[FILTERED]',
|
|
53
|
+
'object_id' => Integer
|
|
54
|
+
)
|
|
55
|
+
)
|
|
56
|
+
),
|
|
57
|
+
hash_including(
|
|
58
|
+
'http_server_response' => {
|
|
59
|
+
'status' => 201,
|
|
60
|
+
'mime_type' => 'application/json; charset=utf-8'
|
|
61
|
+
}
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
end
|
|
41
65
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
66
|
+
it 'properly captures method parameters in the appmap' do
|
|
67
|
+
expect(events).to include hash_including(
|
|
68
|
+
'event' => 'call',
|
|
69
|
+
'thread_id' => Integer,
|
|
70
|
+
'defined_class' => 'Api::UsersController',
|
|
71
|
+
'method_id' => 'build_user',
|
|
72
|
+
'path' => 'app/controllers/api/users_controller.rb',
|
|
73
|
+
'lineno' => 23,
|
|
74
|
+
'static' => false,
|
|
75
|
+
'parameters' => include(
|
|
76
|
+
'name' => 'params',
|
|
77
|
+
'class' => 'ActiveSupport::HashWithIndifferentAccess',
|
|
78
|
+
'object_id' => Integer,
|
|
79
|
+
'value' => '{"login"=>"alice"}',
|
|
80
|
+
'kind' => 'req'
|
|
81
|
+
),
|
|
82
|
+
'receiver' => anything
|
|
83
|
+
)
|
|
84
|
+
end
|
|
47
85
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
86
|
+
it 'returns a minimal event' do
|
|
87
|
+
expect(events).to include hash_including(
|
|
88
|
+
'event' => 'return',
|
|
89
|
+
'return_value' => Hash,
|
|
90
|
+
'id' => Integer,
|
|
91
|
+
'thread_id' => Integer,
|
|
92
|
+
'parent_id' => Integer,
|
|
93
|
+
'elapsed' => Numeric
|
|
94
|
+
)
|
|
95
|
+
end
|
|
53
96
|
end
|
|
54
97
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
98
|
+
describe 'showing a user' do
|
|
99
|
+
before(:all) { run_spec 'spec/controllers/users_controller_spec.rb:22' }
|
|
100
|
+
let(:appmap_json_file) do
|
|
101
|
+
'UsersController_GET_users_login_shows_the_user.appmap.json'
|
|
102
|
+
end
|
|
58
103
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
object_id: .*
|
|
71
|
-
value: '{"login"=>"alice"}'
|
|
72
|
-
kind: req
|
|
73
|
-
receiver:
|
|
74
|
-
CREATE_CALL
|
|
104
|
+
it 'records the normalized path info' do
|
|
105
|
+
expect(events).to include(
|
|
106
|
+
hash_including(
|
|
107
|
+
'http_server_request' => {
|
|
108
|
+
'request_method' => 'GET',
|
|
109
|
+
'path_info' => '/users/alice',
|
|
110
|
+
'normalized_path_info' => '/users/:id(.:format)'
|
|
111
|
+
}
|
|
112
|
+
)
|
|
113
|
+
)
|
|
114
|
+
end
|
|
75
115
|
end
|
|
76
116
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
117
|
+
describe 'listing users' do
|
|
118
|
+
before(:all) { run_spec 'spec/controllers/users_controller_spec.rb:11' }
|
|
119
|
+
let(:appmap_json_file) { 'UsersController_GET_users_lists_the_users.appmap.json' }
|
|
120
|
+
|
|
121
|
+
it 'records and labels view rendering' do
|
|
122
|
+
expect(events).to include hash_including(
|
|
123
|
+
'event' => 'call',
|
|
124
|
+
'thread_id' => Numeric,
|
|
125
|
+
'defined_class' => 'ActionView::Renderer',
|
|
126
|
+
'method_id' => 'render',
|
|
127
|
+
'path' => String,
|
|
128
|
+
'lineno' => Integer,
|
|
129
|
+
'static' => false
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
expect(appmap['classMap']).to include hash_including(
|
|
133
|
+
'name' => 'action_view',
|
|
134
|
+
'children' => include(hash_including(
|
|
135
|
+
'name' => 'ActionView',
|
|
136
|
+
'children' => include(hash_including(
|
|
137
|
+
'name' => 'Renderer',
|
|
138
|
+
'children' => include(hash_including(
|
|
139
|
+
'name' => 'render',
|
|
140
|
+
'labels' => ['mvc.view']
|
|
141
|
+
))
|
|
142
|
+
))
|
|
143
|
+
))
|
|
144
|
+
)
|
|
145
|
+
end
|
|
82
146
|
end
|
|
83
147
|
end
|
|
84
148
|
end
|
|
85
149
|
end
|
|
86
|
-
|
|
87
|
-
it_behaves_like 'rails version', '5'
|
|
88
|
-
it_behaves_like 'rails version', '6'
|
|
89
150
|
end
|