Skip to content

Commit 2e89eaf

Browse files
committed
feat: Write an AppMap for each observed HTTP request
1 parent be69f98 commit 2e89eaf

File tree

2 files changed

+59
-10
lines changed

2 files changed

+59
-10
lines changed

lib/appmap/middleware/remote_recording.rb

+51-6
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
module AppMap
44
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.
67
class RemoteRecording
78
def initialize(app)
89
require 'json'
@@ -21,7 +22,7 @@ def event_loop
2122
end
2223
end
2324

24-
def start_recording
25+
def ws_start_recording
2526
return [ 409, 'Recording is already in progress' ] if @tracer
2627

2728
@events = []
@@ -32,7 +33,7 @@ def start_recording
3233
[ 200 ]
3334
end
3435

35-
def stop_recording(req)
36+
def ws_stop_recording(req)
3637
return [ 404, 'No recording is in progress' ] unless @tracer
3738

3839
tracer = @tracer
@@ -75,10 +76,50 @@ def stop_recording(req)
7576
end
7677

7778
def call(env)
79+
# Note: Puma config is avaliable here. For example:
80+
# $ env['puma.config'].final_options[:workers]
81+
# 0
82+
7883
req = Rack::Request.new(env)
7984
return handle_record_request(req) if req.path == '/_appmap/record'
8085

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.dup.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
82123
end
83124

84125
def recording_state
@@ -92,9 +133,9 @@ def handle_record_request(req)
92133
if method.eql?('GET')
93134
recording_state
94135
elsif method.eql?('POST')
95-
start_recording
136+
ws_start_recording
96137
elsif method.eql?('DELETE')
97-
stop_recording(req)
138+
ws_stop_recording(req)
98139
else
99140
[ 404, '' ]
100141
end
@@ -106,6 +147,10 @@ def html_response?(headers)
106147
headers['Content-Type'] && headers['Content-Type'] =~ /html/
107148
end
108149

150+
def record_all_requests?
151+
ENV['APPMAP_RECORD_REQUESTS'] == 'true'
152+
end
153+
109154
def recording?
110155
!@event_thread.nil?
111156
end

lib/appmap/trace.rb

+8-4
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@ def empty?
4848
@tracers.empty?
4949
end
5050

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|
5353
@tracers << tracer
5454
tracer.enable if enable
5555
end
@@ -64,7 +64,7 @@ def last_package_for_current_thread
6464
end
6565

6666
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|
6868
tracer.record_event(event, package: package, defined_class: defined_class, method: method)
6969
end
7070
end
@@ -114,14 +114,16 @@ def record(event)
114114

115115
class Tracer
116116
attr_accessor :stacks
117+
attr_reader :thread_id, :events
117118

118119
# Records the events which happen in a program.
119-
def initialize
120+
def initialize(thread_id: nil)
120121
@events = []
121122
@last_package_for_thread = {}
122123
@methods = Set.new
123124
@stack_printer = StackPrinter.new if StackPrinter.enabled?
124125
@enabled = false
126+
@thread_id = thread_id
125127
end
126128

127129
def enable
@@ -143,6 +145,8 @@ def disable # :nodoc:
143145
def record_event(event, package: nil, defined_class: nil, method: nil)
144146
return unless @enabled
145147

148+
raise "Expected event in thread #{@thread_id}, got #{event.thread_id}" if @thread_id && @thread_id != event.thread_id
149+
146150
@stack_printer.record(event) if @stack_printer
147151
@last_package_for_thread[Thread.current.object_id] = package if package
148152
@events << event

0 commit comments

Comments
 (0)