appmap 0.23.0 → 0.25.0

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