eyes-subscriber 0.1.4

Tracing subscriber for sending traces to Eyes (eyes.coreyja.com)
Documentation
use async_trait::async_trait;
use futures_util::SinkExt;
use tokio::net::TcpStream;
use tokio_tungstenite::{
    connect_async, tungstenite::protocol::Message, MaybeTlsStream, WebSocketStream,
};
use url::Url;
use uuid::Uuid;

use crate::{
    transport::{Transport, TransportError},
    EventData,
};

pub struct WebSocketTransport {
    url: Url,
    ws_stream: Option<WebSocketStream<MaybeTlsStream<TcpStream>>>,
}

impl WebSocketTransport {
    pub fn new(base_url: Url, org_id: Uuid, app_id: Uuid) -> Result<Self, TransportError> {
        let mut url = base_url.clone();

        // Convert http/https to ws/wss
        match url.scheme() {
            "http" => url.set_scheme("ws").map_err(|_| {
                TransportError::Configuration("Failed to set WebSocket scheme".to_string())
            })?,
            "https" => url.set_scheme("wss").map_err(|_| {
                TransportError::Configuration("Failed to set WebSocket scheme".to_string())
            })?,
            "ws" | "wss" => {}
            _ => {
                return Err(TransportError::Configuration(
                    "Invalid URL scheme for WebSocket".to_string(),
                ));
            }
        }

        url.set_path(&format!("/api/orgs/{}/apps/{}/ws", org_id, app_id));

        Ok(Self {
            url,
            ws_stream: None,
        })
    }
}

#[async_trait]
impl Transport for WebSocketTransport {
    async fn connect(&mut self) -> Result<(), TransportError> {
        let (ws_stream, _) = connect_async(self.url.to_string()).await.map_err(|e| {
            TransportError::Connection(format!("WebSocket connection failed: {}", e))
        })?;

        self.ws_stream = Some(ws_stream);
        Ok(())
    }

    async fn send(&mut self, event: EventData) -> Result<(), TransportError> {
        let ws_stream = self
            .ws_stream
            .as_mut()
            .ok_or_else(|| TransportError::Send("WebSocket not connected".to_string()))?;

        let json = serde_json::to_string(&event)
            .map_err(|e| TransportError::Send(format!("Failed to serialize event: {}", e)))?;

        ws_stream.send(Message::Text(json)).await.map_err(|e| {
            TransportError::Send(format!("Failed to send WebSocket message: {}", e))
        })?;

        Ok(())
    }

    async fn close(&mut self) -> Result<(), TransportError> {
        if let Some(mut ws_stream) = self.ws_stream.take() {
            ws_stream
                .close(None)
                .await
                .map_err(|e| TransportError::Send(format!("Failed to close WebSocket: {}", e)))?;
        }
        Ok(())
    }
}