Skip to main content

bootstrap/utils/
tracing.rs

1//! Wrapper macros for `tracing` macros to avoid having to write `cfg(feature = "tracing")`-gated
2//! `debug!`/`trace!` everytime, e.g.
3//!
4//! ```rust,ignore (example)
5//! #[cfg(feature = "tracing")]
6//! trace!("...");
7//! ```
8//!
9//! When `feature = "tracing"` is inactive, these macros expand to nothing.
10
11#[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/// Create a tracing span around an I/O operation, if tracing is enabled.
55/// Note that at least one tracing value field has to be passed to this macro, otherwise it will not
56/// compile.
57#[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// # Note on `tracing` usage in bootstrap
95//
96// Due to the conditional compilation via the `tracing` cargo feature, this means that `tracing`
97// usages in bootstrap need to be also gated behind the `tracing` feature:
98//
99// - `tracing` macros with log levels (`trace!`, `debug!`, `warn!`, `info`, `error`) should not be
100//   used *directly*. You should use the wrapped `tracing` macros which gate the actual invocations
101//   behind `feature = "tracing"`.
102// - `tracing`'s `#[instrument(..)]` macro will need to be gated like `#![cfg_attr(feature =
103//   "tracing", instrument(..))]`.
104#[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        // When we're creating this layer, we do not yet know the location of the tracing output
131        // directory, because it is stored in the output directory determined after Config is parsed,
132        // but we already want to make tracing calls during (and before) config parsing.
133        // So we store the output into a temporary file, and then move it to the tracing directory
134        // before bootstrap ends.
135        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    /// Visitor that extracts both known and unknown field values from events and spans.
182    #[derive(Default)]
183    struct FieldValues {
184        /// Main event message
185        message: Option<String>,
186        /// Name of a recorded psna
187        step_name: Option<String>,
188        /// Short name of an executed command
189        cmd_name: Option<String>,
190        /// The rest of arbitrary event/span fields
191        fields: Vec<(&'static str, String)>,
192    }
193
194    impl Visit for FieldValues {
195        /// Record fields if possible using `record_str`, to avoid rendering simple strings with
196        /// their `Debug` representation, which adds extra quotes.
197        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    /// Holds the name of a step span, stored in `tracing_subscriber`'s extensions.
230    struct StepNameExtension(String);
231
232    /// Holds the name of a command span, stored in `tracing_subscriber`'s extensions.
233    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                // Use a fixed-width timestamp without date, that shouldn't be very important
251                let timestamp = time.format("%H:%M:%S.%3f");
252                write!(writer, "{timestamp} ")?;
253            }
254            // Make sure that levels are aligned to the same number of characters, in order not to
255            // break the layout
256            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            // Write fields while treating the "location" field specially, and assuming that it
328            // contains the source file location relevant to the span.
329            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            // We handle steps specially. We instrument them dynamically in `Builder::ensure`,
345            // and we want to have custom name for each step span. But tracing doesn't allow setting
346            // dynamic span names. So we detect step spans here and override their name.
347            match span.metadata().target() {
348                // Executed step
349                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                    // There should be only one more field called `args`
355                    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                // Executed command
362                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                // Other span
371                _ => {
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    /// Keep only the module name and file name to make it shorter
402    fn shorten_filename(filename: &str) -> String {
403        Path::new(filename)
404            .components()
405            // Take last two path components
406            .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            // Record value of span fields
423            // Note that we do not implement changing values of span fields after they are created.
424            // For that we would also need to implement the `on_record` method
425            let mut field_values = FieldValues::default();
426            attrs.record(&mut field_values);
427
428            // We need to propagate the actual name of the span to the Chrome layer below, because
429            // it cannot access field values. We do that through extensions.
430            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;