appmap 0.23.0 → 0.25.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 (109) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.rubocop.yml +17 -8
  4. data/.travis.yml +6 -0
  5. data/CHANGELOG.md +19 -0
  6. data/README.md +29 -12
  7. data/Rakefile +3 -3
  8. data/appmap.gemspec +3 -1
  9. data/exe/appmap +6 -18
  10. data/lib/appmap.rb +47 -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/command/upload.rb +4 -2
  17. data/lib/appmap/event.rb +168 -0
  18. data/lib/appmap/hook.rb +151 -0
  19. data/lib/appmap/middleware/remote_recording.rb +14 -20
  20. data/lib/appmap/rails/action_handler.rb +10 -6
  21. data/lib/appmap/rails/sql_handler.rb +10 -8
  22. data/lib/appmap/railtie.rb +31 -18
  23. data/lib/appmap/rspec.rb +238 -261
  24. data/lib/appmap/trace.rb +88 -0
  25. data/lib/appmap/version.rb +1 -1
  26. data/package-lock.json +90 -92
  27. data/spec/abstract_controller4_base_spec.rb +1 -1
  28. data/spec/abstract_controller_base_spec.rb +7 -3
  29. data/spec/config_spec.rb +25 -0
  30. data/spec/fixtures/hook/attr_accessor.rb +5 -0
  31. data/spec/fixtures/hook/class_method.rb +17 -0
  32. data/spec/fixtures/hook/constructor.rb +7 -0
  33. data/spec/fixtures/hook/exception_method.rb +11 -0
  34. data/spec/fixtures/hook/instance_method.rb +23 -0
  35. data/spec/fixtures/rails4_users_app/app/controllers/api/users_controller.rb +3 -3
  36. data/spec/fixtures/rails4_users_app/config/database.yml +2 -1
  37. data/spec/fixtures/rails4_users_app/docker-compose.yml +2 -0
  38. data/spec/fixtures/rails_users_app/.ruby-version +1 -1
  39. data/spec/fixtures/rails_users_app/app/controllers/api/users_controller.rb +2 -2
  40. data/spec/fixtures/rails_users_app/config/database.yml +2 -1
  41. data/spec/fixtures/rails_users_app/create_app +1 -0
  42. data/spec/fixtures/rails_users_app/docker-compose.yml +4 -0
  43. data/spec/fixtures/rails_users_app/spec/models/user_spec.rb +1 -1
  44. data/spec/hook_spec.rb +357 -0
  45. data/spec/rails_spec_helper.rb +25 -16
  46. data/spec/railtie_spec.rb +1 -1
  47. data/spec/record_sql_rails_pg_spec.rb +1 -2
  48. data/spec/remote_recording_spec.rb +117 -0
  49. data/spec/spec_helper.rb +1 -0
  50. data/test/cli_test.rb +7 -36
  51. data/test/fixtures/cli_record_test/appmap.yml +2 -1
  52. data/test/fixtures/cli_record_test/lib/cli_record_test/main.rb +4 -2
  53. data/test/test_helper.rb +0 -42
  54. metadata +46 -62
  55. data/exe/_appmap-record-self +0 -49
  56. data/lib/appmap/command/inspect.rb +0 -14
  57. data/lib/appmap/config.rb +0 -65
  58. data/lib/appmap/config/directory.rb +0 -65
  59. data/lib/appmap/config/file.rb +0 -13
  60. data/lib/appmap/config/named_function.rb +0 -21
  61. data/lib/appmap/config/package_dir.rb +0 -52
  62. data/lib/appmap/config/path.rb +0 -25
  63. data/lib/appmap/feature.rb +0 -262
  64. data/lib/appmap/inspect.rb +0 -91
  65. data/lib/appmap/inspect/inspector.rb +0 -99
  66. data/lib/appmap/inspect/parse_node.rb +0 -170
  67. data/lib/appmap/inspect/parser.rb +0 -15
  68. data/lib/appmap/parser.rb +0 -60
  69. data/lib/appmap/rspec/parse_node.rb +0 -41
  70. data/lib/appmap/rspec/parser.rb +0 -15
  71. data/lib/appmap/trace/event_handler/rack_handler_webrick.rb +0 -65
  72. data/lib/appmap/trace/tracer.rb +0 -356
  73. data/spec/fixtures/rails_users_app/bin/_appmap-record-self +0 -29
  74. data/spec/rack_handler_webrick_spec.rb +0 -59
  75. data/test/config_test.rb +0 -149
  76. data/test/explict_inspect_test.rb +0 -29
  77. data/test/fixtures/active_record_like/active_record.rb +0 -2
  78. data/test/fixtures/active_record_like/active_record/aggregations.rb +0 -4
  79. data/test/fixtures/active_record_like/active_record/association.rb +0 -4
  80. data/test/fixtures/active_record_like/active_record/associations/join_dependency.rb +0 -6
  81. data/test/fixtures/active_record_like/active_record/associations/join_dependency/join_base.rb +0 -8
  82. data/test/fixtures/active_record_like/active_record/associations/join_dependency/join_part.rb +0 -8
  83. data/test/fixtures/active_record_like/active_record/caps/caps.rb +0 -4
  84. data/test/fixtures/ignore_non_ruby_file/class.rb +0 -3
  85. data/test/fixtures/ignore_non_ruby_file/non-ruby.txt +0 -1
  86. data/test/fixtures/includes_excludes/lib/a/a_1.rb +0 -6
  87. data/test/fixtures/includes_excludes/lib/a/a_2.rb +0 -6
  88. data/test/fixtures/includes_excludes/lib/a/x/x_1.rb +0 -8
  89. data/test/fixtures/includes_excludes/lib/b/b_1.rb +0 -6
  90. data/test/fixtures/includes_excludes/lib/root_1.rb +0 -4
  91. data/test/fixtures/inspect_multiple_subdirs/module_a.rb +0 -2
  92. data/test/fixtures/inspect_multiple_subdirs/module_a/class_a.rb +0 -5
  93. data/test/fixtures/inspect_multiple_subdirs/module_b.rb +0 -2
  94. data/test/fixtures/inspect_multiple_subdirs/module_b/class_b.rb +0 -5
  95. data/test/fixtures/inspect_multiple_subdirs/module_b/class_c.rb +0 -5
  96. data/test/fixtures/inspect_package/module_a/module_b/class_in_module.rb +0 -6
  97. data/test/fixtures/parse_file/defs_static_function.rb +0 -96
  98. data/test/fixtures/parse_file/function_within_class.rb +0 -36
  99. data/test/fixtures/parse_file/include_public_methods.rb +0 -127
  100. data/test/fixtures/parse_file/instance_function.rb +0 -17
  101. data/test/fixtures/parse_file/modules.rb +0 -71
  102. data/test/fixtures/parse_file/sclass_static_function.rb +0 -88
  103. data/test/fixtures/parse_file/toplevel_class.rb +0 -13
  104. data/test/fixtures/parse_file/toplevel_function.rb +0 -14
  105. data/test/fixtures/trace_test/trace_program_1.rb +0 -44
  106. data/test/implicit_inspect_test.rb +0 -33
  107. data/test/include_exclude_test.rb +0 -48
  108. data/test/prerecorded_trace_test.rb +0 -76
  109. data/test/trace_test.rb +0 -92
