| 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 | |