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")]
52const COMMAND_SPAN_TARGET: &str = "COMMAND";
53
54#[cfg(feature = "tracing")]
55pub fn trace_cmd(command: &crate::BootstrapCommand) -> tracing::span::EnteredSpan {
56    let fingerprint = command.fingerprint();
57    let location = command.get_created_location();
58    let location = format!("{}:{}", location.file(), location.line());
59
60    tracing::span!(
61        target: COMMAND_SPAN_TARGET,
62        tracing::Level::TRACE,
63        "cmd",
64        cmd_name = fingerprint.program_name().to_string(),
65        cmd = fingerprint.format_short_cmd(),
66        full_cmd = ?command,
67        location
68    )
69    .entered()
70}
71
72// # Note on `tracing` usage in bootstrap
73//
74// Due to the conditional compilation via the `tracing` cargo feature, this means that `tracing`
75// usages in bootstrap need to be also gated behind the `tracing` feature:
76//
77// - `tracing` macros with log levels (`trace!`, `debug!`, `warn!`, `info`, `error`) should not be
78//   used *directly*. You should use the wrapped `tracing` macros which gate the actual invocations
79//   behind `feature = "tracing"`.
80// - `tracing`'s `#[instrument(..)]` macro will need to be gated like `#![cfg_attr(feature =
81//   "tracing", instrument(..))]`.
82#[cfg(feature = "tracing")]
83mod inner {
84    use std::fmt::Debug;
85    use std::fs::File;
86    use std::io::Write;
87    use std::sync::atomic::Ordering;
88
89    use chrono::{DateTime, Utc};
90    use tracing::field::{Field, Visit};
91    use tracing::{Event, Id, Level, Subscriber};
92    use tracing_subscriber::layer::{Context, SubscriberExt};
93    use tracing_subscriber::registry::{LookupSpan, SpanRef};
94    use tracing_subscriber::{EnvFilter, Layer};
95
96    use crate::STEP_SPAN_TARGET;
97    use crate::utils::tracing::COMMAND_SPAN_TARGET;
98
99    pub fn setup_tracing(env_name: &str) -> TracingGuard {
100        let filter = EnvFilter::from_env(env_name);
101
102        let registry = tracing_subscriber::registry().with(filter).with(TracingPrinter::default());
103
104        // When we're creating this layer, we do not yet know the location of the tracing output
105        // directory, because it is stored in the output directory determined after Config is parsed,
106        // but we already want to make tracing calls during (and before) config parsing.
107        // So we store the output into a temporary file, and then move it to the tracing directory
108        // before bootstrap ends.
109        let tempdir = tempfile::TempDir::new().expect("Cannot create temporary directory");
110        let chrome_tracing_path = tempdir.path().join("bootstrap-trace.json");
111        let file = std::io::BufWriter::new(File::create(&chrome_tracing_path).unwrap());
112
113        let chrome_layer = tracing_chrome::ChromeLayerBuilder::new()
114            .writer(file)
115            .include_args(true)
116            .name_fn(Box::new(|event_or_span| match event_or_span {
117                tracing_chrome::EventOrSpan::Event(e) => e.metadata().name().to_string(),
118                tracing_chrome::EventOrSpan::Span(s) => {
119                    if s.metadata().target() == STEP_SPAN_TARGET
120                        && let Some(extension) = s.extensions().get::<StepNameExtension>()
121                    {
122                        extension.0.clone()
123                    } else if s.metadata().target() == COMMAND_SPAN_TARGET
124                        && let Some(extension) = s.extensions().get::<CommandNameExtension>()
125                    {
126                        extension.0.clone()
127                    } else {
128                        s.metadata().name().to_string()
129                    }
130                }
131            }));
132        let (chrome_layer, guard) = chrome_layer.build();
133
134        tracing::subscriber::set_global_default(registry.with(chrome_layer)).unwrap();
135        TracingGuard { guard, _tempdir: tempdir, chrome_tracing_path }
136    }
137
138    pub struct TracingGuard {
139        guard: tracing_chrome::FlushGuard,
140        _tempdir: tempfile::TempDir,
141        chrome_tracing_path: std::path::PathBuf,
142    }
143
144    impl TracingGuard {
145        pub fn copy_to_dir(self, dir: &std::path::Path) {
146            drop(self.guard);
147            std::fs::rename(&self.chrome_tracing_path, dir.join("chrome-trace.json")).unwrap();
148        }
149    }
150
151    /// Visitor that extracts both known and unknown field values from events and spans.
152    #[derive(Default)]
153    struct FieldValues {
154        /// Main event message
155        message: Option<String>,
156        /// Name of a recorded psna
157        step_name: Option<String>,
158        /// Short name of an executed command
159        cmd_name: Option<String>,
160        /// The rest of arbitrary event/span fields
161        fields: Vec<(&'static str, String)>,
162    }
163
164    impl Visit for FieldValues {
165        /// Record fields if possible using `record_str`, to avoid rendering simple strings with
166        /// their `Debug` representation, which adds extra quotes.
167        fn record_str(&mut self, field: &Field, value: &str) {
168            match field.name() {
169                "step_name" => {
170                    self.step_name = Some(value.to_string());
171                }
172                "cmd_name" => {
173                    self.cmd_name = Some(value.to_string());
174                }
175                name => {
176                    self.fields.push((name, value.to_string()));
177                }
178            }
179        }
180
181        fn record_debug(&mut self, field: &Field, value: &dyn Debug) {
182            let formatted = format!("{value:?}");
183            match field.name() {
184                "message" => {
185                    self.message = Some(formatted);
186                }
187                name => {
188                    self.fields.push((name, formatted));
189                }
190            }
191        }
192    }
193
194    #[derive(Copy, Clone)]
195    enum SpanAction {
196        Enter,
197    }
198
199    /// Holds the name of a step span, stored in `tracing_subscriber`'s extensions.
200    struct StepNameExtension(String);
201
202    /// Holds the name of a command span, stored in `tracing_subscriber`'s extensions.
203    struct CommandNameExtension(String);
204
205    #[derive(Default)]
206    struct TracingPrinter {
207        indent: std::sync::atomic::AtomicU32,
208        span_values: std::sync::Mutex<std::collections::HashMap<tracing::Id, FieldValues>>,
209    }
210
211    impl TracingPrinter {
212        fn format_header<W: Write>(
213            &self,
214            writer: &mut W,
215            time: DateTime<Utc>,
216            level: &Level,
217        ) -> std::io::Result<()> {
218            // Use a fixed-width timestamp without date, that shouldn't be very important
219            let timestamp = time.format("%H:%M:%S.%3f");
220            write!(writer, "{timestamp} ")?;
221            // Make sure that levels are aligned to the same number of characters, in order not to
222            // break the layout
223            write!(writer, "{level:>5} ")?;
224            write!(writer, "{}", " ".repeat(self.indent.load(Ordering::Relaxed) as usize))
225        }
226
227        fn write_event<W: Write>(&self, writer: &mut W, event: &Event<'_>) -> std::io::Result<()> {
228            let now = Utc::now();
229
230            self.format_header(writer, now, event.metadata().level())?;
231
232            let mut field_values = FieldValues::default();
233            event.record(&mut field_values);
234
235            if let Some(msg) = &field_values.message {
236                write!(writer, "{msg}")?;
237            }
238
239            if !field_values.fields.is_empty() {
240                if field_values.message.is_some() {
241                    write!(writer, " ")?;
242                }
243                write!(writer, "[")?;
244                for (index, (name, value)) in field_values.fields.iter().enumerate() {
245                    write!(writer, "{name} = {value}")?;
246                    if index < field_values.fields.len() - 1 {
247                        write!(writer, ", ")?;
248                    }
249                }
250                write!(writer, "]")?;
251            }
252            write_location(writer, event.metadata())?;
253            writeln!(writer)?;
254            Ok(())
255        }
256
257        fn write_span<W: Write, S>(
258            &self,
259            writer: &mut W,
260            span: SpanRef<'_, S>,
261            field_values: Option<&FieldValues>,
262            action: SpanAction,
263        ) -> std::io::Result<()>
264        where
265            S: for<'lookup> LookupSpan<'lookup>,
266        {
267            let now = Utc::now();
268
269            self.format_header(writer, now, span.metadata().level())?;
270            match action {
271                SpanAction::Enter => {
272                    write!(writer, "> ")?;
273                }
274            }
275
276            fn write_fields<'a, I: IntoIterator<Item = &'a (&'a str, String)>, W: Write>(
277                writer: &mut W,
278                iter: I,
279            ) -> std::io::Result<()> {
280                let items = iter.into_iter().collect::<Vec<_>>();
281                if !items.is_empty() {
282                    write!(writer, " [")?;
283                    for (index, (name, value)) in items.iter().enumerate() {
284                        write!(writer, "{name} = {value}")?;
285                        if index < items.len() - 1 {
286                            write!(writer, ", ")?;
287                        }
288                    }
289                    write!(writer, "]")?;
290                }
291                Ok(())
292            }
293
294            // We handle steps specially. We instrument them dynamically in `Builder::ensure`,
295            // and we want to have custom name for each step span. But tracing doesn't allow setting
296            // dynamic span names. So we detect step spans here and override their name.
297            match span.metadata().target() {
298                // Executed step
299                STEP_SPAN_TARGET => {
300                    let name =
301                        field_values.and_then(|v| v.step_name.as_deref()).unwrap_or(span.name());
302                    write!(writer, "{name}")?;
303
304                    // There should be only one more field called `args`
305                    if let Some(values) = field_values {
306                        let field = &values.fields[0];
307                        write!(writer, " {{{}}}", field.1)?;
308                    }
309                    write_location(writer, span.metadata())?;
310                }
311                // Executed command
312                COMMAND_SPAN_TARGET => {
313                    write!(writer, "{}", span.name())?;
314                    if let Some(values) = field_values {
315                        write_fields(
316                            writer,
317                            values.fields.iter().filter(|(name, _)| *name != "location"),
318                        )?;
319                        write!(
320                            writer,
321                            " ({})",
322                            values.fields.iter().find(|(name, _)| *name == "location").unwrap().1
323                        )?;
324                    }
325                }
326                // Other span
327                _ => {
328                    write!(writer, "{}", span.name())?;
329                    if let Some(values) = field_values {
330                        write_fields(writer, values.fields.iter())?;
331                    }
332                    write_location(writer, span.metadata())?;
333                }
334            }
335
336            writeln!(writer)?;
337            Ok(())
338        }
339    }
340
341    fn write_location<W: Write>(
342        writer: &mut W,
343        metadata: &'static tracing::Metadata<'static>,
344    ) -> std::io::Result<()> {
345        use std::path::{Path, PathBuf};
346
347        if let Some(filename) = metadata.file() {
348            // Keep only the module name and file name to make it shorter
349            let filename: PathBuf = Path::new(filename)
350                .components()
351                // Take last two path components
352                .rev()
353                .take(2)
354                .collect::<Vec<_>>()
355                .into_iter()
356                .rev()
357                .collect();
358
359            write!(writer, " ({}", filename.display())?;
360            if let Some(line) = metadata.line() {
361                write!(writer, ":{line}")?;
362            }
363            write!(writer, ")")?;
364        }
365        Ok(())
366    }
367
368    impl<S> Layer<S> for TracingPrinter
369    where
370        S: Subscriber,
371        S: for<'lookup> LookupSpan<'lookup>,
372    {
373        fn on_new_span(&self, attrs: &tracing::span::Attributes<'_>, id: &Id, ctx: Context<'_, S>) {
374            // Record value of span fields
375            // Note that we do not implement changing values of span fields after they are created.
376            // For that we would also need to implement the `on_record` method
377            let mut field_values = FieldValues::default();
378            attrs.record(&mut field_values);
379
380            // We need to propagate the actual name of the span to the Chrome layer below, because
381            // it cannot access field values. We do that through extensions.
382            if attrs.metadata().target() == STEP_SPAN_TARGET
383                && let Some(step_name) = field_values.step_name.clone()
384            {
385                ctx.span(id).unwrap().extensions_mut().insert(StepNameExtension(step_name));
386            } else if attrs.metadata().target() == COMMAND_SPAN_TARGET
387                && let Some(cmd_name) = field_values.cmd_name.clone()
388            {
389                ctx.span(id).unwrap().extensions_mut().insert(CommandNameExtension(cmd_name));
390            }
391            self.span_values.lock().unwrap().insert(id.clone(), field_values);
392        }
393
394        fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) {
395            let mut writer = std::io::stderr().lock();
396            self.write_event(&mut writer, event).unwrap();
397        }
398
399        fn on_enter(&self, id: &Id, ctx: Context<'_, S>) {
400            if let Some(span) = ctx.span(id) {
401                let mut writer = std::io::stderr().lock();
402                let values = self.span_values.lock().unwrap();
403                let values = values.get(id);
404                self.write_span(&mut writer, span, values, SpanAction::Enter).unwrap();
405            }
406            self.indent.fetch_add(1, Ordering::Relaxed);
407        }
408
409        fn on_exit(&self, _id: &Id, _ctx: Context<'_, S>) {
410            self.indent.fetch_sub(1, Ordering::Relaxed);
411        }
412    }
413}
414
415#[cfg(feature = "tracing")]
416pub use inner::setup_tracing;