appmap 0.86.0 → 0.89.0

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