Skip to main content

uv_logging/
lib.rs

1use std::fmt;
2
3use jiff::Timestamp;
4use owo_colors::OwoColorize;
5use tracing::{Event, Subscriber, field::Field};
6use tracing_subscriber::field::MakeExt;
7use tracing_subscriber::fmt::format::{self, Writer};
8use tracing_subscriber::fmt::{FmtContext, FormatEvent, FormatFields};
9use tracing_subscriber::registry::LookupSpan;
10
11/// The style of a uv logging line.
12pub struct UvFormat {
13    pub display_timestamp: bool,
14    pub display_level: bool,
15    pub show_spans: bool,
16}
17
18impl Default for UvFormat {
19    /// Regardless of the tracing level, show messages without any adornment.
20    fn default() -> Self {
21        Self {
22            display_timestamp: false,
23            display_level: true,
24            show_spans: false,
25        }
26    }
27}
28
29/// See <https://docs.rs/tracing-subscriber/0.3.18/src/tracing_subscriber/fmt/format/mod.rs.html#1026-1156>
30impl<S, N> FormatEvent<S, N> for UvFormat
31where
32    S: Subscriber + for<'a> LookupSpan<'a>,
33    N: for<'a> FormatFields<'a> + 'static,
34{
35    fn format_event(
36        &self,
37        ctx: &FmtContext<'_, S, N>,
38        mut writer: Writer<'_>,
39        event: &Event<'_>,
40    ) -> fmt::Result {
41        let meta = event.metadata();
42        let ansi = writer.has_ansi_escapes();
43
44        if self.display_timestamp {
45            if ansi {
46                write!(writer, "{} ", Timestamp::now().dimmed())?;
47            } else {
48                write!(writer, "{} ", Timestamp::now())?;
49            }
50        }
51
52        if self.display_level {
53            let level = meta.level();
54            // Same colors as tracing
55            if ansi {
56                match *level {
57                    tracing::Level::TRACE => write!(writer, "{} ", level.purple())?,
58                    tracing::Level::DEBUG => write!(writer, "{} ", level.blue())?,
59                    tracing::Level::INFO => write!(writer, "{} ", level.green())?,
60                    tracing::Level::WARN => write!(writer, "{} ", level.yellow())?,
61                    tracing::Level::ERROR => write!(writer, "{} ", level.red())?,
62                }
63            } else {
64                write!(writer, "{level} ")?;
65            }
66        }
67
68        if self.show_spans {
69            let span = event.parent();
70            let mut seen = false;
71
72            let span = span
73                .and_then(|id| ctx.span(id))
74                .or_else(|| ctx.lookup_current());
75
76            let scope = span.into_iter().flat_map(|span| span.scope().from_root());
77
78            for span in scope {
79                seen = true;
80                if ansi {
81                    write!(writer, "{}:", span.metadata().name().bold())?;
82                } else {
83                    write!(writer, "{}:", span.metadata().name())?;
84                }
85            }
86
87            if seen {
88                writer.write_char(' ')?;
89            }
90        }
91
92        ctx.field_format().format_fields(writer.by_ref(), event)?;
93
94        writeln!(writer)
95    }
96}
97
98/// Return the field formatter for uv logging.
99///
100/// The event formatter is responsible for uv's own log colors, such as the level prefix. Field
101/// values can come from arbitrary `Display` or `Debug` implementations, so strip any ANSI escape
102/// sequences there before writing them to the log line.
103pub fn uv_fields() -> impl for<'writer> FormatFields<'writer> {
104    format::debug_fn(format_field)
105        .display_messages()
106        .delimited(" ")
107}
108
109fn format_field(writer: &mut Writer<'_>, field: &Field, value: &dyn fmt::Debug) -> fmt::Result {
110    // NOTE: The various cases in this function match tracing-subscriber's default field formatting.
111    // See: https://docs.rs/tracing-subscriber/0.3.23/src/tracing_subscriber/fmt/format/mod.rs.html#1303-1338
112
113    let field = field.name();
114    if field.starts_with("log.") {
115        return Ok(());
116    }
117
118    let value = format!("{value:?}");
119    let value = anstream::adapter::strip_str(&value);
120
121    if field == "message" {
122        write!(writer, "{value}")
123    } else {
124        write!(
125            writer,
126            "{}={value}",
127            // Render `type=...` instead of `r#type=...`.
128            field.strip_prefix("r#").unwrap_or(field)
129        )
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use tracing::{Callsite, Event, Level, field::Value, metadata::Kind};
136    use tracing_subscriber::fmt::FormatFields;
137    use tracing_subscriber::fmt::format::Writer;
138
139    use super::uv_fields;
140
141    #[test]
142    fn strips_ansi_from_message_fields() {
143        let callsite = tracing::callsite! {
144            name: "event",
145            kind: Kind::EVENT,
146            level: Level::TRACE,
147            fields: message
148        };
149        let metadata = callsite.metadata();
150        let message = format_args!("Error trace: {}", "\x1b[36m\x1b[1mhint\x1b[0m");
151        let values = [Some(&message as &dyn Value)];
152        let fields = metadata.fields().value_set_all(&values);
153        let event = Event::new(metadata, &fields);
154        let mut output = String::new();
155
156        uv_fields()
157            .format_fields(Writer::new(&mut output), event)
158            .expect("field formatting should succeed");
159
160        assert_eq!(output, "Error trace: hint");
161    }
162}