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 | )] |
89 | use once_cell::sync::OnceCell; |
90 | use owo_colors::{style, Style}; |
91 | use std::env; |
92 | use std::fmt; |
93 | use std::fs::File; |
94 | use std::io::{BufRead, BufReader}; |
95 | use tracing_error::SpanTrace; |
96 | |
97 | static THEME: OnceCell<Theme> = OnceCell::new(); |
98 | |
99 | /// A struct that represents theme that is used by `color_spantrace` |
100 | #[derive (Debug, Copy, Clone, Default)] |
101 | pub struct Theme { |
102 | file: Style, |
103 | line_number: Style, |
104 | target: Style, |
105 | fields: Style, |
106 | active_line: Style, |
107 | } |
108 | |
109 | impl 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)] |
171 | pub struct InstallThemeError; |
172 | |
173 | impl 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 | |
179 | impl 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 |
188 | pub 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 |
209 | pub 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 | |
214 | struct ColorSpanTrace<'a> { |
215 | span_trace: &'a SpanTrace, |
216 | theme: Theme, |
217 | } |
218 | |
219 | macro_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 | |
231 | struct 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)] |
239 | enum 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 | |
248 | impl 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 | |
266 | impl 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 | |
351 | impl 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 | |