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 @@ def event_loop
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 @@ def start_recording
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 @@ def stop_recording(req)
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 . 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
82
123
end
83
124
84
125
def recording_state
@@ -92,9 +133,9 @@ def handle_record_request(req)
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 @@ def html_response?(headers)
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
0 commit comments