use super::spec;
use crate::BodyCaptureEvent;
use jiff::Timestamp;
use rama_core::error::BoxError;
use rama_core::extensions::Extension;
use rama_http_types::mime::Mime;
use std::{
fmt,
future::Future,
pin::Pin,
sync::{
Arc,
atomic::{AtomicBool, Ordering},
},
};
use tokio::sync::mpsc;
use tokio::time::Instant;
mod fs;
pub use fs::{FileRecorder, HarFilePath};
use rama_core::extensions::Extensions;
use rama_utils::str::NonEmptyStr;
use rama_utils::str::arcstr::ArcStr;
#[derive(Debug, Clone)]
pub struct LogMetaInfo {
pub version: NonEmptyStr,
pub creator: spec::Creator,
pub browser: Option<spec::Browser>,
pub comment: Option<ArcStr>,
}
impl Default for LogMetaInfo {
fn default() -> Self {
let log = spec::Log::default();
Self {
version: log.version,
creator: log.creator,
browser: log.browser,
comment: log.comment,
}
}
}
impl From<LogMetaInfo> for spec::Log {
fn from(meta: LogMetaInfo) -> Self {
Self {
version: meta.version,
creator: meta.creator,
browser: meta.browser,
pages: None,
entries: Vec::new(),
comment: meta.comment,
}
}
}
#[derive(Debug)]
pub struct BodyCaptureStream {
receiver: mpsc::Receiver<BodyCaptureEvent>,
}
impl BodyCaptureStream {
pub async fn next_event(&mut self) -> Option<BodyCaptureEvent> {
self.receiver.recv().await
}
pub(crate) fn try_next_event(&mut self) -> Option<BodyCaptureEvent> {
self.receiver.try_recv().ok()
}
}
pub(crate) fn body_capture_channel() -> (mpsc::Sender<BodyCaptureEvent>, BodyCaptureStream) {
let (sender, receiver) = mpsc::channel(1);
(sender, BodyCaptureStream { receiver })
}
#[derive(Debug)]
pub struct HttpRequestCapture {
pub started_date_time: Timestamp,
pub begin: Instant,
pub request: spec::Request,
pub body_mime_type: Option<Mime>,
pub body: BodyCaptureStream,
pub web_socket: bool,
}
pub trait WebSocketCaptureRecorder: Send + Sync + 'static {
fn record(
&self,
message: spec::WebSocketMessage,
) -> impl Future<Output = Result<(), BoxError>> + Send + '_;
}
trait DynWebSocketCaptureRecorder: Send + Sync + 'static {
fn record_box(
self: Arc<Self>,
message: spec::WebSocketMessage,
) -> Pin<Box<dyn Future<Output = Result<(), BoxError>> + Send + 'static>>;
}
impl<R> DynWebSocketCaptureRecorder for R
where
R: WebSocketCaptureRecorder,
{
fn record_box(
self: Arc<Self>,
message: spec::WebSocketMessage,
) -> Pin<Box<dyn Future<Output = Result<(), BoxError>> + Send + 'static>> {
Box::pin(async move { self.record(message).await })
}
}
struct WebSocketCaptureShared {
closed: AtomicBool,
close: Box<dyn Fn() + Send + Sync>,
}
impl WebSocketCaptureShared {
fn close(&self) {
if !self.closed.swap(true, Ordering::AcqRel) {
(self.close)();
}
}
}
struct WebSocketCaptureSlot {
shared: Arc<WebSocketCaptureShared>,
recorder: Arc<dyn DynWebSocketCaptureRecorder>,
claimed: AtomicBool,
}
impl Drop for WebSocketCaptureSlot {
fn drop(&mut self) {
self.shared.close();
}
}
#[derive(Clone, Extension)]
#[extension(tags(http))]
pub struct WebSocketCapture {
slot: Arc<WebSocketCaptureSlot>,
}
impl fmt::Debug for WebSocketCapture {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("WebSocketCapture")
.finish_non_exhaustive()
}
}
impl WebSocketCapture {
#[must_use]
pub fn new<R, C>(recorder: R, close: C) -> Self
where
R: WebSocketCaptureRecorder,
C: Fn() + Send + Sync + 'static,
{
let shared = Arc::new(WebSocketCaptureShared {
closed: AtomicBool::new(false),
close: Box::new(close),
});
Self {
slot: Arc::new(WebSocketCaptureSlot {
shared,
recorder: Arc::new(recorder),
claimed: AtomicBool::new(false),
}),
}
}
#[must_use]
pub fn lease(&self) -> Option<WebSocketCaptureLease> {
if self.slot.shared.closed.load(Ordering::Acquire)
|| self
.slot
.claimed
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
.is_err()
{
None
} else {
Some(WebSocketCaptureLease {
slot: self.slot.clone(),
})
}
}
pub fn close(&self) {
self.slot.shared.close();
}
pub(crate) fn close_handle(&self) -> WebSocketCaptureCloseHandle {
WebSocketCaptureCloseHandle(self.slot.shared.clone())
}
}
pub(crate) struct WebSocketCaptureCloseHandle(Arc<WebSocketCaptureShared>);
impl WebSocketCaptureCloseHandle {
pub(crate) fn close(&self) {
self.0.close();
}
}
impl Drop for WebSocketCaptureCloseHandle {
fn drop(&mut self) {
self.close();
}
}
pub struct WebSocketCaptureLease {
slot: Arc<WebSocketCaptureSlot>,
}
pub struct WebSocketCaptureFuture {
inner: WebSocketCaptureFutureInner,
}
enum WebSocketCaptureFutureInner {
Closed,
Recording(Pin<Box<dyn Future<Output = Result<(), BoxError>> + Send + 'static>>),
}
impl Future for WebSocketCaptureFuture {
type Output = Result<(), BoxError>;
fn poll(
self: Pin<&mut Self>,
ctx: &mut std::task::Context<'_>,
) -> std::task::Poll<Self::Output> {
match &mut self.get_mut().inner {
WebSocketCaptureFutureInner::Closed => std::task::Poll::Ready(Ok(())),
WebSocketCaptureFutureInner::Recording(future) => future.as_mut().poll(ctx),
}
}
}
impl fmt::Debug for WebSocketCaptureLease {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("WebSocketCaptureLease")
.finish_non_exhaustive()
}
}
impl WebSocketCaptureLease {
#[must_use]
pub fn is_closed(&self) -> bool {
self.slot.shared.closed.load(Ordering::Acquire)
}
pub fn record(&self, message: spec::WebSocketMessage) -> WebSocketCaptureFuture {
let inner = if self.is_closed() {
WebSocketCaptureFutureInner::Closed
} else {
WebSocketCaptureFutureInner::Recording(self.slot.recorder.clone().record_box(message))
};
WebSocketCaptureFuture { inner }
}
pub fn close(&self) {
self.slot.shared.close();
}
}
impl Drop for WebSocketCaptureLease {
fn drop(&mut self) {
self.slot.shared.close();
}
}
#[derive(Debug)]
pub struct HttpResponseCapture {
pub response: spec::Response,
pub body: BodyCaptureStream,
}
pub trait RecorderSession: Send + 'static {
fn web_socket_capture(&self) -> Option<WebSocketCapture> {
None
}
fn record_response(
self,
response: HttpResponseCapture,
) -> impl Future<Output = Option<Extensions>> + Send;
fn record_request_only(self) -> impl Future<Output = Option<Extensions>> + Send;
}
pub trait Recorder: Send + Sync + 'static {
fn record(&self, entry: spec::Log) -> impl Future<Output = Option<Extensions>> + Send + '_;
fn stop_record(&self) -> impl Future<Output = ()> + Send;
}
impl<R: Recorder> Recorder for Arc<R> {
fn record(&self, log: spec::Log) -> impl Future<Output = Option<Extensions>> + Send + '_ {
(**self).record(log)
}
fn stop_record(&self) -> impl Future<Output = ()> + Send {
(**self).stop_record()
}
}
pub trait StreamingRecorder: Recorder {
type Session: RecorderSession;
fn start_http_recording(
&self,
request: HttpRequestCapture,
) -> impl Future<Output = Option<Self::Session>> + Send + '_;
}
impl<R: StreamingRecorder> StreamingRecorder for Arc<R> {
type Session = R::Session;
fn start_http_recording(
&self,
request: HttpRequestCapture,
) -> impl Future<Output = Option<Self::Session>> + Send + '_ {
(**self).start_http_recording(request)
}
}
#[cfg(test)]
mod tests {
use super::*;
use parking_lot::Mutex;
use std::sync::atomic::{AtomicUsize, Ordering};
#[derive(Default)]
struct TestWebSocketState {
messages: Mutex<Vec<spec::WebSocketMessage>>,
closes: AtomicUsize,
}
struct TestWebSocketRecorder(Arc<TestWebSocketState>);
impl WebSocketCaptureRecorder for TestWebSocketRecorder {
async fn record(&self, message: spec::WebSocketMessage) -> Result<(), BoxError> {
self.0.messages.lock().push(message);
Ok(())
}
}
#[tokio::test]
async fn web_socket_capture_has_one_lifetime_owner() {
let state = Arc::new(TestWebSocketState::default());
let capture = WebSocketCapture::new(TestWebSocketRecorder(state.clone()), {
let state = state.clone();
move || {
state.closes.fetch_add(1, Ordering::AcqRel);
}
});
let lease = capture.lease().expect("first claimant");
assert!(
capture.lease().is_none(),
"second claimant must be rejected"
);
lease
.record(spec::WebSocketMessage::text(
spec::WebSocketMessageType::Send,
1.0,
"hello",
))
.await
.unwrap();
assert_eq!(state.messages.lock().len(), 1);
assert_eq!(state.closes.load(Ordering::Acquire), 0);
drop(lease);
assert_eq!(state.closes.load(Ordering::Acquire), 1);
drop(capture);
assert_eq!(state.closes.load(Ordering::Acquire), 1);
}
#[test]
fn dropping_close_handle_stops_web_socket_capture() {
let state = Arc::new(TestWebSocketState::default());
let capture = WebSocketCapture::new(TestWebSocketRecorder(state.clone()), {
let state = state.clone();
move || {
state.closes.fetch_add(1, Ordering::AcqRel);
}
});
drop(capture.close_handle());
assert_eq!(state.closes.load(Ordering::Acquire), 1);
assert!(capture.lease().is_none(), "closed capture rejects leases");
}
#[test]
fn explicitly_closed_web_socket_capture_rejects_claims() {
let state = Arc::new(TestWebSocketState::default());
let capture = WebSocketCapture::new(TestWebSocketRecorder(state.clone()), {
let state = state.clone();
move || {
state.closes.fetch_add(1, Ordering::AcqRel);
}
});
capture.close();
capture.close();
assert_eq!(state.closes.load(Ordering::Acquire), 1);
assert!(
capture.lease().is_none(),
"closed capture cannot be claimed"
);
}
#[tokio::test]
async fn explicitly_closed_web_socket_lease_stops_recording() {
let state = Arc::new(TestWebSocketState::default());
let capture = WebSocketCapture::new(TestWebSocketRecorder(state.clone()), {
let state = state.clone();
move || {
state.closes.fetch_add(1, Ordering::AcqRel);
}
});
let lease = capture.lease().expect("capture lease");
lease.close();
lease
.record(spec::WebSocketMessage::text(
spec::WebSocketMessageType::Send,
1.0,
"ignored",
))
.await
.unwrap();
assert_eq!(state.closes.load(Ordering::Acquire), 1);
assert!(state.messages.lock().is_empty());
}
#[test]
fn unclaimed_web_socket_capture_closes_on_drop() {
let state = Arc::new(TestWebSocketState::default());
let capture = WebSocketCapture::new(TestWebSocketRecorder(state.clone()), {
let state = state.clone();
move || {
state.closes.fetch_add(1, Ordering::AcqRel);
}
});
drop(capture);
assert_eq!(state.closes.load(Ordering::Acquire), 1);
}
}