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 | )] |
86 | use once_cell::sync::OnceCell; |
87 | use owo_colors::{style, Style}; |
88 | use std::env; |
89 | use std::fmt; |
90 | use std::fs::File; |
91 | use std::io::{BufRead, BufReader}; |
92 | use tracing_error::SpanTrace; |
93 | |
94 | static THEME: OnceCell<Theme> = OnceCell::new(); |
95 | |
96 | /// A struct that represents theme that is used by `color_spantrace` |
97 | #[derive (Debug, Copy, Clone, Default)] |
98 | pub struct Theme { |
99 | file: Style, |
100 | line_number: Style, |
101 | target: Style, |
102 | fields: Style, |
103 | active_line: Style, |
104 | } |
105 | |
106 | impl 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)] |
168 | pub struct InstallThemeError; |
169 | |
170 | impl 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 | |
176 | impl 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 |
185 | pub 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 |
206 | pub 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 | |
211 | struct ColorSpanTrace<'a> { |
212 | span_trace: &'a SpanTrace, |
213 | theme: Theme, |
214 | } |
215 | |
216 | macro_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 | |
228 | struct 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)] |
236 | enum 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 | |
245 | impl 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 | |
263 | impl 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 | |
348 | impl 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 | |