appmap 0.26.0 → 0.31.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/.gitignore +1 -2
- data/CHANGELOG.md +38 -0
- data/README.md +144 -31
- data/Rakefile +1 -1
- data/exe/appmap +3 -1
- data/lib/appmap.rb +55 -35
- data/lib/appmap/algorithm/stats.rb +2 -1
- data/lib/appmap/class_map.rb +16 -24
- data/lib/appmap/command/record.rb +2 -61
- data/lib/appmap/config.rb +91 -0
- data/lib/appmap/cucumber.rb +89 -0
- data/lib/appmap/event.rb +6 -6
- data/lib/appmap/hook.rb +94 -116
- data/lib/appmap/metadata.rb +62 -0
- data/lib/appmap/middleware/remote_recording.rb +2 -6
- data/lib/appmap/minitest.rb +141 -0
- data/lib/appmap/rails/action_handler.rb +2 -2
- data/lib/appmap/rails/sql_handler.rb +2 -2
- data/lib/appmap/railtie.rb +2 -2
- data/lib/appmap/record.rb +27 -0
- data/lib/appmap/rspec.rb +20 -38
- data/lib/appmap/trace.rb +19 -11
- data/lib/appmap/util.rb +40 -0
- data/lib/appmap/version.rb +1 -1
- data/package-lock.json +3 -3
- data/spec/abstract_controller4_base_spec.rb +1 -1
- data/spec/abstract_controller_base_spec.rb +1 -1
- data/spec/config_spec.rb +3 -3
- data/spec/fixtures/hook/compare.rb +7 -0
- data/spec/fixtures/hook/openssl_sign.rb +87 -0
- data/spec/fixtures/hook/singleton_method.rb +54 -0
- data/spec/fixtures/rails_users_app/Gemfile +1 -0
- data/spec/fixtures/rails_users_app/features/api_users.feature +13 -0
- data/spec/fixtures/rails_users_app/features/support/env.rb +4 -0
- data/spec/fixtures/rails_users_app/features/support/hooks.rb +11 -0
- data/spec/fixtures/rails_users_app/features/support/steps.rb +18 -0
- data/spec/hook_spec.rb +243 -36
- data/spec/rails_spec_helper.rb +2 -0
- data/spec/rspec_feature_metadata_spec.rb +2 -0
- data/spec/spec_helper.rb +4 -0
- data/spec/util_spec.rb +21 -0
- data/test/cli_test.rb +2 -2
- data/test/cucumber_test.rb +72 -0
- data/test/fixtures/cucumber4_recorder/Gemfile +5 -0
- data/test/fixtures/cucumber4_recorder/appmap.yml +3 -0
- data/test/fixtures/cucumber4_recorder/features/say_hello.feature +5 -0
- data/test/fixtures/cucumber4_recorder/features/support/env.rb +5 -0
- data/test/fixtures/cucumber4_recorder/features/support/hooks.rb +11 -0
- data/test/fixtures/cucumber4_recorder/features/support/steps.rb +9 -0
- data/test/fixtures/cucumber4_recorder/lib/hello.rb +7 -0
- data/test/fixtures/cucumber_recorder/Gemfile +5 -0
- data/test/fixtures/cucumber_recorder/appmap.yml +3 -0
- data/test/fixtures/cucumber_recorder/features/say_hello.feature +5 -0
- data/test/fixtures/cucumber_recorder/features/support/env.rb +5 -0
- data/test/fixtures/cucumber_recorder/features/support/hooks.rb +11 -0
- data/test/fixtures/cucumber_recorder/features/support/steps.rb +9 -0
- data/test/fixtures/cucumber_recorder/lib/hello.rb +7 -0
- data/test/fixtures/minitest_recorder/Gemfile +5 -0
- data/test/fixtures/minitest_recorder/appmap.yml +3 -0
- data/test/fixtures/minitest_recorder/lib/hello.rb +5 -0
- data/test/fixtures/minitest_recorder/test/hello_test.rb +12 -0
- data/test/fixtures/process_recorder/appmap.yml +3 -0
- data/test/fixtures/process_recorder/hello.rb +9 -0
- data/test/fixtures/rspec_recorder/Gemfile +1 -1
- data/test/fixtures/rspec_recorder/spec/decorated_hello_spec.rb +12 -0
- data/test/minitest_test.rb +38 -0
- data/test/record_process_test.rb +35 -0
- data/test/rspec_test.rb +5 -0
- metadata +39 -3
- data/spec/fixtures/hook/class_method.rb +0 -17
@@ -74,10 +74,11 @@ module AppMap
|
|
74
74
|
|
75
75
|
class_name_func = ->(event) { event['defined_class'] }
|
76
76
|
full_name_func = lambda do |event|
|
77
|
+
call = event['event'] == 'call'
|
77
78
|
class_name = event['defined_class']
|
78
79
|
static = event['static']
|
79
80
|
function_name = event['method_id']
|
80
|
-
[ class_name, static ? '.' : '#', function_name ].join if class_name && !static.nil? && function_name
|
81
|
+
[ class_name, static ? '.' : '#', function_name ].join if call && class_name && !static.nil? && function_name
|
81
82
|
end
|
82
83
|
|
83
84
|
class_frequency = frequency_calc.call(class_name_func)
|
data/lib/appmap/class_map.rb
CHANGED
@@ -1,7 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'active_support/core_ext'
|
4
|
-
|
5
3
|
module AppMap
|
6
4
|
class ClassMap
|
7
5
|
module HasChildren
|
@@ -50,7 +48,7 @@ module AppMap
|
|
50
48
|
end
|
51
49
|
end
|
52
50
|
Function = Struct.new(:name) do
|
53
|
-
attr_accessor :static, :location
|
51
|
+
attr_accessor :static, :location, :labels
|
54
52
|
|
55
53
|
def type
|
56
54
|
'function'
|
@@ -61,8 +59,9 @@ module AppMap
|
|
61
59
|
name: name,
|
62
60
|
type: type,
|
63
61
|
location: location,
|
64
|
-
static: static
|
65
|
-
|
62
|
+
static: static,
|
63
|
+
labels: labels
|
64
|
+
}.delete_if {|k,v| v.nil?}
|
66
65
|
end
|
67
66
|
end
|
68
67
|
end
|
@@ -71,35 +70,25 @@ module AppMap
|
|
71
70
|
def build_from_methods(config, methods)
|
72
71
|
root = Types::Root.new
|
73
72
|
methods.each do |method|
|
74
|
-
package = package_for_method(
|
75
|
-
|
73
|
+
package = config.package_for_method(method) \
|
74
|
+
or raise "No package found for method #{method}"
|
75
|
+
add_function root, package, method
|
76
76
|
end
|
77
77
|
root.children.map(&:to_h)
|
78
78
|
end
|
79
79
|
|
80
80
|
protected
|
81
81
|
|
82
|
-
def
|
83
|
-
location = method.
|
84
|
-
location_file, = location
|
85
|
-
location_file = location_file[Dir.pwd.length + 1..-1] if location_file.index(Dir.pwd) == 0
|
86
|
-
|
87
|
-
packages.find do |pkg|
|
88
|
-
(location_file.index(pkg.path) == 0) &&
|
89
|
-
!pkg.exclude.find { |p| location_file.index(p) }
|
90
|
-
end or raise "No package found for method #{method}"
|
91
|
-
end
|
92
|
-
|
93
|
-
def add_function(root, package_name, method)
|
94
|
-
location = method.method.source_location
|
82
|
+
def add_function(root, package, method)
|
83
|
+
location = method.source_location
|
95
84
|
location_file, lineno = location
|
96
85
|
location_file = location_file[Dir.pwd.length + 1..-1] if location_file.index(Dir.pwd) == 0
|
97
86
|
|
98
|
-
static = method.
|
87
|
+
static = method.static
|
99
88
|
|
100
89
|
object_infos = [
|
101
90
|
{
|
102
|
-
name:
|
91
|
+
name: package.path,
|
103
92
|
type: 'package'
|
104
93
|
}
|
105
94
|
]
|
@@ -109,12 +98,15 @@ module AppMap
|
|
109
98
|
type: 'class'
|
110
99
|
}
|
111
100
|
end
|
112
|
-
|
113
|
-
name: method.
|
101
|
+
function_info = {
|
102
|
+
name: method.name,
|
114
103
|
type: 'function',
|
115
104
|
location: [ location_file, lineno ].join(':'),
|
116
105
|
static: static
|
117
106
|
}
|
107
|
+
function_info[:labels] = package.labels if package.labels
|
108
|
+
object_infos << function_info
|
109
|
+
|
118
110
|
parent = root
|
119
111
|
object_infos.each do |info|
|
120
112
|
parent = find_or_create parent.children, info do
|
@@ -5,66 +5,7 @@ module AppMap
|
|
5
5
|
RecordStruct = Struct.new(:config, :program)
|
6
6
|
|
7
7
|
class Record < RecordStruct
|
8
|
-
class << self
|
9
|
-
# Builds a Hash of metadata which can be detected by inspecting the system.
|
10
|
-
def detect_metadata
|
11
|
-
{
|
12
|
-
language: {
|
13
|
-
name: 'ruby',
|
14
|
-
engine: RUBY_ENGINE,
|
15
|
-
version: RUBY_VERSION
|
16
|
-
},
|
17
|
-
client: {
|
18
|
-
name: 'appmap',
|
19
|
-
url: AppMap::URL,
|
20
|
-
version: AppMap::VERSION
|
21
|
-
}
|
22
|
-
}.tap do |m|
|
23
|
-
if defined?(::Rails)
|
24
|
-
m[:frameworks] ||= []
|
25
|
-
m[:frameworks] << {
|
26
|
-
name: 'rails',
|
27
|
-
version: ::Rails.version
|
28
|
-
}
|
29
|
-
end
|
30
|
-
m[:git] = git_metadata if git_available
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
protected
|
35
|
-
|
36
|
-
def git_available
|
37
|
-
@git_available = system('git status 2>&1 > /dev/null') if @git_available.nil?
|
38
|
-
end
|
39
|
-
|
40
|
-
def git_metadata
|
41
|
-
git_repo = `git config --get remote.origin.url`.strip
|
42
|
-
git_branch = `git rev-parse --abbrev-ref HEAD`.strip
|
43
|
-
git_sha = `git rev-parse HEAD`.strip
|
44
|
-
git_status = `git status -s`.split("\n").map(&:strip)
|
45
|
-
git_last_annotated_tag = `git describe --abbrev=0 2>/dev/null`.strip
|
46
|
-
git_last_annotated_tag = nil if git_last_annotated_tag.blank?
|
47
|
-
git_last_tag = `git describe --abbrev=0 --tags 2>/dev/null`.strip
|
48
|
-
git_last_tag = nil if git_last_tag.blank?
|
49
|
-
git_commits_since_last_annotated_tag = `git describe`.strip =~ /-(\d+)-(\w+)$/[1] rescue 0 if git_last_annotated_tag
|
50
|
-
git_commits_since_last_tag = `git describe --tags`.strip =~ /-(\d+)-(\w+)$/[1] rescue 0 if git_last_tag
|
51
|
-
|
52
|
-
{
|
53
|
-
repository: git_repo,
|
54
|
-
branch: git_branch,
|
55
|
-
commit: git_sha,
|
56
|
-
status: git_status,
|
57
|
-
git_last_annotated_tag: git_last_annotated_tag,
|
58
|
-
git_last_tag: git_last_tag,
|
59
|
-
git_commits_since_last_annotated_tag: git_commits_since_last_annotated_tag,
|
60
|
-
git_commits_since_last_tag: git_commits_since_last_tag
|
61
|
-
}
|
62
|
-
end
|
63
|
-
end
|
64
|
-
|
65
8
|
def perform(&block)
|
66
|
-
AppMap::Hook.hook(config)
|
67
|
-
|
68
9
|
tracer = AppMap.tracing.trace
|
69
10
|
|
70
11
|
events = []
|
@@ -85,8 +26,8 @@ module AppMap
|
|
85
26
|
quit = true
|
86
27
|
event_thread.join
|
87
28
|
yield AppMap::APPMAP_FORMAT_VERSION,
|
88
|
-
|
89
|
-
AppMap.class_map(
|
29
|
+
AppMap.detect_metadata,
|
30
|
+
AppMap.class_map(tracer.event_methods),
|
90
31
|
events
|
91
32
|
end
|
92
33
|
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AppMap
|
4
|
+
Package = Struct.new(:path, :exclude, :labels) do
|
5
|
+
def initialize(path, exclude, labels = nil)
|
6
|
+
super
|
7
|
+
end
|
8
|
+
|
9
|
+
def to_h
|
10
|
+
{
|
11
|
+
path: path,
|
12
|
+
exclude: exclude.blank? ? nil : exclude,
|
13
|
+
labels: labels.blank? ? nil : labels
|
14
|
+
}.compact
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class Config
|
19
|
+
# Methods that should always be hooked, with their containing
|
20
|
+
# package and labels that should be applied to them.
|
21
|
+
HOOKED_METHODS = {
|
22
|
+
'ActiveSupport::SecurityUtils' => {
|
23
|
+
secure_compare: Package.new('active_support', nil, ['security'])
|
24
|
+
},
|
25
|
+
'OpenSSL::X509::Certificate' => {
|
26
|
+
sign: Package.new('openssl', nil, ['security'])
|
27
|
+
}
|
28
|
+
}
|
29
|
+
|
30
|
+
attr_reader :name, :packages
|
31
|
+
def initialize(name, packages = [])
|
32
|
+
@name = name
|
33
|
+
@packages = packages
|
34
|
+
end
|
35
|
+
|
36
|
+
class << self
|
37
|
+
# Loads configuration data from a file, specified by the file name.
|
38
|
+
def load_from_file(config_file_name)
|
39
|
+
require 'yaml'
|
40
|
+
load YAML.safe_load(::File.read(config_file_name))
|
41
|
+
end
|
42
|
+
|
43
|
+
# Loads configuration from a Hash.
|
44
|
+
def load(config_data)
|
45
|
+
packages = (config_data['packages'] || []).map do |package|
|
46
|
+
Package.new(package['path'], package['exclude'] || [])
|
47
|
+
end
|
48
|
+
Config.new config_data['name'], packages
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def to_h
|
53
|
+
{
|
54
|
+
name: name,
|
55
|
+
packages: packages.map(&:to_h)
|
56
|
+
}
|
57
|
+
end
|
58
|
+
|
59
|
+
def package_for_method(method)
|
60
|
+
location = method.source_location
|
61
|
+
location_file, = location
|
62
|
+
return unless location_file
|
63
|
+
|
64
|
+
defined_class,_,method_name = Hook.qualify_method_name(method)
|
65
|
+
hooked_method = find_hooked_method(defined_class, method_name)
|
66
|
+
return hooked_method if hooked_method
|
67
|
+
|
68
|
+
location_file = location_file[Dir.pwd.length + 1..-1] if location_file.index(Dir.pwd) == 0
|
69
|
+
packages.find do |pkg|
|
70
|
+
(location_file.index(pkg.path) == 0) &&
|
71
|
+
!pkg.exclude.find { |p| location_file.index(p) }
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def included_by_location?(method)
|
76
|
+
!!package_for_method(method)
|
77
|
+
end
|
78
|
+
|
79
|
+
def always_hook?(defined_class, method_name)
|
80
|
+
!!find_hooked_method(defined_class, method_name)
|
81
|
+
end
|
82
|
+
|
83
|
+
def find_hooked_method(defined_class, method_name)
|
84
|
+
find_hooked_class(defined_class)[method_name]
|
85
|
+
end
|
86
|
+
|
87
|
+
def find_hooked_class(defined_class)
|
88
|
+
HOOKED_METHODS[defined_class] || {}
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'appmap/util'
|
4
|
+
|
5
|
+
module AppMap
|
6
|
+
module Cucumber
|
7
|
+
ScenarioAttributes = Struct.new(:name, :feature, :feature_group)
|
8
|
+
|
9
|
+
ProviderStruct = Struct.new(:scenario) do
|
10
|
+
def feature_group
|
11
|
+
# e.g. <Cucumber::Core::Ast::Location::Precise: cucumber/api/features/authenticate.feature:1>
|
12
|
+
feature_path.split('/').last.split('.')[0]
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# ProviderBefore4 provides scenario name, feature name, and feature group name for Cucumber
|
17
|
+
# versions before 4.0.
|
18
|
+
class ProviderBefore4 < ProviderStruct
|
19
|
+
def attributes
|
20
|
+
ScenarioAttributes.new(scenario.name, scenario.feature.name, feature_group)
|
21
|
+
end
|
22
|
+
|
23
|
+
def feature_path
|
24
|
+
scenario.feature.location.to_s
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Provider4 provides scenario name, feature name, and feature group name for Cucumber
|
29
|
+
# versions 4.0 and later.
|
30
|
+
class Provider4 < ProviderStruct
|
31
|
+
def attributes
|
32
|
+
ScenarioAttributes.new(scenario.name, scenario.name.split(' ')[0..1].join(' '), feature_group)
|
33
|
+
end
|
34
|
+
|
35
|
+
def feature_path
|
36
|
+
scenario.location.file
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class << self
|
41
|
+
def write_scenario(scenario, appmap)
|
42
|
+
appmap['metadata'] = update_metadata(scenario, appmap['metadata'])
|
43
|
+
scenario_filename = AppMap::Util.scenario_filename(appmap['metadata']['name'])
|
44
|
+
|
45
|
+
FileUtils.mkdir_p 'tmp/appmap/cucumber'
|
46
|
+
File.write(File.join('tmp/appmap/cucumber', scenario_filename), JSON.generate(appmap))
|
47
|
+
end
|
48
|
+
|
49
|
+
def enabled?
|
50
|
+
ENV['APPMAP'] == 'true'
|
51
|
+
end
|
52
|
+
|
53
|
+
protected
|
54
|
+
|
55
|
+
def cucumber_version
|
56
|
+
Gem.loaded_specs['cucumber']&.version&.to_s
|
57
|
+
end
|
58
|
+
|
59
|
+
def provider(scenario)
|
60
|
+
major, = cucumber_version.split('.').map(&:to_i)
|
61
|
+
if major < 4
|
62
|
+
ProviderBefore4
|
63
|
+
else
|
64
|
+
Provider4
|
65
|
+
end.new(scenario)
|
66
|
+
end
|
67
|
+
|
68
|
+
def update_metadata(scenario, base_metadata)
|
69
|
+
attributes = provider(scenario).attributes
|
70
|
+
|
71
|
+
base_metadata.tap do |m|
|
72
|
+
m['name'] = attributes.name
|
73
|
+
m['feature'] = attributes.feature
|
74
|
+
m['feature_group'] = attributes.feature_group
|
75
|
+
m['labels'] ||= []
|
76
|
+
m['labels'] += (scenario.tags&.map(&:name) || [])
|
77
|
+
m['frameworks'] ||= []
|
78
|
+
m['frameworks'] << {
|
79
|
+
'name' => 'cucumber',
|
80
|
+
'version' => Gem.loaded_specs['cucumber']&.version&.to_s
|
81
|
+
}
|
82
|
+
m['recorder'] = {
|
83
|
+
'name' => 'cucumber'
|
84
|
+
}
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
data/lib/appmap/event.rb
CHANGED
@@ -15,15 +15,13 @@ module AppMap
|
|
15
15
|
end
|
16
16
|
end
|
17
17
|
|
18
|
-
MethodEventStruct = Struct.new(:id, :event, :defined_class, :method_id, :path, :lineno, :
|
18
|
+
MethodEventStruct = Struct.new(:id, :event, :defined_class, :method_id, :path, :lineno, :thread_id)
|
19
19
|
|
20
20
|
class MethodEvent < MethodEventStruct
|
21
21
|
LIMIT = 100
|
22
22
|
|
23
23
|
class << self
|
24
24
|
def build_from_invocation(me, event_type, defined_class, method)
|
25
|
-
singleton = method.owner.singleton_class?
|
26
|
-
|
27
25
|
me.id = AppMap::Event.next_id_counter
|
28
26
|
me.event = event_type
|
29
27
|
me.defined_class = defined_class
|
@@ -32,7 +30,6 @@ module AppMap
|
|
32
30
|
path = path[Dir.pwd.length + 1..-1] if path.index(Dir.pwd) == 0
|
33
31
|
me.path = path
|
34
32
|
me.lineno = method.source_location[1]
|
35
|
-
me.static = singleton
|
36
33
|
me.thread_id = Thread.current.object_id
|
37
34
|
end
|
38
35
|
|
@@ -62,11 +59,10 @@ module AppMap
|
|
62
59
|
end
|
63
60
|
end
|
64
61
|
|
65
|
-
alias static? static
|
66
62
|
end
|
67
63
|
|
68
64
|
class MethodCall < MethodEvent
|
69
|
-
attr_accessor :parameters, :receiver
|
65
|
+
attr_accessor :parameters, :receiver, :static
|
70
66
|
|
71
67
|
class << self
|
72
68
|
def build_from_invocation(mc = MethodCall.new, defined_class, method, receiver, arguments)
|
@@ -88,6 +84,7 @@ module AppMap
|
|
88
84
|
object_id: receiver.__id__,
|
89
85
|
value: display_string(receiver)
|
90
86
|
}
|
87
|
+
mc.static = receiver.is_a?(Module)
|
91
88
|
MethodEvent.build_from_invocation(mc, :call, defined_class, method)
|
92
89
|
end
|
93
90
|
end
|
@@ -95,10 +92,13 @@ module AppMap
|
|
95
92
|
|
96
93
|
def to_h
|
97
94
|
super.tap do |h|
|
95
|
+
h[:static] = static
|
98
96
|
h[:parameters] = parameters
|
99
97
|
h[:receiver] = receiver
|
100
98
|
end
|
101
99
|
end
|
100
|
+
|
101
|
+
alias static? static
|
102
102
|
end
|
103
103
|
|
104
104
|
class MethodReturnIgnoreValue < MethodEvent
|
data/lib/appmap/hook.rb
CHANGED
@@ -6,147 +6,125 @@ module AppMap
|
|
6
6
|
class Hook
|
7
7
|
LOG = false
|
8
8
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
9
|
+
HOOK_DISABLE_KEY = 'AppMap::Hook.disable'
|
10
|
+
|
11
|
+
class << self
|
12
|
+
# Return the class, separator ('.' or '#'), and method name for
|
13
|
+
# the given method.
|
14
|
+
def qualify_method_name(method)
|
15
|
+
if method.owner.singleton_class?
|
16
|
+
# Singleton class names can take two forms:
|
17
|
+
# #<Class:Foo> or
|
18
|
+
# #<Class:#<Bar:0x0123ABC>>. Retrieve the name of
|
19
|
+
# the class from the string.
|
20
|
+
#
|
21
|
+
# (There really isn't a better way to do this. The
|
22
|
+
# singleton's reference to the class it was created
|
23
|
+
# from is stored in an instance variable named
|
24
|
+
# '__attached__'. It doesn't have the '@' prefix, so
|
25
|
+
# it's internal only, and not accessible from user
|
26
|
+
# code.)
|
27
|
+
class_name = /#<Class:((#<(?<cls>.*?):)|((?<cls>.*?)>))/.match(method.owner.to_s)['cls']
|
28
|
+
[ class_name, '.', method.name ]
|
29
|
+
else
|
30
|
+
[ method.owner.name, '#', method.name ]
|
31
|
+
end
|
15
32
|
end
|
16
33
|
end
|
17
34
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
require 'yaml'
|
23
|
-
load YAML.safe_load(::File.read(config_file_name))
|
24
|
-
end
|
35
|
+
attr_reader :config
|
36
|
+
def initialize(config)
|
37
|
+
@config = config
|
38
|
+
end
|
25
39
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
40
|
+
# Observe class loading and hook all methods which match the config.
|
41
|
+
def enable &block
|
42
|
+
before_hook = lambda do |defined_class, method, receiver, args|
|
43
|
+
require 'appmap/event'
|
44
|
+
call_event = AppMap::Event::MethodCall.build_from_invocation(defined_class, method, receiver, args)
|
45
|
+
AppMap.tracing.record_event call_event, defined_class: defined_class, method: method
|
46
|
+
[ call_event, Time.now ]
|
33
47
|
end
|
34
48
|
|
35
|
-
|
36
|
-
|
49
|
+
after_hook = lambda do |call_event, defined_class, method, start_time, return_value, exception|
|
50
|
+
require 'appmap/event'
|
51
|
+
elapsed = Time.now - start_time
|
52
|
+
return_event = AppMap::Event::MethodReturn.build_from_invocation \
|
53
|
+
defined_class, method, call_event.id, elapsed, return_value, exception
|
54
|
+
AppMap.tracing.record_event return_event
|
37
55
|
end
|
38
56
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
57
|
+
with_disabled_hook = lambda do |&fn|
|
58
|
+
# Don't record functions, such as to_s and inspect, that might be called
|
59
|
+
# by the fn. Otherwise there can be a stack overflow.
|
60
|
+
Thread.current[HOOK_DISABLE_KEY] = true
|
61
|
+
begin
|
62
|
+
fn.call
|
63
|
+
ensure
|
64
|
+
Thread.current[HOOK_DISABLE_KEY] = false
|
65
|
+
end
|
44
66
|
end
|
45
|
-
end
|
46
67
|
|
47
|
-
|
68
|
+
tp = TracePoint.new(:end) do |tp|
|
69
|
+
hook = self
|
70
|
+
cls = tp.self
|
48
71
|
|
49
|
-
|
50
|
-
|
51
|
-
def hook(config = AppMap.configure)
|
52
|
-
package_include_paths = config.packages.map(&:path)
|
53
|
-
package_exclude_paths = config.packages.map do |pkg|
|
54
|
-
pkg.exclude.map do |exclude|
|
55
|
-
File.join(pkg.path, exclude)
|
56
|
-
end
|
57
|
-
end.flatten
|
72
|
+
instance_methods = cls.public_instance_methods(false)
|
73
|
+
class_methods = cls.singleton_class.public_instance_methods(false) - instance_methods
|
58
74
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
AppMap.tracing.record_event call_event, defined_class: defined_class, method: method
|
63
|
-
[ call_event, Time.now ]
|
64
|
-
end
|
75
|
+
hook_method = lambda do |cls|
|
76
|
+
lambda do |method_id|
|
77
|
+
next if method_id.to_s =~ /_hooked_by_appmap$/
|
65
78
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
defined_class, method, call_event.id, elapsed, return_value, exception
|
71
|
-
AppMap.tracing.record_event return_event
|
72
|
-
end
|
79
|
+
method = cls.public_instance_method(method_id)
|
80
|
+
disasm = RubyVM::InstructionSequence.disasm(method)
|
81
|
+
# Skip methods that have no instruction sequence, as they are obviously trivial.
|
82
|
+
next unless disasm
|
73
83
|
|
74
|
-
|
75
|
-
|
76
|
-
# by the fn. Otherwise there can be a stack oveflow.
|
77
|
-
Thread.current[HOOK_DISABLE_KEY] = true
|
78
|
-
begin
|
79
|
-
fn.call
|
80
|
-
ensure
|
81
|
-
Thread.current[HOOK_DISABLE_KEY] = false
|
82
|
-
end
|
83
|
-
end
|
84
|
+
defined_class, method_symbol, method_name = Hook.qualify_method_name(method)
|
85
|
+
method_display_name = [defined_class, method_symbol, method_name].join
|
84
86
|
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
instance_methods = cls.public_instance_methods(false)
|
89
|
-
class_methods = cls.singleton_class.public_instance_methods(false) - instance_methods
|
90
|
-
|
91
|
-
hook_method = lambda do |cls|
|
92
|
-
lambda do |method_id|
|
93
|
-
next if method_id.to_s =~ /_hooked_by_appmap$/
|
94
|
-
|
95
|
-
method = cls.public_instance_method(method_id)
|
96
|
-
location = method.source_location
|
97
|
-
location_file, = location
|
98
|
-
next unless location_file
|
99
|
-
|
100
|
-
location_file = location_file[Dir.pwd.length + 1..-1] if location_file.index(Dir.pwd) == 0
|
101
|
-
match = package_include_paths.find { |p| location_file.index(p) == 0 }
|
102
|
-
match &&= !package_exclude_paths.find { |p| location_file.index(p) }
|
103
|
-
next unless match
|
104
|
-
|
105
|
-
disasm = RubyVM::InstructionSequence.disasm(method)
|
106
|
-
# Skip methods that have no instruction sequence, as they are obviously trivial.
|
107
|
-
next unless disasm
|
108
|
-
|
109
|
-
defined_class, method_symbol = \
|
110
|
-
if method.owner.singleton_class?
|
111
|
-
# Singleton class name is like: #<Class:<(.*)>>
|
112
|
-
class_name = method.owner.to_s['#<Class:<'.length-1..-2]
|
113
|
-
[ class_name, '.' ]
|
114
|
-
else
|
115
|
-
[ method.owner.name, '#' ]
|
116
|
-
end
|
87
|
+
# Don't try and trace the AppMap methods or there will be
|
88
|
+
# a stack overflow in the defined hook method.
|
89
|
+
next if /\AAppMap[:\.]/.match?(method_display_name)
|
117
90
|
|
118
|
-
|
91
|
+
next unless \
|
92
|
+
config.always_hook?(defined_class, method_name) ||
|
93
|
+
config.included_by_location?(method)
|
119
94
|
|
120
|
-
|
121
|
-
base_method = method.bind(self).to_proc
|
95
|
+
warn "AppMap: Hooking #{method_display_name}" if LOG
|
122
96
|
|
123
|
-
|
124
|
-
|
125
|
-
return base_method.call(*args, &block) unless enabled
|
97
|
+
cls.define_method method_id do |*args, &block|
|
98
|
+
base_method = method.bind(self).to_proc
|
126
99
|
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
100
|
+
hook_disabled = Thread.current[HOOK_DISABLE_KEY]
|
101
|
+
enabled = true if !hook_disabled && AppMap.tracing.enabled?
|
102
|
+
return base_method.call(*args, &block) unless enabled
|
103
|
+
|
104
|
+
call_event, start_time = with_disabled_hook.call do
|
105
|
+
before_hook.call(defined_class, method, self, args)
|
106
|
+
end
|
107
|
+
return_value = nil
|
108
|
+
exception = nil
|
109
|
+
begin
|
110
|
+
return_value = base_method.call(*args, &block)
|
111
|
+
rescue
|
112
|
+
exception = $ERROR_INFO
|
113
|
+
raise
|
114
|
+
ensure
|
115
|
+
with_disabled_hook.call do
|
116
|
+
after_hook.call(call_event, defined_class, method, start_time, return_value, exception)
|
141
117
|
end
|
142
118
|
end
|
143
119
|
end
|
144
120
|
end
|
145
|
-
|
146
|
-
instance_methods.each(&hook_method.call(cls))
|
147
|
-
class_methods.each(&hook_method.call(cls.singleton_class))
|
148
121
|
end
|
122
|
+
|
123
|
+
instance_methods.each(&hook_method.call(cls))
|
124
|
+
class_methods.each(&hook_method.call(cls.singleton_class))
|
149
125
|
end
|
126
|
+
|
127
|
+
tp.enable(&block)
|
150
128
|
end
|
151
129
|
end
|
152
130
|
end
|