color_spantrace/
lib.rs

1//! A rust library for colorizing [`tracing_error::SpanTrace`] objects in the style
2//! of [`color-backtrace`].
3//!
4//! ## Setup
5//!
6//! Add the following to your `Cargo.toml`:
7//!
8//! ```toml
9//! [dependencies]
10//! color-spantrace = "0.2"
11//! tracing = "0.1"
12//! tracing-error = "0.2"
13//! tracing-subscriber = "0.3"
14//! ```
15//!
16//! Setup a tracing subscriber with an `ErrorLayer`:
17//!
18//! ```rust
19//! use tracing_error::ErrorLayer;
20//! use tracing_subscriber::{prelude::*, registry::Registry};
21//!
22//! Registry::default().with(ErrorLayer::default()).init();
23//! ```
24//!
25//! Create spans and enter them:
26//!
27//! ```rust
28//! use tracing::instrument;
29//! use tracing_error::SpanTrace;
30//!
31//! #[instrument]
32//! fn foo() -> SpanTrace {
33//!     SpanTrace::capture()
34//! }
35//! ```
36//!
37//! And finally colorize the `SpanTrace`:
38//!
39//! ```rust
40//! use tracing_error::SpanTrace;
41//!
42//! let span_trace = SpanTrace::capture();
43//! println!("{}", color_spantrace::colorize(&span_trace));
44//! ```
45//!
46//! ## Output Format
47//!
48//! Running `examples/color-spantrace-usage.rs` from the `color-spantrace` repo produces the following output:
49//!
50//! <pre><font color="#4E9A06"><b>❯</b></font> cargo run --example color-spantrace-usage
51//! <font color="#4E9A06"><b>    Finished</b></font> dev [unoptimized + debuginfo] target(s) in 0.04s
52//! <font color="#4E9A06"><b>     Running</b></font> `target/debug/examples/color-spantrace-usage`
53//! ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ SPANTRACE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
54//!
55//!  0: <font color="#F15D22">color-spantrace-usage::two</font>
56//!     at <font color="#75507B">examples/color-spantrace-usage.rs</font>:<font color="#75507B">18</font>
57//!  1: <font color="#F15D22">color-spantrace-usage::one</font> with <font color="#34E2E2">i=42</font>
58//!     at <font color="#75507B">examples/color-spantrace-usage.rs</font>:<font color="#75507B">13</font></pre>
59//!
60//! [`tracing_error::SpanTrace`]: https://docs.rs/tracing-error/*/tracing_error/struct.SpanTrace.html
61//! [`color-backtrace`]: https://github.com/athre0z/color-backtrace
62#![doc(html_root_url = "https://docs.rs/color-spantrace/0.2.1")]
63#![cfg_attr(
64    nightly_features,
65    feature(rustdoc_missing_doc_code_examples),
66    warn(rustdoc::missing_doc_code_examples)
67)]
68#![warn(
69    missing_debug_implementations,
70    missing_docs,
71    rust_2018_idioms,
72    unreachable_pub,
73    bad_style,
74    dead_code,
75    improper_ctypes,
76    non_shorthand_field_patterns,
77    no_mangle_generic_items,
78    overflowing_literals,
79    path_statements,
80    patterns_in_fns_without_body,
81    private_in_public,
82    unconditional_recursion,
83    unused,
84    unused_allocation,
85    unused_comparisons,
86    unused_parens,
87    while_true
88)]
89use once_cell::sync::OnceCell;
90use owo_colors::{style, Style};
91use std::env;
92use std::fmt;
93use std::fs::File;
94use std::io::{BufRead, BufReader};
95use tracing_error::SpanTrace;
96
97static THEME: OnceCell<Theme> = OnceCell::new();
98
99/// A struct that represents theme that is used by `color_spantrace`
100#[derive(Debug, Copy, Clone, Default)]
101pub struct Theme {
102    file: Style,
103    line_number: Style,
104    target: Style,
105    fields: Style,
106    active_line: Style,
107}
108
109impl Theme {
110    /// Create blank theme
111    pub fn new() -> Self {
112        Self::default()
113    }
114
115    /// A theme for a dark background. This is the default
116    pub fn dark() -> Self {
117        Self {
118            file: style().purple(),
119            line_number: style().purple(),
120            active_line: style().white().bold(),
121            target: style().bright_red(),
122            fields: style().bright_cyan(),
123        }
124    }
125
126    // XXX same as with `light` in `color_eyre`
127    /// A theme for a light background
128    pub fn light() -> Self {
129        Self {
130            file: style().purple(),
131            line_number: style().purple(),
132            target: style().red(),
133            fields: style().blue(),
134            active_line: style().bold(),
135        }
136    }
137
138    /// Styles printed paths
139    pub fn file(mut self, style: Style) -> Self {
140        self.file = style;
141        self
142    }
143
144    /// Styles the line number of a file
145    pub fn line_number(mut self, style: Style) -> Self {
146        self.line_number = style;
147        self
148    }
149
150    /// Styles the target (i.e. the module and function name, and so on)
151    pub fn target(mut self, style: Style) -> Self {
152        self.target = style;
153        self
154    }
155
156    /// Styles fields associated with a the `tracing::Span`.
157    pub fn fields(mut self, style: Style) -> Self {
158        self.fields = style;
159        self
160    }
161
162    /// Styles the selected line of displayed code
163    pub fn active_line(mut self, style: Style) -> Self {
164        self.active_line = style;
165        self
166    }
167}
168
169/// An error returned by `set_theme` if a global theme was already set
170#[derive(Debug)]
171pub struct InstallThemeError;
172
173impl fmt::Display for InstallThemeError {
174    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
175        f.write_str("could not set the provided `Theme` globally as another was already set")
176    }
177}
178
179impl std::error::Error for InstallThemeError {}
180
181/// Sets the global theme.
182///
183/// # Details
184///
185/// This can only be set once and otherwise fails.
186///
187/// **Note:** `colorize` sets the global theme implicitly, if it was not set already. So calling `colorize` and then `set_theme` fails
188pub fn set_theme(theme: Theme) -> Result<(), InstallThemeError> {
189    THEME.set(theme).map_err(|_| InstallThemeError)
190}
191
192/// Display a [`SpanTrace`] with colors and source
193///
194/// This function returns an `impl Display` type which can be then used in place of the original
195/// SpanTrace when writing it too the screen or buffer.
196///
197/// # Example
198///
199/// ```rust
200/// use tracing_error::SpanTrace;
201///
202/// let span_trace = SpanTrace::capture();
203/// println!("{}", color_spantrace::colorize(&span_trace));
204/// ```
205///
206/// **Note:** `colorize` sets the global theme implicitly, if it was not set already. So calling `colorize` and then `set_theme` fails
207///
208/// [`SpanTrace`]: https://docs.rs/tracing-error/*/tracing_error/struct.SpanTrace.html
209pub fn colorize(span_trace: &SpanTrace) -> impl fmt::Display + '_ {
210    let theme = *THEME.get_or_init(Theme::dark);
211    ColorSpanTrace { span_trace, theme }
212}
213
214struct ColorSpanTrace<'a> {
215    span_trace: &'a SpanTrace,
216    theme: Theme,
217}
218
219macro_rules! try_bool {
220    ($e:expr, $dest:ident) => {{
221        let ret = $e.unwrap_or_else(|e| $dest = Err(e));
222
223        if $dest.is_err() {
224            return false;
225        }
226
227        ret
228    }};
229}
230
231struct Frame<'a> {
232    metadata: &'a tracing_core::Metadata<'static>,
233    fields: &'a str,
234    theme: Theme,
235}
236
237/// Defines how verbose the backtrace is supposed to be.
238#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
239enum Verbosity {
240    /// Print a small message including the panic payload and the panic location.
241    Minimal,
242    /// Everything in `Minimal` and additionally print a backtrace.
243    Medium,
244    /// Everything in `Medium` plus source snippets for all backtrace locations.
245    Full,
246}
247
248impl Verbosity {
249    fn lib_from_env() -> Self {
250        Self::convert_env(
251            env::var("RUST_LIB_BACKTRACE")
252                .or_else(|_| env::var("RUST_BACKTRACE"))
253                .ok(),
254        )
255    }
256
257    fn convert_env(env: Option<String>) -> Self {
258        match env {
259            Some(ref x) if x == "full" => Verbosity::Full,
260            Some(_) => Verbosity::Medium,
261            None => Verbosity::Minimal,
262        }
263    }
264}
265
266impl Frame<'_> {
267    fn print(&self, i: u32, f: &mut fmt::Formatter<'_>) -> fmt::Result {
268        self.print_header(i, f)?;
269        self.print_fields(f)?;
270        self.print_source_location(f)?;
271        Ok(())
272    }
273
274    fn print_header(&self, i: u32, f: &mut fmt::Formatter<'_>) -> fmt::Result {
275        write!(
276            f,
277            "{:>2}: {}{}{}",
278            i,
279            self.theme.target.style(self.metadata.target()),
280            self.theme.target.style("::"),
281            self.theme.target.style(self.metadata.name()),
282        )
283    }
284
285    fn print_fields(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
286        if !self.fields.is_empty() {
287            write!(f, " with {}", self.theme.fields.style(self.fields))?;
288        }
289
290        Ok(())
291    }
292
293    fn print_source_location(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
294        if let Some(file) = self.metadata.file() {
295            let lineno = self
296                .metadata
297                .line()
298                .map_or("<unknown line>".to_owned(), |x| x.to_string());
299            write!(
300                f,
301                "\n    at {}:{}",
302                self.theme.file.style(file),
303                self.theme.line_number.style(lineno),
304            )?;
305        } else {
306            write!(f, "\n    at <unknown source file>")?;
307        }
308
309        Ok(())
310    }
311
312    fn print_source_if_avail(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
313        let (lineno, filename) = match (self.metadata.line(), self.metadata.file()) {
314            (Some(a), Some(b)) => (a, b),
315            // Without a line number and file name, we can't sensibly proceed.
316            _ => return Ok(()),
317        };
318
319        let file = match File::open(filename) {
320            Ok(file) => file,
321            // ignore io errors and just don't print the source
322            Err(_) => return Ok(()),
323        };
324
325        use std::fmt::Write;
326
327        // Extract relevant lines.
328        let reader = BufReader::new(file);
329        let start_line = lineno - 2.min(lineno - 1);
330        let surrounding_src = reader.lines().skip(start_line as usize - 1).take(5);
331        let mut buf = String::new();
332        for (line, cur_line_no) in surrounding_src.zip(start_line..) {
333            if cur_line_no == lineno {
334                write!(
335                    &mut buf,
336                    "{:>8} > {}",
337                    cur_line_no.to_string(),
338                    line.unwrap()
339                )?;
340                write!(f, "\n{}", self.theme.active_line.style(&buf))?;
341                buf.clear();
342            } else {
343                write!(f, "\n{:>8} │ {}", cur_line_no, line.unwrap())?;
344            }
345        }
346
347        Ok(())
348    }
349}
350
351impl fmt::Display for ColorSpanTrace<'_> {
352    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
353        let mut err = Ok(());
354        let mut span = 0;
355
356        writeln!(f, "{:━^80}\n", " SPANTRACE ")?;
357        self.span_trace.with_spans(|metadata, fields| {
358            let frame = Frame {
359                metadata,
360                fields,
361                theme: self.theme,
362            };
363
364            if span > 0 {
365                try_bool!(write!(f, "\n",), err);
366            }
367
368            try_bool!(frame.print(span, f), err);
369
370            if Verbosity::lib_from_env() == Verbosity::Full {
371                try_bool!(frame.print_source_if_avail(f), err);
372            }
373
374            span += 1;
375            true
376        });
377
378        err
379    }
380}