@@ -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)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'json'
2
4
  require 'faraday'
3
5
 
@@ -5,13 +7,13 @@ module AppMap
5
7
  module Command
6
8
  UploadResponse = Struct.new(:batch_id, :scenario_uuid)
7
9
 
8
- UploadStruct = Struct.new(:config, :data, :url, :user, :org)
10
+ UploadStruct = Struct.new(:data, :url, :user, :org)
9
11
  class Upload < UploadStruct
10
12
  MAX_DEPTH = 12
11
13
 
12
14
  attr_accessor :batch_id
13
15
 
14
- def initialize(config, data, url, user, org)
16
+ def initialize(data, url, user, org)
15
17
  super
16
18
 
17
19
  # TODO: Make this an option
@@ -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[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,151 @@
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
+ ensure
137
+ with_disabled_hook.call do
138
+ after_hook.call(call_event, defined_class, method, start_time, return_value, exception)
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end
144
+
145
+ instance_methods.each(&hook_method.call(cls))
146
+ class_methods.each(&hook_method.call(cls.singleton_class))
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end
@@ -4,17 +4,14 @@ module AppMap
4
4
  module Middleware
5
5
  # RemoteRecording adds `/_appmap/record` routes to control recordings via HTTP requests
6
6
  class RemoteRecording
7
-
8
7
  def initialize(app)
9
8
  require 'appmap/command/record'
10
9
  require 'appmap/command/upload'
11
- require 'appmap/trace/tracer'
12
- require 'appmap/config'
13
10
  require 'json'
14
11
 
15
12
  @app = app
16
- @features = AppMap.inspect(config)
17
- @functions = @features.map(&:collect_functions).flatten
13
+ @config = AppMap.configure
14
+ AppMap::Hook.hook(@config)
18
15
  end
19
16
 
20
17
  def event_loop
@@ -29,23 +26,23 @@ module AppMap
29
26
  end
30
27
 
31
28
  def start_recording
32
- return [ false, 'Recording is already in progress' ] if @tracer
29
+ return [ 409, 'Recording is already in progress' ] if @tracer
33
30
 
34
31
  @events = []
35
- @tracer = AppMap::Trace.tracers.trace(@functions)
32
+ @tracer = AppMap.tracing.trace
36
33
  @event_thread = Thread.new { event_loop }
37
34
  @event_thread.abort_on_exception = true
38
35
 
39
- [ true ]
36
+ [ 200 ]
40
37
  end
41
38
 
42
39
  def stop_recording(req)
43
- return [ false, 'No recording is in progress' ] unless @tracer
40
+ return [ 404, 'No recording is in progress' ] unless @tracer
44
41
 
45
42
  tracer = @tracer
46
43
  @tracer = nil
47
44
 
48
- AppMap::Trace.tracers.delete(tracer)
45
+ AppMap.tracing.delete(tracer)
49
46
 
50
47
  @event_thread.exit
51
48
  @event_thread.join
@@ -73,9 +70,13 @@ module AppMap
73
70
  name: 'remote_recording'
74
71
  }
75
72
 
76
- response = JSON.generate(version: AppMap::APPMAP_FORMAT_VERSION, classMap: @features, metadata: metadata, events: @events)
73
+ response = JSON.generate \
74
+ version: AppMap::APPMAP_FORMAT_VERSION,
75
+ classMap: AppMap.class_map(@config, tracer.event_methods),
76
+ metadata: metadata,
77
+ events: @events
77
78
 
78
- [ true, response ]
79
+ [ 200, response ]
79
80
  end
80
81
 
81
82
  def call(env)
@@ -103,20 +104,13 @@ module AppMap
103
104
  [ 404, '' ]
104
105
  end
105
106
 
106
- status = 200 if status == true
107
- status = 500 if status == false
108
-
109
- [status, { 'Content-Type' => 'application/text' }, [body || '']]
107
+ [status, { 'Content-Type' => 'application/json' }, [body || '']]
110
108
  end
111
109
 
112
110
  def html_response?(headers)
113
111
  headers['Content-Type'] && headers['Content-Type'] =~ /html/
114
112
  end
115
113
 
116
- def config
117
- @config ||= AppMap::Config.load_from_file 'appmap.yml'
118
- end
119
-
120
114
  def recording?
121
115
  !@event_thread.nil?
122
116
  end