blob: b1226ed7de7be6eb84ca62db1f49c30c968a9284 [file]
//! Wrapper macros for `tracing` macros to avoid having to write `cfg(feature = "tracing")`-gated
//! `debug!`/`trace!` everytime, e.g.
//!
//! ```rust,ignore (example)
//! #[cfg(feature = "tracing")]
//! trace!("...");
//! ```
//!
//! When `feature = "tracing"` is inactive, these macros expand to nothing.
#[macro_export]
macro_rules! trace {
($($tokens:tt)*) => {
#[cfg(feature = "tracing")]
::tracing::trace!($($tokens)*)
}
}
#[macro_export]
macro_rules! debug {
($($tokens:tt)*) => {
#[cfg(feature = "tracing")]
::tracing::debug!($($tokens)*)
}
}
#[macro_export]
macro_rules! warn {
($($tokens:tt)*) => {
#[cfg(feature = "tracing")]
::tracing::warn!($($tokens)*)
}
}
#[macro_export]
macro_rules! info {
($($tokens:tt)*) => {
#[cfg(feature = "tracing")]
::tracing::info!($($tokens)*)
}
}
#[macro_export]
macro_rules! error {
($($tokens:tt)*) => {
#[cfg(feature = "tracing")]
::tracing::error!($($tokens)*)
}
}
#[cfg(feature = "tracing")]
pub const IO_SPAN_TARGET: &str = "IO";
/// Create a tracing span around an I/O operation, if tracing is enabled.
/// Note that at least one tracing value field has to be passed to this macro, otherwise it will not
/// compile.
#[macro_export]
macro_rules! trace_io {
($name:expr, $($args:tt)*) => {
::tracing::trace_span!(
target: $crate::utils::tracing::IO_SPAN_TARGET,
$name,
$($args)*,
location = $crate::utils::tracing::format_location(*::std::panic::Location::caller())
).entered()
}
}
#[cfg(feature = "tracing")]
pub fn format_location(location: std::panic::Location<'static>) -> String {
format!("{}:{}", location.file(), location.line())
}
#[cfg(feature = "tracing")]
const COMMAND_SPAN_TARGET: &str = "COMMAND";
#[cfg(feature = "tracing")]
pub fn trace_cmd(command: &crate::BootstrapCommand) -> tracing::span::EnteredSpan {
let fingerprint = command.fingerprint();
let location = command.get_created_location();
let location = format_location(location);
tracing::span!(
target: COMMAND_SPAN_TARGET,
tracing::Level::TRACE,
"cmd",
cmd_name = fingerprint.program_name().to_string(),
cmd = fingerprint.format_short_cmd(),
full_cmd = ?command,
location
)
.entered()
}
// # Note on `tracing` usage in bootstrap
//
// Due to the conditional compilation via the `tracing` cargo feature, this means that `tracing`
// usages in bootstrap need to be also gated behind the `tracing` feature:
//
// - `tracing` macros with log levels (`trace!`, `debug!`, `warn!`, `info`, `error`) should not be
// used *directly*. You should use the wrapped `tracing` macros which gate the actual invocations
// behind `feature = "tracing"`.
// - `tracing`'s `#[instrument(..)]` macro will need to be gated like `#![cfg_attr(feature =
// "tracing", instrument(..))]`.
#[cfg(feature = "tracing")]
mod inner {
use std::fmt::Debug;
use std::fs::File;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::sync::atomic::Ordering;
use chrono::{DateTime, Utc};
use tracing::field::{Field, Visit};
use tracing::{Event, Id, Level, Subscriber};
use tracing_subscriber::layer::{Context, SubscriberExt};
use tracing_subscriber::registry::{LookupSpan, SpanRef};
use tracing_subscriber::{EnvFilter, Layer};
use super::{COMMAND_SPAN_TARGET, IO_SPAN_TARGET};
use crate::STEP_SPAN_TARGET;
pub fn setup_tracing(env_name: &str) -> TracingGuard {
let filter = EnvFilter::from_env(env_name);
let registry = tracing_subscriber::registry().with(filter).with(TracingPrinter::default());
// When we're creating this layer, we do not yet know the location of the tracing output
// directory, because it is stored in the output directory determined after Config is parsed,
// but we already want to make tracing calls during (and before) config parsing.
// So we store the output into a temporary file, and then move it to the tracing directory
// before bootstrap ends.
let tempdir = tempfile::TempDir::new().expect("Cannot create temporary directory");
let chrome_tracing_path = tempdir.path().join("bootstrap-trace.json");
let file = std::io::BufWriter::new(File::create(&chrome_tracing_path).unwrap());
let chrome_layer = tracing_chrome::ChromeLayerBuilder::new()
.writer(file)
.include_args(true)
.name_fn(Box::new(|event_or_span| match event_or_span {
tracing_chrome::EventOrSpan::Event(e) => e.metadata().name().to_string(),
tracing_chrome::EventOrSpan::Span(s) => {
if s.metadata().target() == STEP_SPAN_TARGET
&& let Some(extension) = s.extensions().get::<StepNameExtension>()
{
extension.0.clone()
} else if s.metadata().target() == COMMAND_SPAN_TARGET
&& let Some(extension) = s.extensions().get::<CommandNameExtension>()
{
extension.0.clone()
} else {
s.metadata().name().to_string()
}
}
}));
let (chrome_layer, guard) = chrome_layer.build();
tracing::subscriber::set_global_default(registry.with(chrome_layer)).unwrap();
TracingGuard { guard, _tempdir: tempdir, chrome_tracing_path }
}
pub struct TracingGuard {
guard: tracing_chrome::FlushGuard,
_tempdir: tempfile::TempDir,
chrome_tracing_path: std::path::PathBuf,
}
impl TracingGuard {
pub fn copy_to_dir(self, dir: &std::path::Path) {
drop(self.guard);
crate::utils::helpers::move_file(
&self.chrome_tracing_path,
dir.join("chrome-trace.json"),
)
.unwrap();
}
}
/// Visitor that extracts both known and unknown field values from events and spans.
#[derive(Default)]
struct FieldValues {
/// Main event message
message: Option<String>,
/// Name of a recorded psna
step_name: Option<String>,
/// Short name of an executed command
cmd_name: Option<String>,
/// The rest of arbitrary event/span fields
fields: Vec<(&'static str, String)>,
}
impl Visit for FieldValues {
/// Record fields if possible using `record_str`, to avoid rendering simple strings with
/// their `Debug` representation, which adds extra quotes.
fn record_str(&mut self, field: &Field, value: &str) {
match field.name() {
"step_name" => {
self.step_name = Some(value.to_string());
}
"cmd_name" => {
self.cmd_name = Some(value.to_string());
}
name => {
self.fields.push((name, value.to_string()));
}
}
}
fn record_debug(&mut self, field: &Field, value: &dyn Debug) {
let formatted = format!("{value:?}");
match field.name() {
"message" => {
self.message = Some(formatted);
}
name => {
self.fields.push((name, formatted));
}
}
}
}
#[derive(Copy, Clone)]
enum SpanAction {
Enter,
}
/// Holds the name of a step span, stored in `tracing_subscriber`'s extensions.
struct StepNameExtension(String);
/// Holds the name of a command span, stored in `tracing_subscriber`'s extensions.
struct CommandNameExtension(String);
#[derive(Default)]
struct TracingPrinter {
indent: std::sync::atomic::AtomicU32,
span_values: std::sync::Mutex<std::collections::HashMap<tracing::Id, FieldValues>>,
}
impl TracingPrinter {
fn format_header<W: Write>(
&self,
writer: &mut W,
time: DateTime<Utc>,
level: &Level,
) -> std::io::Result<()> {
// Use a fixed-width timestamp without date, that shouldn't be very important
let timestamp = time.format("%H:%M:%S.%3f");
write!(writer, "{timestamp} ")?;
// Make sure that levels are aligned to the same number of characters, in order not to
// break the layout
write!(writer, "{level:>5} ")?;
write!(writer, "{}", " ".repeat(self.indent.load(Ordering::Relaxed) as usize))
}
fn write_event<W: Write>(&self, writer: &mut W, event: &Event<'_>) -> std::io::Result<()> {
let now = Utc::now();
self.format_header(writer, now, event.metadata().level())?;
let mut field_values = FieldValues::default();
event.record(&mut field_values);
if let Some(msg) = &field_values.message {
write!(writer, "{msg}")?;
}
if !field_values.fields.is_empty() {
if field_values.message.is_some() {
write!(writer, " ")?;
}
write!(writer, "[")?;
for (index, (name, value)) in field_values.fields.iter().enumerate() {
write!(writer, "{name} = {value}")?;
if index < field_values.fields.len() - 1 {
write!(writer, ", ")?;
}
}
write!(writer, "]")?;
}
write_location(writer, event.metadata())?;
writeln!(writer)?;
Ok(())
}
fn write_span<W: Write, S>(
&self,
writer: &mut W,
span: SpanRef<'_, S>,
field_values: Option<&FieldValues>,
action: SpanAction,
) -> std::io::Result<()>
where
S: for<'lookup> LookupSpan<'lookup>,
{
let now = Utc::now();
self.format_header(writer, now, span.metadata().level())?;
match action {
SpanAction::Enter => {
write!(writer, "> ")?;
}
}
fn write_fields<'a, I: IntoIterator<Item = &'a (&'a str, String)>, W: Write>(
writer: &mut W,
iter: I,
) -> std::io::Result<()> {
let items = iter.into_iter().collect::<Vec<_>>();
if !items.is_empty() {
write!(writer, " [")?;
for (index, (name, value)) in items.iter().enumerate() {
write!(writer, "{name} = {value}")?;
if index < items.len() - 1 {
write!(writer, ", ")?;
}
}
write!(writer, "]")?;
}
Ok(())
}
// Write fields while treating the "location" field specially, and assuming that it
// contains the source file location relevant to the span.
let write_with_location = |writer: &mut W| -> std::io::Result<()> {
if let Some(values) = field_values {
write_fields(
writer,
values.fields.iter().filter(|(name, _)| *name != "location"),
)?;
let location =
&values.fields.iter().find(|(name, _)| *name == "location").unwrap().1;
let (filename, line) = location.rsplit_once(':').unwrap();
let filename = shorten_filename(filename);
write!(writer, " ({filename}:{line})",)?;
}
Ok(())
};
// We handle steps specially. We instrument them dynamically in `Builder::ensure`,
// and we want to have custom name for each step span. But tracing doesn't allow setting
// dynamic span names. So we detect step spans here and override their name.
match span.metadata().target() {
// Executed step
STEP_SPAN_TARGET => {
let name =
field_values.and_then(|v| v.step_name.as_deref()).unwrap_or(span.name());
write!(writer, "{name}")?;
// There should be only one more field called `args`
if let Some(values) = field_values {
let field = &values.fields[0];
write!(writer, " {{{}}}", field.1)?;
}
write_location(writer, span.metadata())?;
}
// Executed command
COMMAND_SPAN_TARGET => {
write!(writer, "{}", span.name())?;
write_with_location(writer)?;
}
IO_SPAN_TARGET => {
write!(writer, "{}", span.name())?;
write_with_location(writer)?;
}
// Other span
_ => {
write!(writer, "{}", span.name())?;
if let Some(values) = field_values {
write_fields(writer, values.fields.iter())?;
}
write_location(writer, span.metadata())?;
}
}
writeln!(writer)?;
Ok(())
}
}
fn write_location<W: Write>(
writer: &mut W,
metadata: &'static tracing::Metadata<'static>,
) -> std::io::Result<()> {
if let Some(filename) = metadata.file() {
let filename = shorten_filename(filename);
write!(writer, " ({filename}")?;
if let Some(line) = metadata.line() {
write!(writer, ":{line}")?;
}
write!(writer, ")")?;
}
Ok(())
}
/// Keep only the module name and file name to make it shorter
fn shorten_filename(filename: &str) -> String {
Path::new(filename)
.components()
// Take last two path components
.rev()
.take(2)
.collect::<Vec<_>>()
.into_iter()
.rev()
.collect::<PathBuf>()
.display()
.to_string()
}
impl<S> Layer<S> for TracingPrinter
where
S: Subscriber,
S: for<'lookup> LookupSpan<'lookup>,
{
fn on_new_span(&self, attrs: &tracing::span::Attributes<'_>, id: &Id, ctx: Context<'_, S>) {
// Record value of span fields
// Note that we do not implement changing values of span fields after they are created.
// For that we would also need to implement the `on_record` method
let mut field_values = FieldValues::default();
attrs.record(&mut field_values);
// We need to propagate the actual name of the span to the Chrome layer below, because
// it cannot access field values. We do that through extensions.
if attrs.metadata().target() == STEP_SPAN_TARGET
&& let Some(step_name) = field_values.step_name.clone()
{
ctx.span(id).unwrap().extensions_mut().insert(StepNameExtension(step_name));
} else if attrs.metadata().target() == COMMAND_SPAN_TARGET
&& let Some(cmd_name) = field_values.cmd_name.clone()
{
ctx.span(id).unwrap().extensions_mut().insert(CommandNameExtension(cmd_name));
}
self.span_values.lock().unwrap().insert(id.clone(), field_values);
}
fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) {
let mut writer = std::io::stderr().lock();
self.write_event(&mut writer, event).unwrap();
}
fn on_enter(&self, id: &Id, ctx: Context<'_, S>) {
if let Some(span) = ctx.span(id) {
let mut writer = std::io::stderr().lock();
let values = self.span_values.lock().unwrap();
let values = values.get(id);
self.write_span(&mut writer, span, values, SpanAction::Enter).unwrap();
}
self.indent.fetch_add(1, Ordering::Relaxed);
}
fn on_exit(&self, _id: &Id, _ctx: Context<'_, S>) {
self.indent.fetch_sub(1, Ordering::Relaxed);
}
}
}
#[cfg(feature = "tracing")]
pub use inner::setup_tracing;