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.
Files changed (112) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/.rubocop.yml +17 -8
  4. data/.travis.yml +6 -0
  5. data/CHANGELOG.md +43 -0
  6. data/README.md +33 -21
  7. data/Rakefile +3 -3
  8. data/appmap.gemspec +3 -1
  9. data/exe/appmap +5 -73
  10. data/lib/appmap.rb +61 -6
  11. data/lib/appmap/algorithm/prune_class_map.rb +2 -0
  12. data/lib/appmap/algorithm/stats.rb +4 -2
  13. data/lib/appmap/class_map.rb +143 -0
  14. data/lib/appmap/command/record.rb +8 -6
  15. data/lib/appmap/command/stats.rb +2 -0
  16. data/lib/appmap/event.rb +168 -0
  17. data/lib/appmap/hook.rb +152 -0
  18. data/lib/appmap/middleware/remote_recording.rb +14 -21
  19. data/lib/appmap/rails/action_handler.rb +10 -6
  20. data/lib/appmap/rails/sql_handler.rb +10 -13
  21. data/lib/appmap/railtie.rb +31 -18
  22. data/lib/appmap/rspec.rb +247 -260
  23. data/lib/appmap/trace.rb +88 -0
  24. data/lib/appmap/version.rb +1 -1
  25. data/package-lock.json +90 -92
  26. data/spec/abstract_controller4_base_spec.rb +1 -1
  27. data/spec/abstract_controller_base_spec.rb +7 -3
  28. data/spec/config_spec.rb +25 -0
  29. data/spec/fixtures/hook/attr_accessor.rb +5 -0
  30. data/spec/fixtures/hook/class_method.rb +17 -0
  31. data/spec/fixtures/hook/constructor.rb +7 -0
  32. data/spec/fixtures/hook/exception_method.rb +11 -0
  33. data/spec/fixtures/hook/instance_method.rb +23 -0
  34. data/spec/fixtures/rails4_users_app/app/controllers/api/users_controller.rb +3 -3
  35. data/spec/fixtures/rails4_users_app/config/database.yml +2 -1
  36. data/spec/fixtures/rails4_users_app/docker-compose.yml +2 -0
  37. data/spec/fixtures/rails_users_app/.ruby-version +1 -1
  38. data/spec/fixtures/rails_users_app/app/controllers/api/users_controller.rb +2 -2
  39. data/spec/fixtures/rails_users_app/config/database.yml +2 -1
  40. data/spec/fixtures/rails_users_app/create_app +1 -0
  41. data/spec/fixtures/rails_users_app/docker-compose.yml +4 -0
  42. data/spec/fixtures/rails_users_app/spec/models/user_spec.rb +1 -1
  43. data/spec/hook_spec.rb +369 -0
  44. data/spec/rails_spec_helper.rb +25 -16
  45. data/spec/railtie_spec.rb +1 -1
  46. data/spec/record_sql_rails_pg_spec.rb +1 -2
  47. data/spec/remote_recording_spec.rb +117 -0
  48. data/spec/spec_helper.rb +5 -0
  49. data/test/cli_test.rb +4 -46
  50. data/test/fixtures/cli_record_test/appmap.yml +2 -1
  51. data/test/fixtures/cli_record_test/lib/cli_record_test/main.rb +4 -2
  52. data/test/fixtures/rspec_recorder/Gemfile +1 -1
  53. data/test/fixtures/rspec_recorder/spec/decorated_hello_spec.rb +12 -0
  54. data/test/rspec_test.rb +5 -0
  55. data/test/test_helper.rb +0 -42
  56. metadata +46 -63
  57. data/exe/_appmap-record-self +0 -49
  58. data/lib/appmap/command/inspect.rb +0 -14
  59. data/lib/appmap/command/upload.rb +0 -99
  60. data/lib/appmap/config.rb +0 -65
  61. data/lib/appmap/config/directory.rb +0 -65
  62. data/lib/appmap/config/file.rb +0 -13
  63. data/lib/appmap/config/named_function.rb +0 -21
  64. data/lib/appmap/config/package_dir.rb +0 -52
  65. data/lib/appmap/config/path.rb +0 -25
  66. data/lib/appmap/feature.rb +0 -262
  67. data/lib/appmap/inspect.rb +0 -91
  68. data/lib/appmap/inspect/inspector.rb +0 -99
  69. data/lib/appmap/inspect/parse_node.rb +0 -170
  70. data/lib/appmap/inspect/parser.rb +0 -15
  71. data/lib/appmap/parser.rb +0 -60
  72. data/lib/appmap/rspec/parse_node.rb +0 -41
  73. data/lib/appmap/rspec/parser.rb +0 -15
  74. data/lib/appmap/trace/event_handler/rack_handler_webrick.rb +0 -65
  75. data/lib/appmap/trace/tracer.rb +0 -356
  76. data/spec/fixtures/rails_users_app/bin/_appmap-record-self +0 -29
  77. data/spec/rack_handler_webrick_spec.rb +0 -59
  78. data/test/config_test.rb +0 -149
  79. data/test/explict_inspect_test.rb +0 -29
  80. data/test/fixtures/active_record_like/active_record.rb +0 -2
  81. data/test/fixtures/active_record_like/active_record/aggregations.rb +0 -4
  82. data/test/fixtures/active_record_like/active_record/association.rb +0 -4
  83. data/test/fixtures/active_record_like/active_record/associations/join_dependency.rb +0 -6
  84. data/test/fixtures/active_record_like/active_record/associations/join_dependency/join_base.rb +0 -8
  85. data/test/fixtures/active_record_like/active_record/associations/join_dependency/join_part.rb +0 -8
  86. data/test/fixtures/active_record_like/active_record/caps/caps.rb +0 -4
  87. data/test/fixtures/ignore_non_ruby_file/class.rb +0 -3
  88. data/test/fixtures/ignore_non_ruby_file/non-ruby.txt +0 -1
  89. data/test/fixtures/includes_excludes/lib/a/a_1.rb +0 -6
  90. data/test/fixtures/includes_excludes/lib/a/a_2.rb +0 -6
  91. data/test/fixtures/includes_excludes/lib/a/x/x_1.rb +0 -8
  92. data/test/fixtures/includes_excludes/lib/b/b_1.rb +0 -6
  93. data/test/fixtures/includes_excludes/lib/root_1.rb +0 -4
  94. data/test/fixtures/inspect_multiple_subdirs/module_a.rb +0 -2
  95. data/test/fixtures/inspect_multiple_subdirs/module_a/class_a.rb +0 -5
  96. data/test/fixtures/inspect_multiple_subdirs/module_b.rb +0 -2
  97. data/test/fixtures/inspect_multiple_subdirs/module_b/class_b.rb +0 -5
  98. data/test/fixtures/inspect_multiple_subdirs/module_b/class_c.rb +0 -5
  99. data/test/fixtures/inspect_package/module_a/module_b/class_in_module.rb +0 -6
  100. data/test/fixtures/parse_file/defs_static_function.rb +0 -96
  101. data/test/fixtures/parse_file/function_within_class.rb +0 -36
  102. data/test/fixtures/parse_file/include_public_methods.rb +0 -127
  103. data/test/fixtures/parse_file/instance_function.rb +0 -17
  104. data/test/fixtures/parse_file/modules.rb +0 -71
  105. data/test/fixtures/parse_file/sclass_static_function.rb +0 -88
  106. data/test/fixtures/parse_file/toplevel_class.rb +0 -13
  107. data/test/fixtures/parse_file/toplevel_function.rb +0 -14
  108. data/test/fixtures/trace_test/trace_program_1.rb +0 -44
  109. data/test/implicit_inspect_test.rb +0 -33
  110. data/test/include_exclude_test.rb +0 -48
  111. data/test/prerecorded_trace_test.rb +0 -76
  112. 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
- features = AppMap.inspect(config)
65
- functions = features.map(&:collect_functions).flatten
66
-
67
- require 'appmap/trace/tracer'
66
+ AppMap::Hook.hook(config)
68
67
 
69
- tracer = AppMap::Trace.tracers.trace(functions)
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 features, events
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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module AppMap
2
4
  module Command
3
5
  StatsStruct = Struct.new(:appmap)
@@ -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
@@ -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