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