Skip to main content

sacp_trace_viewer/
lib.rs

1//! SACP Trace Viewer Library
2//!
3//! Provides an interactive sequence diagram viewer for SACP trace events.
4//! Can serve events from memory (for live viewing) or from a file.
5
6use std::path::PathBuf;
7use std::sync::{Arc, Mutex};
8
9use axum::{
10    Router,
11    extract::State,
12    http::StatusCode,
13    response::{Html, IntoResponse, Response},
14    routing::get,
15};
16use tokio::net::TcpListener;
17
18/// The HTML viewer page (embedded at compile time).
19pub const VIEWER_HTML: &str = include_str!("viewer.html");
20
21/// Source of trace events for the viewer.
22#[derive(Clone)]
23pub enum TraceSource {
24    /// Read events from a file (re-reads on each request for live updates).
25    File(PathBuf),
26    /// Read events from shared memory.
27    Memory(Arc<Mutex<Vec<serde_json::Value>>>),
28}
29
30/// Handle to push events when using memory-backed trace source.
31#[derive(Clone)]
32pub struct TraceHandle {
33    events: Arc<Mutex<Vec<serde_json::Value>>>,
34}
35
36impl TraceHandle {
37    /// Push a new event to the trace.
38    pub fn push(&self, event: serde_json::Value) {
39        self.events.lock().unwrap().push(event);
40    }
41
42    /// Get the current number of events.
43    pub fn len(&self) -> usize {
44        self.events.lock().unwrap().len()
45    }
46
47    /// Check if empty.
48    pub fn is_empty(&self) -> bool {
49        self.events.lock().unwrap().is_empty()
50    }
51}
52
53struct AppState {
54    source: TraceSource,
55}
56
57/// Configuration for the trace viewer server.
58pub struct TraceViewerConfig {
59    /// Port to serve on (0 = auto-select).
60    pub port: u16,
61    /// Whether to open browser automatically.
62    pub open_browser: bool,
63}
64
65impl Default for TraceViewerConfig {
66    fn default() -> Self {
67        Self {
68            port: 0,
69            open_browser: true,
70        }
71    }
72}
73
74/// Start the trace viewer server with a memory-backed event source.
75///
76/// Returns a handle to push events and a future that runs the server.
77/// The server will poll for new events automatically.
78///
79/// # Example
80///
81/// ```no_run
82/// # async fn example() -> anyhow::Result<()> {
83/// let (handle, server) = sacp_trace_viewer::serve_memory(Default::default()).await?;
84///
85/// // Push events from your application
86/// handle.push(serde_json::json!({"type": "request", "method": "test"}));
87///
88/// // Run the server (or spawn it)
89/// server.await?;
90/// # Ok(())
91/// # }
92/// ```
93pub async fn serve_memory(
94    config: TraceViewerConfig,
95) -> anyhow::Result<(
96    TraceHandle,
97    impl std::future::Future<Output = anyhow::Result<()>>,
98)> {
99    let events = Arc::new(Mutex::new(Vec::new()));
100    let handle = TraceHandle {
101        events: events.clone(),
102    };
103    let source = TraceSource::Memory(events);
104
105    let server = serve_impl(source, config);
106    Ok((handle, server))
107}
108
109/// Start the trace viewer server with a file-backed event source.
110///
111/// The file is re-read on each request, allowing live updates as the file grows.
112///
113/// # Example
114///
115/// ```no_run
116/// # use std::path::PathBuf;
117/// # async fn example() -> anyhow::Result<()> {
118/// sacp_trace_viewer::serve_file(PathBuf::from("trace.jsons"), Default::default()).await?;
119/// # Ok(())
120/// # }
121/// ```
122pub async fn serve_file(path: PathBuf, config: TraceViewerConfig) -> anyhow::Result<()> {
123    serve_impl(TraceSource::File(path), config).await
124}
125
126async fn serve_impl(source: TraceSource, config: TraceViewerConfig) -> anyhow::Result<()> {
127    let state = Arc::new(AppState { source });
128
129    let app = Router::new()
130        .route("/", get(serve_viewer))
131        .route("/events", get(serve_events))
132        .with_state(state);
133
134    let listener = TcpListener::bind(format!("127.0.0.1:{}", config.port)).await?;
135    let addr = listener.local_addr()?;
136
137    eprintln!("Trace viewer at http://{}", addr);
138
139    if config.open_browser {
140        let url = format!("http://{}", addr);
141        if let Err(e) = open::that(&url) {
142            eprintln!("Failed to open browser: {}. Open {} manually.", e, url);
143        }
144    }
145
146    axum::serve(listener, app).await?;
147    Ok(())
148}
149
150/// Serve the main viewer HTML page.
151async fn serve_viewer() -> Html<&'static str> {
152    Html(VIEWER_HTML)
153}
154
155/// Serve the trace events as JSON.
156async fn serve_events(State(state): State<Arc<AppState>>) -> Response {
157    match &state.source {
158        TraceSource::File(path) => serve_events_from_file(path).await,
159        TraceSource::Memory(events) => serve_events_from_memory(events),
160    }
161}
162
163async fn serve_events_from_file(path: &PathBuf) -> Response {
164    match tokio::fs::read_to_string(path).await {
165        Ok(content) => {
166            let events: Vec<serde_json::Value> = content
167                .lines()
168                .filter(|line| !line.trim().is_empty())
169                .filter_map(|line| serde_json::from_str(line).ok())
170                .collect();
171
172            match serde_json::to_string(&events) {
173                Ok(json) => {
174                    (StatusCode::OK, [("content-type", "application/json")], json).into_response()
175                }
176                Err(e) => (
177                    StatusCode::INTERNAL_SERVER_ERROR,
178                    format!("Failed to serialize events: {}", e),
179                )
180                    .into_response(),
181            }
182        }
183        Err(e) => (
184            StatusCode::INTERNAL_SERVER_ERROR,
185            format!("Failed to read trace file: {}", e),
186        )
187            .into_response(),
188    }
189}
190
191fn serve_events_from_memory(events: &Arc<Mutex<Vec<serde_json::Value>>>) -> Response {
192    let events = events.lock().unwrap();
193    match serde_json::to_string(&*events) {
194        Ok(json) => (StatusCode::OK, [("content-type", "application/json")], json).into_response(),
195        Err(e) => (
196            StatusCode::INTERNAL_SERVER_ERROR,
197            format!("Failed to serialize events: {}", e),
198        )
199            .into_response(),
200    }
201}