1#[macro_export]
12macro_rules! trace {
13 ($($tokens:tt)*) => {
14 #[cfg(feature = "tracing")]
15 ::tracing::trace!($($tokens)*)
16 }
17}
18
19#[macro_export]
20macro_rules! debug {
21 ($($tokens:tt)*) => {
22 #[cfg(feature = "tracing")]
23 ::tracing::debug!($($tokens)*)
24 }
25}
26
27#[macro_export]
28macro_rules! warn {
29 ($($tokens:tt)*) => {
30 #[cfg(feature = "tracing")]
31 ::tracing::warn!($($tokens)*)
32 }
33}
34
35#[macro_export]
36macro_rules! info {
37 ($($tokens:tt)*) => {
38 #[cfg(feature = "tracing")]
39 ::tracing::info!($($tokens)*)
40 }
41}
42
43#[macro_export]
44macro_rules! error {
45 ($($tokens:tt)*) => {
46 #[cfg(feature = "tracing")]
47 ::tracing::error!($($tokens)*)
48 }
49}
50
51#[cfg(feature = "tracing")]
52pub const IO_SPAN_TARGET: &str = "IO";
53
54#[macro_export]
58macro_rules! trace_io {
59 ($name:expr, $($args:tt)*) => {
60 ::tracing::trace_span!(
61 target: $crate::utils::tracing::IO_SPAN_TARGET,
62 $name,
63 $($args)*,
64 location = $crate::utils::tracing::format_location(*::std::panic::Location::caller())
65 ).entered()
66 }
67}
68
69#[cfg(feature = "tracing")]
70pub fn format_location(location: std::panic::Location<'static>) -> String {
71 format!("{}:{}", location.file(), location.line())
72}
73
74#[cfg(feature = "tracing")]
75const COMMAND_SPAN_TARGET: &str = "COMMAND";
76
77#[cfg(feature = "tracing")]
78pub fn trace_cmd(command: &crate::BootstrapCommand) -> tracing::span::EnteredSpan {
79 let fingerprint = command.fingerprint();
80 let location = command.get_created_location();
81 let location = format_location(location);
82
83 tracing::span!(
84 target: COMMAND_SPAN_TARGET,
85 tracing::Level::TRACE,
86 "cmd",
87 cmd_name = fingerprint.program_name().to_string(),
88 cmd = fingerprint.format_short_cmd(),
89 full_cmd = ?command,
90 location
91 )
92 .entered()
93}
94
95#[cfg(feature = "tracing")]
106mod inner {
107 use std::fmt::Debug;
108 use std::fs::File;
109 use std::io::Write;
110 use std::path::{Path, PathBuf};
111 use std::sync::atomic::Ordering;
112
113 use chrono::{DateTime, Utc};
114 use tracing::field::{Field, Visit};
115 use tracing::{Event, Id, Level, Subscriber};
116 use tracing_subscriber::layer::{Context, SubscriberExt};
117 use tracing_subscriber::registry::{LookupSpan, SpanRef};
118 use tracing_subscriber::{EnvFilter, Layer};
119
120 use super::{COMMAND_SPAN_TARGET, IO_SPAN_TARGET};
121 use crate::STEP_SPAN_TARGET;
122
123 pub fn setup_tracing(env_name: &str) -> TracingGuard {
124 let filter = EnvFilter::from_env(env_name);
125
126 let registry = tracing_subscriber::registry().with(filter).with(TracingPrinter::default());
127
128 let tempdir = tempfile::TempDir::new().expect("Cannot create temporary directory");
134 let chrome_tracing_path = tempdir.path().join("bootstrap-trace.json");
135 let file = std::io::BufWriter::new(File::create(&chrome_tracing_path).unwrap());
136
137 let chrome_layer = tracing_chrome::ChromeLayerBuilder::new()
138 .writer(file)
139 .include_args(true)
140 .name_fn(Box::new(|event_or_span| match event_or_span {
141 tracing_chrome::EventOrSpan::Event(e) => e.metadata().name().to_string(),
142 tracing_chrome::EventOrSpan::Span(s) => {
143 if s.metadata().target() == STEP_SPAN_TARGET
144 && let Some(extension) = s.extensions().get::<StepNameExtension>()
145 {
146 extension.0.clone()
147 } else if s.metadata().target() == COMMAND_SPAN_TARGET
148 && let Some(extension) = s.extensions().get::<CommandNameExtension>()
149 {
150 extension.0.clone()
151 } else {
152 s.metadata().name().to_string()
153 }
154 }
155 }));
156 let (chrome_layer, guard) = chrome_layer.build();
157
158 tracing::subscriber::set_global_default(registry.with(chrome_layer)).unwrap();
159 TracingGuard { guard, _tempdir: tempdir, chrome_tracing_path }
160 }
161
162 pub struct TracingGuard {
163 guard: tracing_chrome::FlushGuard,
164 _tempdir: tempfile::TempDir,
165 chrome_tracing_path: std::path::PathBuf,
166 }
167
168 impl TracingGuard {
169 pub fn copy_to_dir(self, dir: &std::path::Path) {
170 drop(self.guard);
171 crate::utils::helpers::move_file(
172 &self.chrome_tracing_path,
173 dir.join("chrome-trace.json"),
174 )
175 .unwrap();
176 }
177 }
178
179 #[derive(Default)]
181 struct FieldValues {
182 message: Option<String>,
184 step_name: Option<String>,
186 cmd_name: Option<String>,
188 fields: Vec<(&'static str, String)>,
190 }
191
192 impl Visit for FieldValues {
193 fn record_str(&mut self, field: &Field, value: &str) {
196 match field.name() {
197 "step_name" => {
198 self.step_name = Some(value.to_string());
199 }
200 "cmd_name" => {
201 self.cmd_name = Some(value.to_string());
202 }
203 name => {
204 self.fields.push((name, value.to_string()));
205 }
206 }
207 }
208
209 fn record_debug(&mut self, field: &Field, value: &dyn Debug) {
210 let formatted = format!("{value:?}");
211 match field.name() {
212 "message" => {
213 self.message = Some(formatted);
214 }
215 name => {
216 self.fields.push((name, formatted));
217 }
218 }
219 }
220 }
221
222 #[derive(Copy, Clone)]
223 enum SpanAction {
224 Enter,
225 }
226
227 struct StepNameExtension(String);
229
230 struct CommandNameExtension(String);
232
233 #[derive(Default)]
234 struct TracingPrinter {
235 indent: std::sync::atomic::AtomicU32,
236 span_values: std::sync::Mutex<std::collections::HashMap<tracing::Id, FieldValues>>,
237 }
238
239 impl TracingPrinter {
240 fn format_header<W: Write>(
241 &self,
242 writer: &mut W,
243 time: DateTime<Utc>,
244 level: &Level,
245 ) -> std::io::Result<()> {
246 let timestamp = time.format("%H:%M:%S.%3f");
248 write!(writer, "{timestamp} ")?;
249 write!(writer, "{level:>5} ")?;
252 write!(writer, "{}", " ".repeat(self.indent.load(Ordering::Relaxed) as usize))
253 }
254
255 fn write_event<W: Write>(&self, writer: &mut W, event: &Event<'_>) -> std::io::Result<()> {
256 let now = Utc::now();
257
258 self.format_header(writer, now, event.metadata().level())?;
259
260 let mut field_values = FieldValues::default();
261 event.record(&mut field_values);
262
263 if let Some(msg) = &field_values.message {
264 write!(writer, "{msg}")?;
265 }
266
267 if !field_values.fields.is_empty() {
268 if field_values.message.is_some() {
269 write!(writer, " ")?;
270 }
271 write!(writer, "[")?;
272 for (index, (name, value)) in field_values.fields.iter().enumerate() {
273 write!(writer, "{name} = {value}")?;
274 if index < field_values.fields.len() - 1 {
275 write!(writer, ", ")?;
276 }
277 }
278 write!(writer, "]")?;
279 }
280 write_location(writer, event.metadata())?;
281 writeln!(writer)?;
282 Ok(())
283 }
284
285 fn write_span<W: Write, S>(
286 &self,
287 writer: &mut W,
288 span: SpanRef<'_, S>,
289 field_values: Option<&FieldValues>,
290 action: SpanAction,
291 ) -> std::io::Result<()>
292 where
293 S: for<'lookup> LookupSpan<'lookup>,
294 {
295 let now = Utc::now();
296
297 self.format_header(writer, now, span.metadata().level())?;
298 match action {
299 SpanAction::Enter => {
300 write!(writer, "> ")?;
301 }
302 }
303
304 fn write_fields<'a, I: IntoIterator<Item = &'a (&'a str, String)>, W: Write>(
305 writer: &mut W,
306 iter: I,
307 ) -> std::io::Result<()> {
308 let items = iter.into_iter().collect::<Vec<_>>();
309 if !items.is_empty() {
310 write!(writer, " [")?;
311 for (index, (name, value)) in items.iter().enumerate() {
312 write!(writer, "{name} = {value}")?;
313 if index < items.len() - 1 {
314 write!(writer, ", ")?;
315 }
316 }
317 write!(writer, "]")?;
318 }
319 Ok(())
320 }
321
322 let write_with_location = |writer: &mut W| -> std::io::Result<()> {
325 if let Some(values) = field_values {
326 write_fields(
327 writer,
328 values.fields.iter().filter(|(name, _)| *name != "location"),
329 )?;
330 let location =
331 &values.fields.iter().find(|(name, _)| *name == "location").unwrap().1;
332 let (filename, line) = location.rsplit_once(':').unwrap();
333 let filename = shorten_filename(filename);
334 write!(writer, " ({filename}:{line})",)?;
335 }
336 Ok(())
337 };
338
339 match span.metadata().target() {
343 STEP_SPAN_TARGET => {
345 let name =
346 field_values.and_then(|v| v.step_name.as_deref()).unwrap_or(span.name());
347 write!(writer, "{name}")?;
348
349 if let Some(values) = field_values {
351 let field = &values.fields[0];
352 write!(writer, " {{{}}}", field.1)?;
353 }
354 write_location(writer, span.metadata())?;
355 }
356 COMMAND_SPAN_TARGET => {
358 write!(writer, "{}", span.name())?;
359 write_with_location(writer)?;
360 }
361 IO_SPAN_TARGET => {
362 write!(writer, "{}", span.name())?;
363 write_with_location(writer)?;
364 }
365 _ => {
367 write!(writer, "{}", span.name())?;
368 if let Some(values) = field_values {
369 write_fields(writer, values.fields.iter())?;
370 }
371 write_location(writer, span.metadata())?;
372 }
373 }
374
375 writeln!(writer)?;
376 Ok(())
377 }
378 }
379
380 fn write_location<W: Write>(
381 writer: &mut W,
382 metadata: &'static tracing::Metadata<'static>,
383 ) -> std::io::Result<()> {
384 if let Some(filename) = metadata.file() {
385 let filename = shorten_filename(filename);
386
387 write!(writer, " ({filename}")?;
388 if let Some(line) = metadata.line() {
389 write!(writer, ":{line}")?;
390 }
391 write!(writer, ")")?;
392 }
393 Ok(())
394 }
395
396 fn shorten_filename(filename: &str) -> String {
398 Path::new(filename)
399 .components()
400 .rev()
402 .take(2)
403 .collect::<Vec<_>>()
404 .into_iter()
405 .rev()
406 .collect::<PathBuf>()
407 .display()
408 .to_string()
409 }
410
411 impl<S> Layer<S> for TracingPrinter
412 where
413 S: Subscriber,
414 S: for<'lookup> LookupSpan<'lookup>,
415 {
416 fn on_new_span(&self, attrs: &tracing::span::Attributes<'_>, id: &Id, ctx: Context<'_, S>) {
417 let mut field_values = FieldValues::default();
421 attrs.record(&mut field_values);
422
423 if attrs.metadata().target() == STEP_SPAN_TARGET
426 && let Some(step_name) = field_values.step_name.clone()
427 {
428 ctx.span(id).unwrap().extensions_mut().insert(StepNameExtension(step_name));
429 } else if attrs.metadata().target() == COMMAND_SPAN_TARGET
430 && let Some(cmd_name) = field_values.cmd_name.clone()
431 {
432 ctx.span(id).unwrap().extensions_mut().insert(CommandNameExtension(cmd_name));
433 }
434 self.span_values.lock().unwrap().insert(id.clone(), field_values);
435 }
436
437 fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) {
438 let mut writer = std::io::stderr().lock();
439 self.write_event(&mut writer, event).unwrap();
440 }
441
442 fn on_enter(&self, id: &Id, ctx: Context<'_, S>) {
443 if let Some(span) = ctx.span(id) {
444 let mut writer = std::io::stderr().lock();
445 let values = self.span_values.lock().unwrap();
446 let values = values.get(id);
447 self.write_span(&mut writer, span, values, SpanAction::Enter).unwrap();
448 }
449 self.indent.fetch_add(1, Ordering::Relaxed);
450 }
451
452 fn on_exit(&self, _id: &Id, _ctx: Context<'_, S>) {
453 self.indent.fetch_sub(1, Ordering::Relaxed);
454 }
455 }
456}
457
458#[cfg(feature = "tracing")]
459pub use inner::setup_tracing;