agent_client_protocol_trace_viewer/
lib.rs1use 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(Debug, Clone)]
23pub enum TraceSource {
24 File(PathBuf),
26 Memory(Arc<Mutex<Vec<serde_json::Value>>>),
28}
29
30#[derive(Debug, 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
40 .lock()
41 .expect("events mutex poisoned")
42 .push(event);
43 }
44
45 #[must_use]
47 pub fn len(&self) -> usize {
48 self.events.lock().expect("events mutex poisoned").len()
49 }
50
51 #[must_use]
53 pub fn is_empty(&self) -> bool {
54 self.events
55 .lock()
56 .expect("events mutex poisoned")
57 .is_empty()
58 }
59}
60
61struct AppState {
62 source: TraceSource,
63}
64
65#[derive(Debug)]
67pub struct TraceViewerConfig {
68 pub port: u16,
70 pub open_browser: bool,
72}
73
74impl Default for TraceViewerConfig {
75 fn default() -> Self {
76 Self {
77 port: 0,
78 open_browser: true,
79 }
80 }
81}
82
83pub fn serve_memory(
103 config: TraceViewerConfig,
104) -> anyhow::Result<(
105 TraceHandle,
106 impl std::future::Future<Output = anyhow::Result<()>>,
107)> {
108 let events = Arc::new(Mutex::new(Vec::new()));
109 let handle = TraceHandle {
110 events: events.clone(),
111 };
112 let source = TraceSource::Memory(events);
113
114 let server = serve_impl(source, config);
115 Ok((handle, server))
116}
117
118pub async fn serve_file(path: PathBuf, config: TraceViewerConfig) -> anyhow::Result<()> {
132 serve_impl(TraceSource::File(path), config).await
133}
134
135async fn serve_impl(source: TraceSource, config: TraceViewerConfig) -> anyhow::Result<()> {
136 let state = Arc::new(AppState { source });
137
138 let app = Router::new()
139 .route("/", get(serve_viewer))
140 .route("/events", get(serve_events))
141 .with_state(state);
142
143 let listener = TcpListener::bind(format!("127.0.0.1:{}", config.port)).await?;
144 let addr = listener.local_addr()?;
145
146 eprintln!("Trace viewer at http://{addr}");
147
148 if config.open_browser {
149 let url = format!("http://{addr}");
150 if let Err(e) = open::that(&url) {
151 eprintln!("Failed to open browser: {e}. Open {url} manually.");
152 }
153 }
154
155 axum::serve(listener, app).await?;
156 Ok(())
157}
158
159async fn serve_viewer() -> Html<&'static str> {
161 Html(VIEWER_HTML)
162}
163
164async fn serve_events(State(state): State<Arc<AppState>>) -> Response {
166 match &state.source {
167 TraceSource::File(path) => serve_events_from_file(path).await,
168 TraceSource::Memory(events) => serve_events_from_memory(events),
169 }
170}
171
172async fn serve_events_from_file(path: &PathBuf) -> Response {
173 match tokio::fs::read_to_string(path).await {
174 Ok(content) => {
175 let events: Vec<serde_json::Value> = content
176 .lines()
177 .filter(|line| !line.trim().is_empty())
178 .filter_map(|line| serde_json::from_str(line).ok())
179 .collect();
180
181 match serde_json::to_string(&events) {
182 Ok(json) => {
183 (StatusCode::OK, [("content-type", "application/json")], json).into_response()
184 }
185 Err(e) => (
186 StatusCode::INTERNAL_SERVER_ERROR,
187 format!("Failed to serialize events: {e}"),
188 )
189 .into_response(),
190 }
191 }
192 Err(e) => (
193 StatusCode::INTERNAL_SERVER_ERROR,
194 format!("Failed to read trace file: {e}"),
195 )
196 .into_response(),
197 }
198}
199
200fn serve_events_from_memory(events: &Arc<Mutex<Vec<serde_json::Value>>>) -> Response {
201 let events = events.lock().expect("events mutex poisoned");
202 match serde_json::to_string(&*events) {
203 Ok(json) => (StatusCode::OK, [("content-type", "application/json")], json).into_response(),
204 Err(e) => (
205 StatusCode::INTERNAL_SERVER_ERROR,
206 format!("Failed to serialize events: {e}"),
207 )
208 .into_response(),
209 }
210}