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(data:"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 = *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 envResult::var("RUST_LIB_BACKTRACE")
252 .or_else(|_| env::var(key:"RUST_BACKTRACE"))
253 .ok(),
254 )
255 }
256
257 fn convert_env(env: Option<String>) -> Self {
258 match env {
259 Some(ref x: &String) 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}
381