browser_control/cli/
trace.rs1use std::time::Instant;
22
23use crate::detect::Engine;
24
25pub struct CommandTrace {
35 command: &'static str,
36 start: Instant,
37 browser: String,
38 engine: String,
39 route: &'static str,
40 tab_name: String,
41 target_id: String,
42}
43
44impl CommandTrace {
45 pub fn new(command: &'static str) -> Self {
49 Self {
50 command,
51 start: Instant::now(),
52 browser: String::new(),
53 engine: String::new(),
54 route: "",
55 tab_name: String::new(),
56 target_id: String::new(),
57 }
58 }
59
60 pub fn browser(&mut self, s: impl Into<String>) -> &mut Self {
61 self.browser = s.into();
62 self
63 }
64
65 pub fn engine(&mut self, e: Engine) -> &mut Self {
66 self.engine = match e {
67 Engine::Cdp => "cdp".into(),
68 Engine::Bidi => "bidi".into(),
69 };
70 self
71 }
72
73 pub fn route(&mut self, r: &'static str) -> &mut Self {
81 self.route = r;
82 self
83 }
84
85 pub fn tab_name(&mut self, s: impl Into<String>) -> &mut Self {
86 self.tab_name = s.into();
87 self
88 }
89
90 pub fn target_id(&mut self, s: impl Into<String>) -> &mut Self {
91 self.target_id = s.into();
92 self
93 }
94
95 pub fn ok<T>(self, val: T) -> T {
99 self.emit("ok", None);
100 val
101 }
102
103 pub fn err(self, err: anyhow::Error) -> anyhow::Error {
106 let msg = format!("{err:#}");
107 self.emit("err", Some(&msg));
108 err
109 }
110
111 pub fn finish(self, result: anyhow::Result<()>) -> anyhow::Result<()> {
116 match result {
117 Ok(()) => {
118 self.ok(());
119 Ok(())
120 }
121 Err(e) => Err(self.err(e)),
122 }
123 }
124
125 fn emit(&self, outcome: &'static str, err_msg: Option<&str>) {
126 let elapsed_ms = self.start.elapsed().as_millis() as u64;
127 tracing::info!(
128 target: "browser_control::cli",
129 command = self.command,
130 browser = self.browser.as_str(),
131 engine = self.engine.as_str(),
132 route = self.route,
133 tab_name = self.tab_name.as_str(),
134 target_id = self.target_id.as_str(),
135 elapsed_ms,
136 outcome,
137 err = err_msg.unwrap_or(""),
138 );
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145 use tracing::subscriber::with_default;
146
147 #[test]
150 fn ok_emits_one_line_with_full_schema() {
151 let captured: std::sync::Arc<std::sync::Mutex<Vec<String>>> =
152 std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
153 let sub = TestSubscriber::new(captured.clone());
154 with_default(sub, || {
155 let mut t = CommandTrace::new("eval");
156 t.browser("brave-twilight")
157 .engine(Engine::Cdp)
158 .route("scratch")
159 .target_id("T42");
160 let _ = t.ok(123);
161 });
162 let lines = captured.lock().unwrap();
163 assert_eq!(lines.len(), 1, "exactly one line emitted");
164 let line = &lines[0];
165 assert!(line.contains("command=\"eval\""), "command field: {line}");
166 assert!(line.contains("browser=\"brave-twilight\""));
167 assert!(line.contains("engine=\"cdp\""));
168 assert!(line.contains("route=\"scratch\""));
169 assert!(line.contains("target_id=\"T42\""));
170 assert!(line.contains("outcome=\"ok\""));
171 assert!(line.contains("elapsed_ms="));
172 }
173
174 #[test]
175 fn err_emits_with_outcome_err_and_msg() {
176 let captured: std::sync::Arc<std::sync::Mutex<Vec<String>>> =
177 std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
178 let sub = TestSubscriber::new(captured.clone());
179 with_default(sub, || {
180 let mut t = CommandTrace::new("fetch");
181 t.browser("chrome-pikachu")
182 .engine(Engine::Cdp)
183 .route("named-tab")
184 .tab_name("scrape-cart");
185 let _ = t.err(anyhow::anyhow!("simulated failure"));
186 });
187 let line = captured.lock().unwrap().pop().unwrap();
188 assert!(line.contains("command=\"fetch\""));
189 assert!(line.contains("outcome=\"err\""));
190 assert!(line.contains("err=\"simulated failure\""));
191 assert!(line.contains("tab_name=\"scrape-cart\""));
192 }
193
194 struct TestSubscriber {
198 captured: std::sync::Arc<std::sync::Mutex<Vec<String>>>,
199 }
200 impl TestSubscriber {
201 fn new(captured: std::sync::Arc<std::sync::Mutex<Vec<String>>>) -> Self {
202 Self { captured }
203 }
204 }
205 impl tracing::Subscriber for TestSubscriber {
206 fn enabled(&self, _metadata: &tracing::Metadata<'_>) -> bool {
207 true
208 }
209 fn new_span(&self, _span: &tracing::span::Attributes<'_>) -> tracing::span::Id {
210 tracing::span::Id::from_u64(1)
211 }
212 fn record(&self, _span: &tracing::span::Id, _values: &tracing::span::Record<'_>) {}
213 fn record_follows_from(&self, _span: &tracing::span::Id, _follows: &tracing::span::Id) {}
214 fn event(&self, event: &tracing::Event<'_>) {
215 let mut visitor = StringVisitor {
216 fields: String::new(),
217 };
218 event.record(&mut visitor);
219 self.captured.lock().unwrap().push(visitor.fields);
220 }
221 fn enter(&self, _span: &tracing::span::Id) {}
222 fn exit(&self, _span: &tracing::span::Id) {}
223 }
224 struct StringVisitor {
225 fields: String,
226 }
227 impl tracing::field::Visit for StringVisitor {
228 fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
229 use std::fmt::Write;
230 let _ = write!(self.fields, "{}={:?} ", field.name(), value);
231 }
232 fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
233 use std::fmt::Write;
234 let _ = write!(self.fields, "{}={:?} ", field.name(), value);
235 }
236 fn record_u64(&mut self, field: &tracing::field::Field, value: u64) {
237 use std::fmt::Write;
238 let _ = write!(self.fields, "{}={} ", field.name(), value);
239 }
240 }
241}