appmap 0.86.0 → 0.89.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: aaf2ec3e35b79244511d47a45f8d3f8f85dc5b666b022cbffe07e6224250f4fd
4
- data.tar.gz: 0eada13772cc55470113fc06c47c5fddac339914a6012eafefc75d350bbb5c81
3
+ metadata.gz: 9d046b1cedf455ae967a663b200c05751c4c0659defa5e7dce0a7f760ba865b6
4
+ data.tar.gz: 790f5a9705b0d32b60467ecb8e51dc75e8fdbb2ac3af2022ee1c8a1a7c8cc8ca
5
5
  SHA512:
6
- metadata.gz: 243680c3c2dcf1fe285bf2c2baf4dfe0913b4627bfdcb9654353d4855be3ed3def55973d331ad04ae13a307f710d6eba284313c338f70b8cf5e24025f0e6befe
7
- data.tar.gz: c5715cb83a9d5463208392918cb040505b3a52be12e13cb40936146e2b468efc9932bd3b1233df7f0a05fece28387282009e6abe71a940858735e31ee18885e9
6
+ metadata.gz: 7c999d39cb9385f3758746cf93d50f291a4a6f9ae1c8b33266897975bdd9aabf4c8a3f666a3b3650ee70a24978d574a5db0bfefe2539240b582f1a430eabe399
7
+ data.tar.gz: 731810f9c4afe0c0c9130ab81d71e0c20f85860bf57fef5d5736845aba4c4d240dff101bc5a46aef4c6ca9e08f5adfe90ac749bb059fc7ecacd08aec77926f4e
data/CHANGELOG.md CHANGED
@@ -1,3 +1,35 @@
1
+ # [0.89.0](https://github.com/applandinc/appmap-ruby/compare/v0.88.0...v0.89.0) (2022-09-07)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * Make Rack a runtime dependency ([3f2924d](https://github.com/applandinc/appmap-ruby/commit/3f2924d41af291bfe771829c2ea7978332ff1d44))
7
+
8
+
9
+ ### Features
10
+
11
+ * Add builtin labels for JWT ([3569333](https://github.com/applandinc/appmap-ruby/commit/3569333e99f82e03fff892001f2b65eb9e8aa52a))
12
+ * Write an AppMap for each observed HTTP request ([2e89eaf](https://github.com/applandinc/appmap-ruby/commit/2e89eaf1b1f327771d1f4f0642fa6c202e0f3afb))
13
+
14
+ # [0.88.0](https://github.com/applandinc/appmap-ruby/compare/v0.87.0...v0.88.0) (2022-08-31)
15
+
16
+
17
+ ### Bug Fixes
18
+
19
+ * Allow recording of error responses ([e538556](https://github.com/applandinc/appmap-ruby/commit/e5385560cbbab2d27ce6c0a1620d5c7539e0d985))
20
+
21
+
22
+ ### Features
23
+
24
+ * Add HTTP server request support for Rails 7 ([bd7f6c9](https://github.com/applandinc/appmap-ruby/commit/bd7f6c9109ee6c850250016276c8fbb6a8a66a2e))
25
+
26
+ # [0.87.0](https://github.com/applandinc/appmap-ruby/compare/v0.86.0...v0.87.0) (2022-08-19)
27
+
28
+
29
+ ### Features
30
+
31
+ * Improve performance of initial hooking ([901e262](https://github.com/applandinc/appmap-ruby/commit/901e26237027920ede6b0f9d4bc3d175c861b23a))
32
+
1
33
  # [0.86.0](https://github.com/applandinc/appmap-ruby/compare/v0.85.0...v0.86.0) (2022-08-10)
2
34
 
3
35
 
data/Gemfile CHANGED
@@ -3,3 +3,5 @@ source "https://rubygems.org"
3
3
  git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
4
 
5
5
  gemspec
6
+
7
+ gem 'rack', '~> 2'
data/README.md CHANGED
@@ -92,9 +92,7 @@ You can launch a database like this:
92
92
  ```
93
93
  ➜ docker-compose -p appmap-ruby up -d
94
94
  ... stuff
95
- ➜ docker-compose ps pg
96
- Name Command State Ports
97
- -----------------------------------------------------------------------------------------
98
- appmap-ruby_pg_1 docker-entrypoint.sh postgres Up (healthy) 0.0.0.0:59134->5432/tcp
99
- ➜ export DATABASE_URL=postgres://postgres@localhost:59134
95
+ ➜ docker-compose port pg 5432
96
+ 0.0.0.0:64479
97
+ ➜ export DATABASE_URL=postgres://postgres@localhost:64479
100
98
  ```
data/appmap.gemspec CHANGED
@@ -28,10 +28,10 @@ Gem::Specification.new do |spec|
28
28
  spec.require_paths = ['lib']
29
29
 
30
30
  spec.add_dependency 'method_source'
31
- spec.add_dependency 'rack'
32
31
  spec.add_dependency 'reverse_markdown'
33
32
 
34
33
  spec.add_runtime_dependency 'activesupport'
34
+ spec.add_runtime_dependency 'rack'
35
35
 
36
36
  spec.add_development_dependency 'bundler', '>= 1.16'
37
37
  spec.add_development_dependency 'minitest', '~> 5.15'
@@ -39,7 +39,7 @@ Gem::Specification.new do |spec|
39
39
  spec.add_development_dependency 'rake', '>= 12.3.3'
40
40
  spec.add_development_dependency 'rdoc'
41
41
  spec.add_development_dependency 'rubocop'
42
- spec.add_development_dependency "rake-compiler"
42
+ spec.add_development_dependency 'rake-compiler'
43
43
 
44
44
  # Testing
45
45
  spec.add_development_dependency 'climate_control'
@@ -0,0 +1,8 @@
1
+ - method: JWT#decode
2
+ label: jwt.decode
3
+
4
+ - method: JWT#encode
5
+ label: jwt.encode
6
+
7
+ - method: JWT::Signature#verify
8
+ label: jwt.signature.verify
@@ -73,8 +73,8 @@ module AppMap
73
73
  class << self
74
74
  def build_from_invocation(parent_id, return_value, elapsed, response, event: HTTPServerResponse.new)
75
75
  event ||= HTTPServerResponse.new
76
- event.status = response.status
77
- event.headers = response.headers.dup
76
+ event.status = response[:status] || response.status
77
+ event.headers = (response[:headers] || response.headers).dup
78
78
  AppMap::Event::MethodReturn.build_from_invocation parent_id, return_value, nil, elapsed: elapsed, event: event, parameter_schema: true
79
79
  end
80
80
  end
@@ -114,6 +114,46 @@ module AppMap
114
114
  AppMap.tracing.record_event return_event
115
115
  end
116
116
  end
117
+
118
+ # RequestListener listens to the 'start_processing.action_controller' notification as a
119
+ # source of HTTP server request events. A strategy other than HookMethod is required for
120
+ # Rails >= 7 due to the hooked methods visibility dropping to private.
121
+ class RequestListener
122
+ def self.begin_request(_name, _started, _finished, _unique_id, payload)
123
+ RequestListener.new(payload)
124
+ end
125
+
126
+ protected
127
+
128
+ def initialize(payload)
129
+ @request_id = payload[:request].request_id
130
+ @subscriber = self.class.instance_method(:after_hook).bind(self)
131
+
132
+ ActiveSupport::Notifications.subscribe 'process_action.action_controller', @subscriber
133
+ before_hook payload
134
+ end
135
+
136
+ def before_hook(payload)
137
+ @call_event = HTTPServerRequest.new payload[:request]
138
+ AppMap.tracing.record_event @call_event
139
+ end
140
+
141
+ def after_hook(_name, started, finished, _unique_id, payload)
142
+ return unless @request_id == payload[:request].request_id
143
+
144
+ return_value = Thread.current[TEMPLATE_RENDER_VALUE]
145
+ Thread.current[TEMPLATE_RENDER_VALUE] = nil
146
+ return_event = HTTPServerResponse.build_from_invocation(
147
+ @call_event.id,
148
+ return_value,
149
+ finished - started,
150
+ payload[:response] || payload
151
+ )
152
+
153
+ AppMap.tracing.record_event return_event
154
+ ActiveSupport::Notifications.unsubscribe(@subscriber)
155
+ end
156
+ end
117
157
  end
118
158
  end
119
159
  end
data/lib/appmap/hook.rb CHANGED
@@ -183,12 +183,6 @@ module AppMap
183
183
 
184
184
  hook = lambda do |hook_cls|
185
185
  lambda do |method_id|
186
- # Don't try and trace the AppMap methods or there will be
187
- # a stack overflow in the defined hook method.
188
- next if %w[Marshal AppMap ActiveSupport].member?((hook_cls&.name || '').split('::')[0])
189
-
190
- next if method_id == :call
191
-
192
186
  method = \
193
187
  begin
194
188
  hook_cls.instance_method(method_id)
@@ -197,6 +191,16 @@ module AppMap
197
191
  next
198
192
  end
199
193
 
194
+ package = config.lookup_package(hook_cls, method)
195
+ # doing this check first returned early in 98.7% of cases in sample_app_6th_ed
196
+ next unless package
197
+
198
+ # Don't try and trace the AppMap methods or there will be
199
+ # a stack overflow in the defined hook method.
200
+ next if %w[Marshal AppMap ActiveSupport].member?((hook_cls&.name || '').split('::')[0])
201
+
202
+ next if method_id == :call
203
+
200
204
  next if self.class.already_hooked?(method)
201
205
 
202
206
  warn "AppMap: Examining #{hook_cls} #{method.name}" if LOG
@@ -206,9 +210,6 @@ module AppMap
206
210
  # TODO: Figure out how to tell the difference?
207
211
  next unless disasm
208
212
 
209
- package = config.lookup_package(hook_cls, method)
210
- next unless package
211
-
212
213
  package.handler_class.new(package, hook_cls, method).activate
213
214
  end
214
215
  end
@@ -2,7 +2,8 @@
2
2
 
3
3
  module AppMap
4
4
  module Middleware
5
- # RemoteRecording adds `/_appmap/record` routes to control recordings via HTTP requests
5
+ # RemoteRecording adds `/_appmap/record` routes to control recordings via HTTP requests.
6
+ # It can also be enabled to emit an AppMap for each request.
6
7
  class RemoteRecording
7
8
  def initialize(app)
8
9
  require 'json'
@@ -21,7 +22,7 @@ module AppMap
21
22
  end
22
23
  end
23
24
 
24
- def start_recording
25
+ def ws_start_recording
25
26
  return [ 409, 'Recording is already in progress' ] if @tracer
26
27
 
27
28
  @events = []
@@ -32,7 +33,7 @@ module AppMap
32
33
  [ 200 ]
33
34
  end
34
35
 
35
- def stop_recording(req)
36
+ def ws_stop_recording(req)
36
37
  return [ 404, 'No recording is in progress' ] unless @tracer
37
38
 
38
39
  tracer = @tracer
@@ -75,10 +76,50 @@ module AppMap
75
76
  end
76
77
 
77
78
  def call(env)
79
+ # Note: Puma config is avaliable here. For example:
80
+ # $ env['puma.config'].final_options[:workers]
81
+ # 0
82
+
78
83
  req = Rack::Request.new(env)
79
84
  return handle_record_request(req) if req.path == '/_appmap/record'
80
85
 
81
- @app.call(env)
86
+ start_time = Time.now
87
+ # Support multi-threaded web server such as Puma by recording each thread
88
+ # into a separate Tracer.
89
+ tracer = AppMap.tracing.trace(thread: Thread.current) if record_all_requests?
90
+
91
+ @app.call(env).tap do |status, headers|
92
+ if tracer
93
+ AppMap.tracing.delete(tracer)
94
+
95
+ events = tracer.events.map(&:to_h)
96
+
97
+ appmap_name = "#{req.request_method} #{req.path} (#{status}) - #{start_time.strftime('%T.%L')}"
98
+ appmap_file_name = AppMap::Util.scenario_filename([ start_time.to_f, req.url ].join('_'))
99
+ output_dir = File.join(AppMap::DEFAULT_APPMAP_DIR, 'requests')
100
+ appmap_file_path = File.join(output_dir, appmap_file_name)
101
+
102
+ metadata = AppMap.detect_metadata
103
+ metadata[:name] = appmap_name
104
+ metadata[:timestamp] = start_time.to_f
105
+ metadata[:recorder] = {
106
+ name: 'record_requests'
107
+ }
108
+
109
+ appmap = {
110
+ version: AppMap::APPMAP_FORMAT_VERSION,
111
+ classMap: AppMap.class_map(tracer.event_methods),
112
+ metadata: metadata,
113
+ events: events
114
+ }
115
+
116
+ FileUtils.mkdir_p(output_dir)
117
+ File.write(appmap_file_path, JSON.generate(appmap))
118
+
119
+ headers['AppMap-Name'] = File.expand_path(appmap_name)
120
+ headers['AppMap-File-Name'] = File.expand_path(appmap_file_path)
121
+ end
122
+ end
82
123
  end
83
124
 
84
125
  def recording_state
@@ -92,9 +133,9 @@ module AppMap
92
133
  if method.eql?('GET')
93
134
  recording_state
94
135
  elsif method.eql?('POST')
95
- start_recording
136
+ ws_start_recording
96
137
  elsif method.eql?('DELETE')
97
- stop_recording(req)
138
+ ws_stop_recording(req)
98
139
  else
99
140
  [ 404, '' ]
100
141
  end
@@ -106,6 +147,10 @@ module AppMap
106
147
  headers['Content-Type'] && headers['Content-Type'] =~ /html/
107
148
  end
108
149
 
150
+ def record_all_requests?
151
+ ENV['APPMAP_RECORD_REQUESTS'] == 'true'
152
+ end
153
+
109
154
  def recording?
110
155
  !@event_thread.nil?
111
156
  end
@@ -18,7 +18,15 @@ module AppMap
18
18
  ActiveSupport::Notifications.subscribe 'sql.sequel', AppMap::Handler::Rails::SQLHandler.new
19
19
  ActiveSupport::Notifications.subscribe 'sql.active_record', AppMap::Handler::Rails::SQLHandler.new
20
20
 
21
- AppMap::Handler::Rails::RequestHandler::HookMethod.new.activate
21
+ http_hook_available = ActionController::Instrumentation.public_instance_methods.include?(:process_action)
22
+ if http_hook_available
23
+ AppMap::Handler::Rails::RequestHandler::HookMethod.new.activate
24
+ else
25
+ ActiveSupport::Notifications.subscribe(
26
+ 'start_processing.action_controller',
27
+ AppMap::Handler::Rails::RequestHandler::RequestListener.method(:begin_request)
28
+ )
29
+ end
22
30
  end
23
31
  end
24
32
  end if ENV['APPMAP'] == 'true'
data/lib/appmap/trace.rb CHANGED
@@ -48,8 +48,8 @@ module AppMap
48
48
  @tracers.empty?
49
49
  end
50
50
 
51
- def trace(enable: true)
52
- Tracer.new.tap do |tracer|
51
+ def trace(enable: true, thread: nil)
52
+ Tracer.new(thread_id: thread&.object_id).tap do |tracer|
53
53
  @tracers << tracer
54
54
  tracer.enable if enable
55
55
  end
@@ -64,7 +64,7 @@ module AppMap
64
64
  end
65
65
 
66
66
  def record_event(event, package: nil, defined_class: nil, method: nil)
67
- @tracers.each do |tracer|
67
+ @tracers.select { |tracer| tracer.thread_id.nil? || tracer.thread_id === event.thread_id }.each do |tracer|
68
68
  tracer.record_event(event, package: package, defined_class: defined_class, method: method)
69
69
  end
70
70
  end
@@ -114,14 +114,16 @@ module AppMap
114
114
 
115
115
  class Tracer
116
116
  attr_accessor :stacks
117
+ attr_reader :thread_id, :events
117
118
 
118
119
  # Records the events which happen in a program.
119
- def initialize
120
+ def initialize(thread_id: nil)
120
121
  @events = []
121
122
  @last_package_for_thread = {}
122
123
  @methods = Set.new
123
124
  @stack_printer = StackPrinter.new if StackPrinter.enabled?
124
125
  @enabled = false
126
+ @thread_id = thread_id
125
127
  end
126
128
 
127
129
  def enable
@@ -143,6 +145,8 @@ module AppMap
143
145
  def record_event(event, package: nil, defined_class: nil, method: nil)
144
146
  return unless @enabled
145
147
 
148
+ raise "Expected event in thread #{@thread_id}, got #{event.thread_id}" if @thread_id && @thread_id != event.thread_id
149
+
146
150
  @stack_printer.record(event) if @stack_printer
147
151
  @last_package_for_thread[Thread.current.object_id] = package if package
148
152
  @events << event
@@ -3,7 +3,7 @@
3
3
  module AppMap
4
4
  URL = 'https://github.com/applandinc/appmap-ruby'
5
5
 
6
- VERSION = '0.86.0'
6
+ VERSION = '0.89.0'
7
7
 
8
8
  APPMAP_FORMAT_VERSION = '1.7.0'
9
9
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: appmap
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.86.0
4
+ version: 0.89.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kevin Gilpin
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-08-10 00:00:00.000000000 Z
11
+ date: 2022-09-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: method_source
@@ -25,7 +25,7 @@ dependencies:
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
27
  - !ruby/object:Gem::Dependency
28
- name: rack
28
+ name: reverse_markdown
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - ">="
@@ -39,7 +39,7 @@ dependencies:
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0'
41
41
  - !ruby/object:Gem::Dependency
42
- name: reverse_markdown
42
+ name: activesupport
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - ">="
@@ -53,7 +53,7 @@ dependencies:
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
55
  - !ruby/object:Gem::Dependency
56
- name: activesupport
56
+ name: rack
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - ">="
@@ -386,6 +386,7 @@ files:
386
386
  - lib/appmap/gem_hooks/activerecord.yml
387
387
  - lib/appmap/gem_hooks/cancancan.yml
388
388
  - lib/appmap/gem_hooks/devise.yml
389
+ - lib/appmap/gem_hooks/jwt.yml
389
390
  - lib/appmap/gem_hooks/pandoc-ruby.yml
390
391
  - lib/appmap/gem_hooks/rails.yml
391
392
  - lib/appmap/gem_hooks/railties.yml