1use 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
18pub const VIEWER_HTML: &str = include_str!("viewer.html");
20
21#[derive(Clone)]
23pub enum TraceSource {
24 File(PathBuf),
26 Memory(Arc<Mutex<Vec<serde_json::Value>>>),
28}
29
30#[derive(Clone)]
32pub struct TraceHandle {
33 events: Arc<Mutex<Vec<serde_json::Value>>>,
34}
35
36impl TraceHandle {
37 pub fn push(&self, event: serde_json::Value) {
39 self.events.lock().unwrap().push(event);
40 }
41
42 pub fn len(&self) -> usize {
44 self.events.lock().unwrap().len()
45 }
46
47 pub fn is_empty(&self) -> bool {
49 self.events.lock().unwrap().is_empty()
50 }
51}
52
53struct AppState {
54 source: TraceSource,
55}
56
57pub struct TraceViewerConfig {
59 pub port: u16,
61 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
74pub 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
109pub 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
150async fn serve_viewer() -> Html<&'static str> {
152 Html(VIEWER_HTML)
153}
154
155async 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}