appmap 0.23.0 → 0.27.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 +2 -0
- data/.rubocop.yml +17 -8
- data/.travis.yml +6 -0
- data/CHANGELOG.md +43 -0
- data/README.md +33 -21
- data/Rakefile +3 -3
- data/appmap.gemspec +3 -1
- data/exe/appmap +5 -73
- data/lib/appmap.rb +61 -6
- data/lib/appmap/algorithm/prune_class_map.rb +2 -0
- data/lib/appmap/algorithm/stats.rb +4 -2
- data/lib/appmap/class_map.rb +143 -0
- data/lib/appmap/command/record.rb +8 -6
- data/lib/appmap/command/stats.rb +2 -0
- data/lib/appmap/event.rb +168 -0
- data/lib/appmap/hook.rb +152 -0
- data/lib/appmap/middleware/remote_recording.rb +14 -21
- data/lib/appmap/rails/action_handler.rb +10 -6
- data/lib/appmap/rails/sql_handler.rb +10 -13
- data/lib/appmap/railtie.rb +31 -18
- data/lib/appmap/rspec.rb +247 -260
- data/lib/appmap/trace.rb +88 -0
- data/lib/appmap/version.rb +1 -1
- data/package-lock.json +90 -92
- data/spec/abstract_controller4_base_spec.rb +1 -1
- data/spec/abstract_controller_base_spec.rb +7 -3
- data/spec/config_spec.rb +25 -0
- data/spec/fixtures/hook/attr_accessor.rb +5 -0
- data/spec/fixtures/hook/class_method.rb +17 -0
- data/spec/fixtures/hook/constructor.rb +7 -0
- data/spec/fixtures/hook/exception_method.rb +11 -0
- data/spec/fixtures/hook/instance_method.rb +23 -0
- data/spec/fixtures/rails4_users_app/app/controllers/api/users_controller.rb +3 -3
- data/spec/fixtures/rails4_users_app/config/database.yml +2 -1
- data/spec/fixtures/rails4_users_app/docker-compose.yml +2 -0
- data/spec/fixtures/rails_users_app/.ruby-version +1 -1
- data/spec/fixtures/rails_users_app/app/controllers/api/users_controller.rb +2 -2
- data/spec/fixtures/rails_users_app/config/database.yml +2 -1
- data/spec/fixtures/rails_users_app/create_app +1 -0
- data/spec/fixtures/rails_users_app/docker-compose.yml +4 -0
- data/spec/fixtures/rails_users_app/spec/models/user_spec.rb +1 -1
- data/spec/hook_spec.rb +369 -0
- data/spec/rails_spec_helper.rb +25 -16
- data/spec/railtie_spec.rb +1 -1
- data/spec/record_sql_rails_pg_spec.rb +1 -2
- data/spec/remote_recording_spec.rb +117 -0
- data/spec/spec_helper.rb +5 -0
- data/test/cli_test.rb +4 -46
- data/test/fixtures/cli_record_test/appmap.yml +2 -1
- data/test/fixtures/cli_record_test/lib/cli_record_test/main.rb +4 -2
- data/test/fixtures/rspec_recorder/Gemfile +1 -1
- data/test/fixtures/rspec_recorder/spec/decorated_hello_spec.rb +12 -0
- data/test/rspec_test.rb +5 -0
- data/test/test_helper.rb +0 -42
- metadata +46 -63
- data/exe/_appmap-record-self +0 -49
- data/lib/appmap/command/inspect.rb +0 -14
- data/lib/appmap/command/upload.rb +0 -99
- data/lib/appmap/config.rb +0 -65
- data/lib/appmap/config/directory.rb +0 -65
- data/lib/appmap/config/file.rb +0 -13
- data/lib/appmap/config/named_function.rb +0 -21
- data/lib/appmap/config/package_dir.rb +0 -52
- data/lib/appmap/config/path.rb +0 -25
- data/lib/appmap/feature.rb +0 -262
- data/lib/appmap/inspect.rb +0 -91
- data/lib/appmap/inspect/inspector.rb +0 -99
- data/lib/appmap/inspect/parse_node.rb +0 -170
- data/lib/appmap/inspect/parser.rb +0 -15
- data/lib/appmap/parser.rb +0 -60
- data/lib/appmap/rspec/parse_node.rb +0 -41
- data/lib/appmap/rspec/parser.rb +0 -15
- data/lib/appmap/trace/event_handler/rack_handler_webrick.rb +0 -65
- data/lib/appmap/trace/tracer.rb +0 -356
- data/spec/fixtures/rails_users_app/bin/_appmap-record-self +0 -29
- data/spec/rack_handler_webrick_spec.rb +0 -59
- data/test/config_test.rb +0 -149
- data/test/explict_inspect_test.rb +0 -29
- data/test/fixtures/active_record_like/active_record.rb +0 -2
- data/test/fixtures/active_record_like/active_record/aggregations.rb +0 -4
- data/test/fixtures/active_record_like/active_record/association.rb +0 -4
- data/test/fixtures/active_record_like/active_record/associations/join_dependency.rb +0 -6
- data/test/fixtures/active_record_like/active_record/associations/join_dependency/join_base.rb +0 -8
- data/test/fixtures/active_record_like/active_record/associations/join_dependency/join_part.rb +0 -8
- data/test/fixtures/active_record_like/active_record/caps/caps.rb +0 -4
- data/test/fixtures/ignore_non_ruby_file/class.rb +0 -3
- data/test/fixtures/ignore_non_ruby_file/non-ruby.txt +0 -1
- data/test/fixtures/includes_excludes/lib/a/a_1.rb +0 -6
- data/test/fixtures/includes_excludes/lib/a/a_2.rb +0 -6
- data/test/fixtures/includes_excludes/lib/a/x/x_1.rb +0 -8
- data/test/fixtures/includes_excludes/lib/b/b_1.rb +0 -6
- data/test/fixtures/includes_excludes/lib/root_1.rb +0 -4
- data/test/fixtures/inspect_multiple_subdirs/module_a.rb +0 -2
- data/test/fixtures/inspect_multiple_subdirs/module_a/class_a.rb +0 -5
- data/test/fixtures/inspect_multiple_subdirs/module_b.rb +0 -2
- data/test/fixtures/inspect_multiple_subdirs/module_b/class_b.rb +0 -5
- data/test/fixtures/inspect_multiple_subdirs/module_b/class_c.rb +0 -5
- data/test/fixtures/inspect_package/module_a/module_b/class_in_module.rb +0 -6
- data/test/fixtures/parse_file/defs_static_function.rb +0 -96
- data/test/fixtures/parse_file/function_within_class.rb +0 -36
- data/test/fixtures/parse_file/include_public_methods.rb +0 -127
- data/test/fixtures/parse_file/instance_function.rb +0 -17
- data/test/fixtures/parse_file/modules.rb +0 -71
- data/test/fixtures/parse_file/sclass_static_function.rb +0 -88
- data/test/fixtures/parse_file/toplevel_class.rb +0 -13
- data/test/fixtures/parse_file/toplevel_function.rb +0 -14
- data/test/fixtures/trace_test/trace_program_1.rb +0 -44
- data/test/implicit_inspect_test.rb +0 -33
- data/test/include_exclude_test.rb +0 -48
- data/test/prerecorded_trace_test.rb +0 -76
- data/test/trace_test.rb +0 -92
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module AppMap
|
|
2
4
|
module Algorithm
|
|
3
5
|
StatsStruct = Struct.new(:appmap)
|
|
@@ -31,7 +33,7 @@ module AppMap
|
|
|
31
33
|
comparator = ->(a,b) { b.count <=> a.count }
|
|
32
34
|
class_frequency.sort!(&comparator)
|
|
33
35
|
method_frequency.sort!(&comparator)
|
|
34
|
-
|
|
36
|
+
|
|
35
37
|
self
|
|
36
38
|
end
|
|
37
39
|
|
|
@@ -55,7 +57,7 @@ module AppMap
|
|
|
55
57
|
end
|
|
56
58
|
end
|
|
57
59
|
Frequency = Struct.new(:name, :count)
|
|
58
|
-
|
|
60
|
+
|
|
59
61
|
def perform(limit: nil)
|
|
60
62
|
events = appmap['events'] || []
|
|
61
63
|
frequency_calc = lambda do |key_func|
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_support/core_ext'
|
|
4
|
+
|
|
5
|
+
module AppMap
|
|
6
|
+
class ClassMap
|
|
7
|
+
module HasChildren
|
|
8
|
+
def self.included(base)
|
|
9
|
+
base.module_eval do
|
|
10
|
+
def children
|
|
11
|
+
@children ||= []
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
module Types
|
|
18
|
+
class Root
|
|
19
|
+
include HasChildren
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
Package = Struct.new(:name) do
|
|
23
|
+
include HasChildren
|
|
24
|
+
|
|
25
|
+
def type
|
|
26
|
+
'package'
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def to_h
|
|
30
|
+
{
|
|
31
|
+
name: name,
|
|
32
|
+
type: type,
|
|
33
|
+
children: children.map(&:to_h)
|
|
34
|
+
}
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
Class = Struct.new(:name) do
|
|
38
|
+
include HasChildren
|
|
39
|
+
|
|
40
|
+
def type
|
|
41
|
+
'class'
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def to_h
|
|
45
|
+
{
|
|
46
|
+
name: name,
|
|
47
|
+
type: type,
|
|
48
|
+
children: children.map(&:to_h)
|
|
49
|
+
}
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
Function = Struct.new(:name) do
|
|
53
|
+
attr_accessor :static, :location
|
|
54
|
+
|
|
55
|
+
def type
|
|
56
|
+
'function'
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def to_h
|
|
60
|
+
{
|
|
61
|
+
name: name,
|
|
62
|
+
type: type,
|
|
63
|
+
location: location,
|
|
64
|
+
static: static
|
|
65
|
+
}
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
class << self
|
|
71
|
+
def build_from_methods(config, methods)
|
|
72
|
+
root = Types::Root.new
|
|
73
|
+
methods.each do |method|
|
|
74
|
+
package = package_for_method(config.packages, method)
|
|
75
|
+
add_function root, package.path, method
|
|
76
|
+
end
|
|
77
|
+
root.children.map(&:to_h)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
protected
|
|
81
|
+
|
|
82
|
+
def package_for_method(packages, method)
|
|
83
|
+
location = method.method.source_location
|
|
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
|
|
95
|
+
location_file, lineno = location
|
|
96
|
+
location_file = location_file[Dir.pwd.length + 1..-1] if location_file.index(Dir.pwd) == 0
|
|
97
|
+
|
|
98
|
+
static = method.method.owner.singleton_class?
|
|
99
|
+
|
|
100
|
+
object_infos = [
|
|
101
|
+
{
|
|
102
|
+
name: package_name,
|
|
103
|
+
type: 'package'
|
|
104
|
+
}
|
|
105
|
+
]
|
|
106
|
+
object_infos += method.defined_class.split('::').map do |name|
|
|
107
|
+
{
|
|
108
|
+
name: name,
|
|
109
|
+
type: 'class'
|
|
110
|
+
}
|
|
111
|
+
end
|
|
112
|
+
object_infos << {
|
|
113
|
+
name: method.method.name,
|
|
114
|
+
type: 'function',
|
|
115
|
+
location: [ location_file, lineno ].join(':'),
|
|
116
|
+
static: static
|
|
117
|
+
}
|
|
118
|
+
parent = root
|
|
119
|
+
object_infos.each do |info|
|
|
120
|
+
parent = find_or_create parent.children, info do
|
|
121
|
+
Types.const_get(info[:type].classify).new(info[:name].to_s).tap do |type|
|
|
122
|
+
info.keys.tap do |keys|
|
|
123
|
+
keys.delete(:name)
|
|
124
|
+
keys.delete(:type)
|
|
125
|
+
end.each do |key|
|
|
126
|
+
type.send "#{key}=", info[key]
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def find_or_create(list, info)
|
|
134
|
+
obj = list.find { |item| item.type == info[:type] && item.name == info[:name] }
|
|
135
|
+
return obj if obj
|
|
136
|
+
|
|
137
|
+
yield.tap do |new_obj|
|
|
138
|
+
list << new_obj
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module AppMap
|
|
2
4
|
module Command
|
|
3
5
|
RecordStruct = Struct.new(:config, :program)
|
|
@@ -61,12 +63,9 @@ module AppMap
|
|
|
61
63
|
end
|
|
62
64
|
|
|
63
65
|
def perform(&block)
|
|
64
|
-
|
|
65
|
-
functions = features.map(&:collect_functions).flatten
|
|
66
|
-
|
|
67
|
-
require 'appmap/trace/tracer'
|
|
66
|
+
AppMap::Hook.hook(config)
|
|
68
67
|
|
|
69
|
-
tracer = AppMap
|
|
68
|
+
tracer = AppMap.tracing.trace
|
|
70
69
|
|
|
71
70
|
events = []
|
|
72
71
|
quit = false
|
|
@@ -85,7 +84,10 @@ module AppMap
|
|
|
85
84
|
at_exit do
|
|
86
85
|
quit = true
|
|
87
86
|
event_thread.join
|
|
88
|
-
yield
|
|
87
|
+
yield AppMap::APPMAP_FORMAT_VERSION,
|
|
88
|
+
self.class.detect_metadata,
|
|
89
|
+
AppMap.class_map(config, tracer.event_methods),
|
|
90
|
+
events
|
|
89
91
|
end
|
|
90
92
|
|
|
91
93
|
load program if program
|
data/lib/appmap/command/stats.rb
CHANGED
data/lib/appmap/event.rb
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AppMap
|
|
4
|
+
module Event
|
|
5
|
+
@@id_counter = 0
|
|
6
|
+
|
|
7
|
+
class << self
|
|
8
|
+
# reset_id_counter is used by test cases to get consistent event ids.
|
|
9
|
+
def reset_id_counter
|
|
10
|
+
@@id_counter = 0
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def next_id_counter
|
|
14
|
+
@@id_counter += 1
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
MethodEventStruct = Struct.new(:id, :event, :defined_class, :method_id, :path, :lineno, :static, :thread_id)
|
|
19
|
+
|
|
20
|
+
class MethodEvent < MethodEventStruct
|
|
21
|
+
LIMIT = 100
|
|
22
|
+
|
|
23
|
+
class << self
|
|
24
|
+
def build_from_invocation(me, event_type, defined_class, method)
|
|
25
|
+
singleton = method.owner.singleton_class?
|
|
26
|
+
|
|
27
|
+
me.id = AppMap::Event.next_id_counter
|
|
28
|
+
me.event = event_type
|
|
29
|
+
me.defined_class = defined_class
|
|
30
|
+
me.method_id = method.name.to_s
|
|
31
|
+
path = method.source_location[0]
|
|
32
|
+
path = path[Dir.pwd.length + 1..-1] if path.index(Dir.pwd) == 0
|
|
33
|
+
me.path = path
|
|
34
|
+
me.lineno = method.source_location[1]
|
|
35
|
+
me.static = singleton
|
|
36
|
+
me.thread_id = Thread.current.object_id
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Gets a display string for a value. This is not meant to be a machine deserializable value.
|
|
40
|
+
def display_string(value)
|
|
41
|
+
return nil unless value
|
|
42
|
+
|
|
43
|
+
last_resort_string = lambda do
|
|
44
|
+
warn "AppMap encountered an error inspecting a #{value.class.name}: #{$!.message}"
|
|
45
|
+
'*Error inspecting variable*'
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
value_string = \
|
|
49
|
+
begin
|
|
50
|
+
value.to_s
|
|
51
|
+
rescue NoMethodError
|
|
52
|
+
begin
|
|
53
|
+
value.inspect
|
|
54
|
+
rescue StandardError
|
|
55
|
+
last_resort_string.call
|
|
56
|
+
end
|
|
57
|
+
rescue StandardError
|
|
58
|
+
last_resort_string.call
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
(value_string||'')[0...LIMIT].encode('utf-8', invalid: :replace, undef: :replace, replace: '_')
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
alias static? static
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
class MethodCall < MethodEvent
|
|
69
|
+
attr_accessor :parameters, :receiver
|
|
70
|
+
|
|
71
|
+
class << self
|
|
72
|
+
def build_from_invocation(mc = MethodCall.new, defined_class, method, receiver, arguments)
|
|
73
|
+
mc.tap do
|
|
74
|
+
mc.parameters = method.parameters.map.with_index do |method_param, idx|
|
|
75
|
+
param_type, param_name = method_param
|
|
76
|
+
param_name ||= 'arg'
|
|
77
|
+
value = arguments[idx]
|
|
78
|
+
{
|
|
79
|
+
name: param_name,
|
|
80
|
+
class: value.class.name,
|
|
81
|
+
object_id: value.__id__,
|
|
82
|
+
value: display_string(value),
|
|
83
|
+
kind: param_type
|
|
84
|
+
}
|
|
85
|
+
end
|
|
86
|
+
mc.receiver = {
|
|
87
|
+
class: receiver.class.name,
|
|
88
|
+
object_id: receiver.__id__,
|
|
89
|
+
value: display_string(receiver)
|
|
90
|
+
}
|
|
91
|
+
MethodEvent.build_from_invocation(mc, :call, defined_class, method)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def to_h
|
|
97
|
+
super.tap do |h|
|
|
98
|
+
h[:parameters] = parameters
|
|
99
|
+
h[:receiver] = receiver
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
class MethodReturnIgnoreValue < MethodEvent
|
|
105
|
+
attr_accessor :parent_id, :elapsed
|
|
106
|
+
|
|
107
|
+
class << self
|
|
108
|
+
def build_from_invocation(mr = MethodReturnIgnoreValue.new, defined_class, method, parent_id, elapsed)
|
|
109
|
+
mr.tap do |_|
|
|
110
|
+
mr.parent_id = parent_id
|
|
111
|
+
mr.elapsed = elapsed
|
|
112
|
+
MethodEvent.build_from_invocation(mr, :return, defined_class, method)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def to_h
|
|
118
|
+
super.tap do |h|
|
|
119
|
+
h[:parent_id] = parent_id
|
|
120
|
+
h[:elapsed] = elapsed
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
class MethodReturn < MethodReturnIgnoreValue
|
|
126
|
+
attr_accessor :return_value, :exceptions
|
|
127
|
+
|
|
128
|
+
class << self
|
|
129
|
+
def build_from_invocation(mr = MethodReturn.new, defined_class, method, parent_id, elapsed, return_value, exception)
|
|
130
|
+
mr.tap do |_|
|
|
131
|
+
if return_value
|
|
132
|
+
mr.return_value = {
|
|
133
|
+
class: return_value.class.name,
|
|
134
|
+
value: display_string(return_value),
|
|
135
|
+
object_id: return_value.__id__
|
|
136
|
+
}
|
|
137
|
+
end
|
|
138
|
+
if exception
|
|
139
|
+
next_exception = exception
|
|
140
|
+
exceptions = []
|
|
141
|
+
while next_exception
|
|
142
|
+
exception_backtrace = next_exception.backtrace_locations.try(:[], 0)
|
|
143
|
+
exceptions << {
|
|
144
|
+
class: next_exception.class.name,
|
|
145
|
+
message: next_exception.message,
|
|
146
|
+
object_id: next_exception.__id__,
|
|
147
|
+
path: exception_backtrace&.path,
|
|
148
|
+
lineno: exception_backtrace&.lineno
|
|
149
|
+
}.compact
|
|
150
|
+
next_exception = next_exception.cause
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
mr.exceptions = exceptions
|
|
154
|
+
end
|
|
155
|
+
MethodReturnIgnoreValue.build_from_invocation(mr, defined_class, method, parent_id, elapsed)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def to_h
|
|
161
|
+
super.tap do |h|
|
|
162
|
+
h[:return_value] = return_value if return_value
|
|
163
|
+
h[:exceptions] = exceptions if exceptions
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
data/lib/appmap/hook.rb
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'English'
|
|
4
|
+
|
|
5
|
+
module AppMap
|
|
6
|
+
class Hook
|
|
7
|
+
LOG = false
|
|
8
|
+
|
|
9
|
+
Package = Struct.new(:path, :exclude) do
|
|
10
|
+
def to_h
|
|
11
|
+
{
|
|
12
|
+
path: path,
|
|
13
|
+
exclude: exclude.blank? ? nil : exclude
|
|
14
|
+
}.compact
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
Config = Struct.new(:name, :packages) do
|
|
19
|
+
class << self
|
|
20
|
+
# Loads configuration data from a file, specified by the file name.
|
|
21
|
+
def load_from_file(config_file_name)
|
|
22
|
+
require 'yaml'
|
|
23
|
+
load YAML.safe_load(::File.read(config_file_name))
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Loads configuration from a Hash.
|
|
27
|
+
def load(config_data)
|
|
28
|
+
packages = (config_data['packages'] || []).map do |package|
|
|
29
|
+
Package.new(package['path'], package['exclude'] || [])
|
|
30
|
+
end
|
|
31
|
+
Config.new config_data['name'], packages
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def initialize(name, packages = [])
|
|
36
|
+
super name, packages || []
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def to_h
|
|
40
|
+
{
|
|
41
|
+
name: name,
|
|
42
|
+
packages: packages.map(&:to_h)
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
HOOK_DISABLE_KEY = 'AppMap::Hook.disable'
|
|
48
|
+
|
|
49
|
+
class << self
|
|
50
|
+
# Observe class loading and hook all methods which match the config.
|
|
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
|
|
58
|
+
|
|
59
|
+
before_hook = lambda do |defined_class, method, receiver, args|
|
|
60
|
+
require 'appmap/event'
|
|
61
|
+
call_event = AppMap::Event::MethodCall.build_from_invocation(defined_class, method, receiver, args)
|
|
62
|
+
AppMap.tracing.record_event call_event, defined_class: defined_class, method: method
|
|
63
|
+
[ call_event, Time.now ]
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
after_hook = lambda do |call_event, defined_class, method, start_time, return_value, exception|
|
|
67
|
+
require 'appmap/event'
|
|
68
|
+
elapsed = Time.now - start_time
|
|
69
|
+
return_event = AppMap::Event::MethodReturn.build_from_invocation \
|
|
70
|
+
defined_class, method, call_event.id, elapsed, return_value, exception
|
|
71
|
+
AppMap.tracing.record_event return_event
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
with_disabled_hook = lambda do |&fn|
|
|
75
|
+
# Don't record functions, such as to_s and inspect, that might be called
|
|
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
|
+
|
|
85
|
+
TracePoint.trace(:end) do |tp|
|
|
86
|
+
cls = tp.self
|
|
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
|
|
117
|
+
|
|
118
|
+
warn "AppMap: Hooking #{defined_class}#{method_symbol}#{method.name}" if LOG
|
|
119
|
+
|
|
120
|
+
cls.define_method method_id do |*args, &block|
|
|
121
|
+
base_method = method.bind(self).to_proc
|
|
122
|
+
|
|
123
|
+
hook_disabled = Thread.current[HOOK_DISABLE_KEY]
|
|
124
|
+
enabled = true if !hook_disabled && AppMap.tracing.enabled?
|
|
125
|
+
return base_method.call(*args, &block) unless enabled
|
|
126
|
+
|
|
127
|
+
call_event, start_time = with_disabled_hook.call do
|
|
128
|
+
before_hook.call(defined_class, method, self, args)
|
|
129
|
+
end
|
|
130
|
+
return_value = nil
|
|
131
|
+
exception = nil
|
|
132
|
+
begin
|
|
133
|
+
return_value = base_method.call(*args, &block)
|
|
134
|
+
rescue
|
|
135
|
+
exception = $ERROR_INFO
|
|
136
|
+
raise
|
|
137
|
+
ensure
|
|
138
|
+
with_disabled_hook.call do
|
|
139
|
+
after_hook.call(call_event, defined_class, method, start_time, return_value, exception)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
instance_methods.each(&hook_method.call(cls))
|
|
147
|
+
class_methods.each(&hook_method.call(cls.singleton_class))
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|