22
33module 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
0 commit comments