appmap 0.23.0 → 0.27.0

Sign up to get free protection for your applications and to get access to all the features.
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