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