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