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,
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    unconditional_recursion,
82    unused,
83    unused_allocation,
84    unused_comparisons,
85    unused_parens,
86    while_true
87)]
88use once_cell::sync::OnceCell;
89use owo_colors::{style, Style};
90use std::env;
91use std::fmt;
92use std::fs::File;
93use std::io::{BufRead, BufReader};
94use tracing_error::SpanTrace;
95
96static THEME: OnceCell<Theme> = OnceCell::new();
97
98/// A struct that represents theme that is used by `color_spantrace`
99#[derive(Debug, Copy, Clone, Default)]
100pub struct Theme {
101    file: Style,
102    line_number: Style,
103    target: Style,
104    fields: Style,
105    active_line: Style,
106}
107
108impl Theme {
109    /// Create blank theme
110    pub fn new() -> Self {
111        Self::default()
112    }
113
114    /// A theme for a dark background. This is the default
115    pub fn dark() -> Self {
116        Self {
117            file: style().purple(),
118            line_number: style().purple(),
119            active_line: style().white().bold(),
120            target: style().bright_red(),
121            fields: style().bright_cyan(),
122        }
123    }
124
125    // XXX same as with `light` in `color_eyre`
126    /// A theme for a light background
127    pub fn light() -> Self {
128        Self {
129            file: style().purple(),
130            line_number: style().purple(),
131            target: style().red(),
132            fields: style().blue(),
133            active_line: style().bold(),
134        }
135    }
136
137    /// Styles printed paths
138    pub fn file(mut self, style: Style) -> Self {
139        self.file = style;
140        self
141    }
142
143    /// Styles the line number of a file
144    pub fn line_number(mut self, style: Style) -> Self {
145        self.line_number = style;
146        self
147    }
148
149    /// Styles the target (i.e. the module and function name, and so on)
150    pub fn target(mut self, style: Style) -> Self {
151        self.target = style;
152        self
153    }
154
155    /// Styles fields associated with a the `tracing::Span`.
156    pub fn fields(mut self, style: Style) -> Self {
157        self.fields = style;
158        self
159    }
160
161    /// Styles the selected line of displayed code
162    pub fn active_line(mut self, style: Style) -> Self {
163        self.active_line = style;
164        self
165    }
166}
167
168/// An error returned by `set_theme` if a global theme was already set
169#[derive(Debug)]
170pub struct InstallThemeError;
171
172impl fmt::Display for InstallThemeError {
173    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
174        f.write_str("could not set the provided `Theme` globally as another was already set")
175    }
176}
177
178impl std::error::Error for InstallThemeError {}
179
180/// Sets the global theme.
181///
182/// # Details
183///
184/// This can only be set once and otherwise fails.
185///
186/// **Note:** `colorize` sets the global theme implicitly, if it was not set already. So calling `colorize` and then `set_theme` fails
187pub fn set_theme(theme: Theme) -> Result<(), InstallThemeError> {
188    THEME.set(theme).map_err(|_| InstallThemeError)
189}
190
191/// Display a [`SpanTrace`] with colors and source
192///
193/// This function returns an `impl Display` type which can be then used in place of the original
194/// SpanTrace when writing it too the screen or buffer.
195///
196/// # Example
197///
198/// ```rust
199/// use tracing_error::SpanTrace;
200///
201/// let span_trace = SpanTrace::capture();
202/// println!("{}", color_spantrace::colorize(&span_trace));
203/// ```
204///
205/// **Note:** `colorize` sets the global theme implicitly, if it was not set already. So calling `colorize` and then `set_theme` fails
206///
207/// [`SpanTrace`]: https://docs.rs/tracing-error/*/tracing_error/struct.SpanTrace.html
208pub fn colorize(span_trace: &SpanTrace) -> impl fmt::Display + '_ {
209    let theme = *THEME.get_or_init(Theme::dark);
210    ColorSpanTrace { span_trace, theme }
211}
212
213struct ColorSpanTrace<'a> {
214    span_trace: &'a SpanTrace,
215    theme: Theme,
216}
217
218macro_rules! try_bool {
219    ($e:expr, $dest:ident) => {{
220        let ret = $e.unwrap_or_else(|e| $dest = Err(e));
221
222        if $dest.is_err() {
223            return false;
224        }
225
226        ret
227    }};
228}
229
230struct Frame<'a> {
231    metadata: &'a tracing_core::Metadata<'static>,
232    fields: &'a str,
233    theme: Theme,
234}
235
236/// Defines how verbose the backtrace is supposed to be.
237#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
238enum Verbosity {
239    /// Print a small message including the panic payload and the panic location.
240    Minimal,
241    /// Everything in `Minimal` and additionally print a backtrace.
242    Medium,
243    /// Everything in `Medium` plus source snippets for all backtrace locations.
244    Full,
245}
246
247impl Verbosity {
248    fn lib_from_env() -> Self {
249        Self::convert_env(
250            env::var("RUST_LIB_BACKTRACE")
251                .or_else(|_| env::var("RUST_BACKTRACE"))
252                .ok(),
253        )
254    }
255
256    fn convert_env(env: Option<String>) -> Self {
257        match env {
258            Some(ref x) if x == "full" => Verbosity::Full,
259            Some(_) => Verbosity::Medium,
260            None => Verbosity::Minimal,
261        }
262    }
263}
264
265impl Frame<'_> {
266    fn print(&self, i: u32, f: &mut fmt::Formatter<'_>) -> fmt::Result {
267        self.print_header(i, f)?;
268        self.print_fields(f)?;
269        self.print_source_location(f)?;
270        Ok(())
271    }
272
273    fn print_header(&self, i: u32, f: &mut fmt::Formatter<'_>) -> fmt::Result {
274        write!(
275            f,
276            "{:>2}: {}{}{}",
277            i,
278            self.theme.target.style(self.metadata.target()),
279            self.theme.target.style("::"),
280            self.theme.target.style(self.metadata.name()),
281        )
282    }
283
284    fn print_fields(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
285        if !self.fields.is_empty() {
286            write!(f, " with {}", self.theme.fields.style(self.fields))?;
287        }
288
289        Ok(())
290    }
291
292    fn print_source_location(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
293        if let Some(file) = self.metadata.file() {
294            let lineno = self
295                .metadata
296                .line()
297                .map_or("<unknown line>".to_owned(), |x| x.to_string());
298            write!(
299                f,
300                "\n    at {}:{}",
301                self.theme.file.style(file),
302                self.theme.line_number.style(lineno),
303            )?;
304        } else {
305            write!(f, "\n    at <unknown source file>")?;
306        }
307
308        Ok(())
309    }
310
311    fn print_source_if_avail(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
312        let (lineno, filename) = match (self.metadata.line(), self.metadata.file()) {
313            (Some(a), Some(b)) => (a, b),
314            // Without a line number and file name, we can't sensibly proceed.
315            _ => return Ok(()),
316        };
317
318        let file = match File::open(filename) {
319            Ok(file) => file,
320            // ignore io errors and just don't print the source
321            Err(_) => return Ok(()),
322        };
323
324        use std::fmt::Write;
325
326        // Extract relevant lines.
327        let reader = BufReader::new(file);
328        let start_line = lineno - 2.min(lineno - 1);
329        let surrounding_src = reader.lines().skip(start_line as usize - 1).take(5);
330        let mut buf = String::new();
331        for (line, cur_line_no) in surrounding_src.zip(start_line..) {
332            if cur_line_no == lineno {
333                write!(
334                    &mut buf,
335                    "{:>8} > {}",
336                    cur_line_no.to_string(),
337                    line.unwrap()
338                )?;
339                write!(f, "\n{}", self.theme.active_line.style(&buf))?;
340                buf.clear();
341            } else {
342                write!(f, "\n{:>8} │ {}", cur_line_no, line.unwrap())?;
343            }
344        }
345
346        Ok(())
347    }
348}
349
350impl fmt::Display for ColorSpanTrace<'_> {
351    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
352        let mut err = Ok(());
353        let mut span = 0;
354
355        writeln!(f, "{:━^80}\n", " SPANTRACE ")?;
356        self.span_trace.with_spans(|metadata, fields| {
357            let frame = Frame {
358                metadata,
359                fields,
360                theme: self.theme,
361            };
362
363            if span > 0 {
364                try_bool!(write!(f, "\n",), err);
365            }
366
367            try_bool!(frame.print(span, f), err);
368
369            if Verbosity::lib_from_env() == Verbosity::Full {
370                try_bool!(frame.print_source_if_avail(f), err);
371            }
372
373            span += 1;
374            true
375        });
376
377        err
378    }
379}