rama_http/layer/har/recorder/
mod.rs1use super::spec;
34use crate::BodyCaptureEvent;
35use jiff::Timestamp;
36use rama_core::error::BoxError;
37use rama_core::extensions::Extension;
38use rama_http_types::mime::Mime;
39use std::{
40 fmt,
41 future::Future,
42 pin::Pin,
43 sync::{
44 Arc,
45 atomic::{AtomicBool, Ordering},
46 },
47};
48use tokio::sync::mpsc;
49use tokio::time::Instant;
50
51mod fs;
52pub use fs::{FileRecorder, HarFilePath};
53use rama_core::extensions::Extensions;
54use rama_utils::str::NonEmptyStr;
55use rama_utils::str::arcstr::ArcStr;
56
57#[derive(Debug, Clone)]
58pub struct LogMetaInfo {
60 pub version: NonEmptyStr,
62 pub creator: spec::Creator,
64 pub browser: Option<spec::Browser>,
66 pub comment: Option<ArcStr>,
68}
69
70impl Default for LogMetaInfo {
71 fn default() -> Self {
72 let log = spec::Log::default();
73 Self {
74 version: log.version,
75 creator: log.creator,
76 browser: log.browser,
77 comment: log.comment,
78 }
79 }
80}
81
82impl From<LogMetaInfo> for spec::Log {
83 fn from(meta: LogMetaInfo) -> Self {
84 Self {
85 version: meta.version,
86 creator: meta.creator,
87 browser: meta.browser,
88 pages: None,
89 entries: Vec::new(),
90 comment: meta.comment,
91 }
92 }
93}
94
95#[derive(Debug)]
101pub struct BodyCaptureStream {
102 receiver: mpsc::Receiver<BodyCaptureEvent>,
103}
104
105impl BodyCaptureStream {
106 pub async fn next_event(&mut self) -> Option<BodyCaptureEvent> {
108 self.receiver.recv().await
109 }
110
111 pub(crate) fn try_next_event(&mut self) -> Option<BodyCaptureEvent> {
112 self.receiver.try_recv().ok()
113 }
114}
115
116pub(crate) fn body_capture_channel() -> (mpsc::Sender<BodyCaptureEvent>, BodyCaptureStream) {
117 let (sender, receiver) = mpsc::channel(1);
120 (sender, BodyCaptureStream { receiver })
121}
122
123#[derive(Debug)]
125pub struct HttpRequestCapture {
126 pub started_date_time: Timestamp,
128 pub begin: Instant,
130 pub request: spec::Request,
132 pub body_mime_type: Option<Mime>,
134 pub body: BodyCaptureStream,
136 pub web_socket: bool,
138}
139
140pub trait WebSocketCaptureRecorder: Send + Sync + 'static {
152 fn record(
154 &self,
155 message: spec::WebSocketMessage,
156 ) -> impl Future<Output = Result<(), BoxError>> + Send + '_;
157}
158
159trait DynWebSocketCaptureRecorder: Send + Sync + 'static {
160 fn record_box(
161 self: Arc<Self>,
162 message: spec::WebSocketMessage,
163 ) -> Pin<Box<dyn Future<Output = Result<(), BoxError>> + Send + 'static>>;
164}
165
166impl<R> DynWebSocketCaptureRecorder for R
167where
168 R: WebSocketCaptureRecorder,
169{
170 fn record_box(
171 self: Arc<Self>,
172 message: spec::WebSocketMessage,
173 ) -> Pin<Box<dyn Future<Output = Result<(), BoxError>> + Send + 'static>> {
174 Box::pin(async move { self.record(message).await })
175 }
176}
177
178struct WebSocketCaptureShared {
179 closed: AtomicBool,
180 close: Box<dyn Fn() + Send + Sync>,
181}
182
183impl WebSocketCaptureShared {
184 fn close(&self) {
185 if !self.closed.swap(true, Ordering::AcqRel) {
186 (self.close)();
187 }
188 }
189}
190
191struct WebSocketCaptureSlot {
192 shared: Arc<WebSocketCaptureShared>,
193 recorder: Arc<dyn DynWebSocketCaptureRecorder>,
194 claimed: AtomicBool,
195}
196
197impl Drop for WebSocketCaptureSlot {
198 fn drop(&mut self) {
199 self.shared.close();
200 }
201}
202
203#[derive(Clone, Extension)]
205#[extension(tags(http))]
206pub struct WebSocketCapture {
207 slot: Arc<WebSocketCaptureSlot>,
208}
209
210impl fmt::Debug for WebSocketCapture {
211 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
212 formatter
213 .debug_struct("WebSocketCapture")
214 .finish_non_exhaustive()
215 }
216}
217
218impl WebSocketCapture {
219 #[must_use]
224 pub fn new<R, C>(recorder: R, close: C) -> Self
225 where
226 R: WebSocketCaptureRecorder,
227 C: Fn() + Send + Sync + 'static,
228 {
229 let shared = Arc::new(WebSocketCaptureShared {
230 closed: AtomicBool::new(false),
231 close: Box::new(close),
232 });
233 Self {
234 slot: Arc::new(WebSocketCaptureSlot {
235 shared,
236 recorder: Arc::new(recorder),
237 claimed: AtomicBool::new(false),
238 }),
239 }
240 }
241
242 #[must_use]
248 pub fn lease(&self) -> Option<WebSocketCaptureLease> {
249 if self.slot.shared.closed.load(Ordering::Acquire)
250 || self
251 .slot
252 .claimed
253 .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
254 .is_err()
255 {
256 None
257 } else {
258 Some(WebSocketCaptureLease {
259 slot: self.slot.clone(),
260 })
261 }
262 }
263
264 pub fn close(&self) {
266 self.slot.shared.close();
267 }
268
269 pub(crate) fn close_handle(&self) -> WebSocketCaptureCloseHandle {
270 WebSocketCaptureCloseHandle(self.slot.shared.clone())
271 }
272}
273
274pub(crate) struct WebSocketCaptureCloseHandle(Arc<WebSocketCaptureShared>);
275
276impl WebSocketCaptureCloseHandle {
277 pub(crate) fn close(&self) {
278 self.0.close();
279 }
280}
281
282impl Drop for WebSocketCaptureCloseHandle {
283 fn drop(&mut self) {
284 self.close();
285 }
286}
287
288pub struct WebSocketCaptureLease {
290 slot: Arc<WebSocketCaptureSlot>,
291}
292
293pub struct WebSocketCaptureFuture {
299 inner: WebSocketCaptureFutureInner,
300}
301
302enum WebSocketCaptureFutureInner {
303 Closed,
304 Recording(Pin<Box<dyn Future<Output = Result<(), BoxError>> + Send + 'static>>),
305}
306
307impl Future for WebSocketCaptureFuture {
308 type Output = Result<(), BoxError>;
309
310 fn poll(
311 self: Pin<&mut Self>,
312 ctx: &mut std::task::Context<'_>,
313 ) -> std::task::Poll<Self::Output> {
314 match &mut self.get_mut().inner {
315 WebSocketCaptureFutureInner::Closed => std::task::Poll::Ready(Ok(())),
316 WebSocketCaptureFutureInner::Recording(future) => future.as_mut().poll(ctx),
317 }
318 }
319}
320
321impl fmt::Debug for WebSocketCaptureLease {
322 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
323 formatter
324 .debug_struct("WebSocketCaptureLease")
325 .finish_non_exhaustive()
326 }
327}
328
329impl WebSocketCaptureLease {
330 #[must_use]
332 pub fn is_closed(&self) -> bool {
333 self.slot.shared.closed.load(Ordering::Acquire)
334 }
335
336 pub fn record(&self, message: spec::WebSocketMessage) -> WebSocketCaptureFuture {
338 let inner = if self.is_closed() {
339 WebSocketCaptureFutureInner::Closed
340 } else {
341 WebSocketCaptureFutureInner::Recording(self.slot.recorder.clone().record_box(message))
342 };
343 WebSocketCaptureFuture { inner }
344 }
345
346 pub fn close(&self) {
351 self.slot.shared.close();
352 }
353}
354
355impl Drop for WebSocketCaptureLease {
356 fn drop(&mut self) {
357 self.slot.shared.close();
358 }
359}
360
361#[derive(Debug)]
363pub struct HttpResponseCapture {
364 pub response: spec::Response,
366 pub body: BodyCaptureStream,
368}
369
370pub trait RecorderSession: Send + 'static {
372 fn web_socket_capture(&self) -> Option<WebSocketCapture> {
378 None
379 }
380
381 fn record_response(
387 self,
388 response: HttpResponseCapture,
389 ) -> impl Future<Output = Option<Extensions>> + Send;
390
391 fn record_request_only(self) -> impl Future<Output = Option<Extensions>> + Send;
393}
394
395pub trait Recorder: Send + Sync + 'static {
396 fn record(&self, entry: spec::Log) -> impl Future<Output = Option<Extensions>> + Send + '_;
401
402 fn stop_record(&self) -> impl Future<Output = ()> + Send;
407}
408
409impl<R: Recorder> Recorder for Arc<R> {
410 fn record(&self, log: spec::Log) -> impl Future<Output = Option<Extensions>> + Send + '_ {
411 (**self).record(log)
412 }
413
414 fn stop_record(&self) -> impl Future<Output = ()> + Send {
415 (**self).stop_record()
416 }
417}
418
419pub trait StreamingRecorder: Recorder {
421 type Session: RecorderSession;
423
424 fn start_http_recording(
429 &self,
430 request: HttpRequestCapture,
431 ) -> impl Future<Output = Option<Self::Session>> + Send + '_;
432}
433
434impl<R: StreamingRecorder> StreamingRecorder for Arc<R> {
435 type Session = R::Session;
436
437 fn start_http_recording(
438 &self,
439 request: HttpRequestCapture,
440 ) -> impl Future<Output = Option<Self::Session>> + Send + '_ {
441 (**self).start_http_recording(request)
442 }
443}
444
445#[cfg(test)]
446mod tests {
447 use super::*;
448 use parking_lot::Mutex;
449 use std::sync::atomic::{AtomicUsize, Ordering};
450
451 #[derive(Default)]
452 struct TestWebSocketState {
453 messages: Mutex<Vec<spec::WebSocketMessage>>,
454 closes: AtomicUsize,
455 }
456
457 struct TestWebSocketRecorder(Arc<TestWebSocketState>);
458
459 impl WebSocketCaptureRecorder for TestWebSocketRecorder {
460 async fn record(&self, message: spec::WebSocketMessage) -> Result<(), BoxError> {
461 self.0.messages.lock().push(message);
462 Ok(())
463 }
464 }
465
466 #[tokio::test]
467 async fn web_socket_capture_has_one_lifetime_owner() {
468 let state = Arc::new(TestWebSocketState::default());
469 let capture = WebSocketCapture::new(TestWebSocketRecorder(state.clone()), {
470 let state = state.clone();
471 move || {
472 state.closes.fetch_add(1, Ordering::AcqRel);
473 }
474 });
475 let lease = capture.lease().expect("first claimant");
476 assert!(
477 capture.lease().is_none(),
478 "second claimant must be rejected"
479 );
480
481 lease
482 .record(spec::WebSocketMessage::text(
483 spec::WebSocketMessageType::Send,
484 1.0,
485 "hello",
486 ))
487 .await
488 .unwrap();
489 assert_eq!(state.messages.lock().len(), 1);
490 assert_eq!(state.closes.load(Ordering::Acquire), 0);
491
492 drop(lease);
493 assert_eq!(state.closes.load(Ordering::Acquire), 1);
494 drop(capture);
495 assert_eq!(state.closes.load(Ordering::Acquire), 1);
496 }
497
498 #[test]
499 fn dropping_close_handle_stops_web_socket_capture() {
500 let state = Arc::new(TestWebSocketState::default());
501 let capture = WebSocketCapture::new(TestWebSocketRecorder(state.clone()), {
502 let state = state.clone();
503 move || {
504 state.closes.fetch_add(1, Ordering::AcqRel);
505 }
506 });
507
508 drop(capture.close_handle());
509
510 assert_eq!(state.closes.load(Ordering::Acquire), 1);
511 assert!(capture.lease().is_none(), "closed capture rejects leases");
512 }
513
514 #[test]
515 fn explicitly_closed_web_socket_capture_rejects_claims() {
516 let state = Arc::new(TestWebSocketState::default());
517 let capture = WebSocketCapture::new(TestWebSocketRecorder(state.clone()), {
518 let state = state.clone();
519 move || {
520 state.closes.fetch_add(1, Ordering::AcqRel);
521 }
522 });
523
524 capture.close();
525 capture.close();
526 assert_eq!(state.closes.load(Ordering::Acquire), 1);
527 assert!(
528 capture.lease().is_none(),
529 "closed capture cannot be claimed"
530 );
531 }
532
533 #[tokio::test]
534 async fn explicitly_closed_web_socket_lease_stops_recording() {
535 let state = Arc::new(TestWebSocketState::default());
536 let capture = WebSocketCapture::new(TestWebSocketRecorder(state.clone()), {
537 let state = state.clone();
538 move || {
539 state.closes.fetch_add(1, Ordering::AcqRel);
540 }
541 });
542 let lease = capture.lease().expect("capture lease");
543
544 lease.close();
545 lease
546 .record(spec::WebSocketMessage::text(
547 spec::WebSocketMessageType::Send,
548 1.0,
549 "ignored",
550 ))
551 .await
552 .unwrap();
553
554 assert_eq!(state.closes.load(Ordering::Acquire), 1);
555 assert!(state.messages.lock().is_empty());
556 }
557
558 #[test]
559 fn unclaimed_web_socket_capture_closes_on_drop() {
560 let state = Arc::new(TestWebSocketState::default());
561 let capture = WebSocketCapture::new(TestWebSocketRecorder(state.clone()), {
562 let state = state.clone();
563 move || {
564 state.closes.fetch_add(1, Ordering::AcqRel);
565 }
566 });
567
568 drop(capture);
569 assert_eq!(state.closes.load(Ordering::Acquire), 1);
570 }
571}